runner.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. 'use strict';
  2. const path = require('path');
  3. const util = require('util');
  4. const { chromium } = require('playwright');
  5. const TIMEOUT_MILLISECONDS = 60000;
  6. function initMocha(reporter) {
  7. console.log = (console => {
  8. const log = console.log.bind(console);
  9. return (...args) => args.length ? log(...args) : log('');
  10. })(console);
  11. function shimMochaInstance(m) {
  12. const originalReporter = m.reporter.bind(m);
  13. let reporterIsChanged = false;
  14. m.reporter = (...args) => {
  15. reporterIsChanged = true;
  16. originalReporter(...args);
  17. };
  18. const run = m.run.bind(m);
  19. m.run = () => {
  20. const all = [], pending = [], failures = [], passes = [];
  21. function error(err) {
  22. if (!err) return {};
  23. let res = {};
  24. Object.getOwnPropertyNames(err).forEach(key => res[key] = err[key]);
  25. return res;
  26. }
  27. function clean(test) {
  28. return {
  29. title: test.title,
  30. fullTitle: test.fullTitle(),
  31. duration: test.duration,
  32. err: error(test.err)
  33. };
  34. }
  35. function result(stats) {
  36. return {
  37. result: {
  38. stats: {
  39. tests: all.length,
  40. passes: passes.length,
  41. pending: pending.length,
  42. failures: failures.length,
  43. start: stats.start.toISOString(),
  44. end: stats.end.toISOString(),
  45. duration: stats.duration
  46. },
  47. tests: all.map(clean),
  48. pending: pending.map(clean),
  49. failures: failures.map(clean),
  50. passes: passes.map(clean)
  51. },
  52. coverage: window.__coverage__
  53. };
  54. }
  55. function setResult() {
  56. !window.__mochaResult__ && (window.__mochaResult__ = result(this.stats));
  57. }
  58. !reporterIsChanged && m.setup({
  59. reporter: Mocha.reporters[reporter] || Mocha.reporters.spec
  60. });
  61. const runner = run(() => setTimeout(() => setResult.call(runner), 0))
  62. .on('pass', test => { passes.push(test); all.push(test); })
  63. .on('fail', test => { failures.push(test); all.push(test); })
  64. .on('pending', test => { pending.push(test); all.push(test); })
  65. .on('end', setResult);
  66. return runner;
  67. };
  68. }
  69. function shimMochaProcess(M) {
  70. // Mocha needs a process.stdout.write in order to change the cursor position.
  71. !M.process && (M.process = {});
  72. !M.process.stdout && (M.process.stdout = {});
  73. M.process.stdout.write = data => console.log('stdout:', data);
  74. M.reporters.Base.useColors = true;
  75. M.reporters.none = function None(runner) {
  76. M.reporters.Base.call(this, runner);
  77. };
  78. }
  79. Object.defineProperty(window, 'mocha', {
  80. get: function() { return undefined },
  81. set: function(m) {
  82. shimMochaInstance(m);
  83. delete window.mocha;
  84. window.mocha = m
  85. },
  86. configurable: true
  87. })
  88. Object.defineProperty(window, 'Mocha', {
  89. get: function() { return undefined },
  90. set: function(m) {
  91. shimMochaProcess(m);
  92. delete window.Mocha;
  93. window.Mocha = m;
  94. },
  95. configurable: true
  96. });
  97. }
  98. function configureViewport(width, height, page) {
  99. if (!width && !height) return page;
  100. let viewport = page.viewport();
  101. width && (viewport.width = width);
  102. height && (viewport.height = height);
  103. return page.setViewport(viewport).then(() => page);
  104. }
  105. function handleConsole(msg) {
  106. const args = msg.args() || [];
  107. Promise.all(args.map(a => a.jsonValue().catch(error => {
  108. console.warn('Failed to retrieve JSON value from argument:', error);
  109. return '';
  110. })))
  111. .then(args => {
  112. // process stdout stub
  113. let isStdout = args[0] === 'stdout:';
  114. isStdout && (args = args.slice(1));
  115. let msg = util.format(...args);
  116. !isStdout && msg && (msg += '\n');
  117. process.stdout.write(msg);
  118. });
  119. }
  120. function prepareUrl(filePath) {
  121. if (/^[a-zA-Z]+:\/\//.test(filePath)) {
  122. // path is URL
  123. return filePath;
  124. }
  125. // local path
  126. let resolvedPath = path.resolve(filePath);
  127. return `file://${resolvedPath}`;
  128. }
  129. exports.runner = function ({ file, reporter, timeout, width, height, args, executablePath, visible, polling }) {
  130. return new Promise(resolve => {
  131. // validate options
  132. if (!file) {
  133. throw new Error('Test page path is required.');
  134. }
  135. args = [].concat(args || []).map(arg => '--' + arg);
  136. !timeout && (timeout = TIMEOUT_MILLISECONDS);
  137. /^\d+$/.test(polling) && (polling = parseInt(polling));
  138. const url = prepareUrl(file);
  139. const options = {
  140. ignoreHTTPSErrors: true,
  141. headless: !visible,
  142. executablePath,
  143. args
  144. };
  145. const result = chromium.launch(options)
  146. .then(browser => browser.newContext()
  147. .then(context => context.newPage()
  148. .then(page => {
  149. if (width || height) {
  150. return page.setViewportSize({ width: width || 800, height: height || 600 }).then(() => page);
  151. }
  152. return page;
  153. })
  154. .then(page => {
  155. page.on('console', handleConsole);
  156. page.on('dialog', dialog => dialog.dismiss());
  157. page.on('pageerror', err => console.error(err));
  158. return page.addInitScript(initMocha, reporter)
  159. .then(() => page.goto(url))
  160. .then(() => page.waitForFunction(() => window.__mochaResult__, { timeout, polling }))
  161. .then(() => page.evaluate(() => window.__mochaResult__))
  162. .then(result => {
  163. if (!result) {
  164. throw new Error('Mocha results not found after waiting. The tests may not have run correctly.');
  165. }
  166. // Close browser before resolving result
  167. return browser.close().then(() => result);
  168. });
  169. })
  170. )
  171. );
  172. resolve(result);
  173. });
  174. };