Home Reference Source Test

source/index.js

  1. import {packageOf} from '@tech_query/node-toolkit';
  2.  
  3. import {resolve} from 'url';
  4.  
  5. import WebServer from 'koapache';
  6.  
  7. import {watch} from 'chokidar';
  8.  
  9. import {toString} from 'qrcode';
  10.  
  11. import promisify from 'promisify-node';
  12.  
  13. const QRCode = promisify( toString );
  14.  
  15.  
  16.  
  17. const { env } = process, config = (packageOf('./test') || '').meta;
  18.  
  19. const NPM_command = env.npm_lifecycle_script;
  20.  
  21. var server, browser, page;
  22.  
  23.  
  24. /**
  25. * Wrapper of `Puppeteer` class
  26. */
  27. export default class PuppeteerBrowser {
  28. /**
  29. * @protected
  30. *
  31. * @param {string} [root] - Root path of the static site
  32. *
  33. * @return {Object} Server information
  34. */
  35. static async getServer(root) {
  36.  
  37. return server || (
  38. server = await (new WebServer(root || '.')).workerHost()
  39. );
  40. }
  41.  
  42. /**
  43. * @type {string}
  44. */
  45. static get browserName() {
  46.  
  47. return (env.npm_config_PUPPETEER_BROWSER || 'chrome').trim();
  48. }
  49.  
  50. /**
  51. * @type {string}
  52. */
  53. static get moduleName() {
  54.  
  55. return 'puppeteer' + (map => {
  56.  
  57. for (let name in map)
  58. if (name === this.browserName) return map[name];
  59.  
  60. return '';
  61. })({
  62. chrome: '',
  63. firefox: '-fx',
  64. IE: '-ie'
  65. });
  66. }
  67.  
  68. /**
  69. * @param {Object} [options] - https://pptr.dev/#?product=Puppeteer&version=v1.5.0&show=api-puppeteerlaunchoptions
  70. *
  71. * @return {Browser}
  72. */
  73. static async launch(options) {
  74.  
  75. const Puppeteer = (await import( this.moduleName )).default;
  76.  
  77. return await Puppeteer.launch( options );
  78. }
  79.  
  80. /**
  81. * @return {string} https://pptr.dev/#?product=Puppeteer&version=v1.5.0&show=api-puppeteerexecutablepath
  82. */
  83. static executablePath() {
  84.  
  85. return env['npm_config_' + this.browserName];
  86. }
  87.  
  88. /**
  89. * @protected
  90. *
  91. * @param {boolean} [visible] - Browser visibility
  92. * (Visible mode will run slowly for seeing clearly)
  93. * @return {Browser}
  94. */
  95. static async getBrowser(visible) {
  96.  
  97. if ( browser ) return browser;
  98.  
  99. visible = (visible != null) ?
  100. visible : NPM_command.includes('--inspect');
  101.  
  102. browser = await this.launch({
  103. executablePath: this.executablePath(),
  104. headless: ! visible,
  105. slowMo: visible ? 100 : 0
  106. });
  107.  
  108. return browser.on('disconnected', () => browser = page = null);
  109. }
  110.  
  111. /**
  112. * After files changed, the page will be focused & reloaded
  113. *
  114. * @param {string} path - Directory to watch recursively
  115. * @param {function} onChange - Call on files changed
  116. *
  117. * @return {FSWatcher}
  118. */
  119. static watch(path, onChange) {
  120.  
  121. var listen;
  122.  
  123. async function refresh() {
  124.  
  125. await onChange();
  126.  
  127. if ( page ) {
  128.  
  129. await page.bringToFront();
  130.  
  131. await page.reload();
  132.  
  133. console.info(`[ Reload ] ${page.url()}`);
  134. }
  135.  
  136. listen = false;
  137. }
  138.  
  139. return watch( path ).on('change', () => {
  140.  
  141. if (! listen) {
  142.  
  143. listen = true;
  144.  
  145. process.nextTick( refresh );
  146. }
  147. });
  148. }
  149.  
  150. /**
  151. * @param {?string} root - Root path to start Web server, default to be `process.cwd()`
  152. * @param {?string} path - Path to open Web page
  153. * @param {function} [fileChange] - Do something between files changed & page reload
  154. * (Browser will be visible)
  155. * @return {Page} Resolve after `DOMContentLoaded` event fired
  156. */
  157. static async getPage(root, path, fileChange) {
  158.  
  159. if ( page ) return page;
  160.  
  161. path = path || '.';
  162.  
  163. fileChange = (fileChange instanceof Function) ? fileChange : null;
  164.  
  165. const server = path.indexOf('http') && await this.getServer( root );
  166.  
  167. const URI = resolve(`http://${server.address}:${server.port}/`, path);
  168.  
  169. if ( fileChange ) console.info(await QRCode( URI ));
  170.  
  171. page = await (await this.getBrowser( fileChange )).newPage();
  172.  
  173. await page.on('close', () => page = null).goto(server ? URI : path, {
  174. waitUntil: 'domcontentloaded'
  175. });
  176.  
  177. if ( fileChange )
  178. this.watch(config.directories.lib || root, fileChange);
  179.  
  180. return page;
  181. }
  182. }