base.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.colors = exports.BaseReporter = void 0;
  6. exports.formatError = formatError;
  7. exports.formatFailure = formatFailure;
  8. exports.formatResultFailure = formatResultFailure;
  9. exports.formatTestTitle = formatTestTitle;
  10. exports.kOutputSymbol = exports.isTTY = void 0;
  11. exports.prepareErrorStack = prepareErrorStack;
  12. exports.relativeFilePath = relativeFilePath;
  13. exports.separator = separator;
  14. exports.stepSuffix = stepSuffix;
  15. exports.stripAnsiEscapes = stripAnsiEscapes;
  16. exports.ttyWidth = void 0;
  17. var _utilsBundle = require("playwright-core/lib/utilsBundle");
  18. var _path = _interopRequireDefault(require("path"));
  19. var _utils = require("playwright-core/lib/utils");
  20. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  21. /**
  22. * Copyright (c) Microsoft Corporation.
  23. *
  24. * Licensed under the Apache License, Version 2.0 (the "License");
  25. * you may not use this file except in compliance with the License.
  26. * You may obtain a copy of the License at
  27. *
  28. * http://www.apache.org/licenses/LICENSE-2.0
  29. *
  30. * Unless required by applicable law or agreed to in writing, software
  31. * distributed under the License is distributed on an "AS IS" BASIS,
  32. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  33. * See the License for the specific language governing permissions and
  34. * limitations under the License.
  35. */
  36. const kOutputSymbol = exports.kOutputSymbol = Symbol('output');
  37. const isTTY = exports.isTTY = !!process.env.PWTEST_TTY_WIDTH || process.stdout.isTTY;
  38. const ttyWidth = exports.ttyWidth = process.env.PWTEST_TTY_WIDTH ? parseInt(process.env.PWTEST_TTY_WIDTH, 10) : process.stdout.columns || 0;
  39. let useColors = isTTY;
  40. if (process.env.DEBUG_COLORS === '0' || process.env.DEBUG_COLORS === 'false' || process.env.FORCE_COLOR === '0' || process.env.FORCE_COLOR === 'false') useColors = false;else if (process.env.DEBUG_COLORS || process.env.FORCE_COLOR) useColors = true;
  41. const colors = exports.colors = useColors ? _utilsBundle.colors : {
  42. bold: t => t,
  43. cyan: t => t,
  44. dim: t => t,
  45. gray: t => t,
  46. green: t => t,
  47. red: t => t,
  48. yellow: t => t,
  49. enabled: false
  50. };
  51. class BaseReporter {
  52. constructor(options = {}) {
  53. this.config = void 0;
  54. this.suite = void 0;
  55. this.totalTestCount = 0;
  56. this.result = void 0;
  57. this.fileDurations = new Map();
  58. this._omitFailures = void 0;
  59. this._fatalErrors = [];
  60. this._failureCount = 0;
  61. this._omitFailures = options.omitFailures || false;
  62. }
  63. version() {
  64. return 'v2';
  65. }
  66. onConfigure(config) {
  67. this.config = config;
  68. }
  69. onBegin(suite) {
  70. this.suite = suite;
  71. this.totalTestCount = suite.allTests().length;
  72. }
  73. onStdOut(chunk, test, result) {
  74. this._appendOutput({
  75. chunk,
  76. type: 'stdout'
  77. }, result);
  78. }
  79. onStdErr(chunk, test, result) {
  80. this._appendOutput({
  81. chunk,
  82. type: 'stderr'
  83. }, result);
  84. }
  85. _appendOutput(output, result) {
  86. if (!result) return;
  87. result[kOutputSymbol] = result[kOutputSymbol] || [];
  88. result[kOutputSymbol].push(output);
  89. }
  90. onTestBegin(test, result) {}
  91. onTestEnd(test, result) {
  92. if (result.status !== 'skipped' && result.status !== test.expectedStatus) ++this._failureCount;
  93. // Ignore any tests that are run in parallel.
  94. for (let suite = test.parent; suite; suite = suite.parent) {
  95. if (suite._parallelMode === 'parallel') return;
  96. }
  97. const projectName = test.titlePath()[1];
  98. const relativePath = relativeTestPath(this.config, test);
  99. const fileAndProject = (projectName ? `[${projectName}] › ` : '') + relativePath;
  100. const duration = this.fileDurations.get(fileAndProject) || 0;
  101. this.fileDurations.set(fileAndProject, duration + result.duration);
  102. }
  103. onError(error) {
  104. this._fatalErrors.push(error);
  105. }
  106. async onEnd(result) {
  107. this.result = result;
  108. }
  109. onStepBegin(test, result, step) {}
  110. onStepEnd(test, result, step) {}
  111. async onExit() {}
  112. printsToStdio() {
  113. return true;
  114. }
  115. fitToScreen(line, prefix) {
  116. if (!ttyWidth) {
  117. // Guard against the case where we cannot determine available width.
  118. return line;
  119. }
  120. return fitToWidth(line, ttyWidth, prefix);
  121. }
  122. generateStartingMessage() {
  123. var _this$config$metadata;
  124. const jobs = (_this$config$metadata = this.config.metadata.actualWorkers) !== null && _this$config$metadata !== void 0 ? _this$config$metadata : this.config.workers;
  125. const shardDetails = this.config.shard ? `, shard ${this.config.shard.current} of ${this.config.shard.total}` : '';
  126. if (!this.totalTestCount) return '';
  127. return '\n' + colors.dim('Running ') + this.totalTestCount + colors.dim(` test${this.totalTestCount !== 1 ? 's' : ''} using `) + jobs + colors.dim(` worker${jobs !== 1 ? 's' : ''}${shardDetails}`);
  128. }
  129. getSlowTests() {
  130. if (!this.config.reportSlowTests) return [];
  131. const fileDurations = [...this.fileDurations.entries()];
  132. fileDurations.sort((a, b) => b[1] - a[1]);
  133. const count = Math.min(fileDurations.length, this.config.reportSlowTests.max || Number.POSITIVE_INFINITY);
  134. const threshold = this.config.reportSlowTests.threshold;
  135. return fileDurations.filter(([, duration]) => duration > threshold).slice(0, count);
  136. }
  137. generateSummaryMessage({
  138. didNotRun,
  139. skipped,
  140. expected,
  141. interrupted,
  142. unexpected,
  143. flaky,
  144. fatalErrors
  145. }) {
  146. const tokens = [];
  147. if (unexpected.length) {
  148. tokens.push(colors.red(` ${unexpected.length} failed`));
  149. for (const test of unexpected) tokens.push(colors.red(formatTestHeader(this.config, test, {
  150. indent: ' '
  151. })));
  152. }
  153. if (interrupted.length) {
  154. tokens.push(colors.yellow(` ${interrupted.length} interrupted`));
  155. for (const test of interrupted) tokens.push(colors.yellow(formatTestHeader(this.config, test, {
  156. indent: ' '
  157. })));
  158. }
  159. if (flaky.length) {
  160. tokens.push(colors.yellow(` ${flaky.length} flaky`));
  161. for (const test of flaky) tokens.push(colors.yellow(formatTestHeader(this.config, test, {
  162. indent: ' '
  163. })));
  164. }
  165. if (skipped) tokens.push(colors.yellow(` ${skipped} skipped`));
  166. if (didNotRun) tokens.push(colors.yellow(` ${didNotRun} did not run`));
  167. if (expected) tokens.push(colors.green(` ${expected} passed`) + colors.dim(` (${(0, _utilsBundle.ms)(this.result.duration)})`));
  168. if (this.result.status === 'timedout') tokens.push(colors.red(` Timed out waiting ${this.config.globalTimeout / 1000}s for the entire test run`));
  169. if (fatalErrors.length && expected + unexpected.length + interrupted.length + flaky.length > 0) tokens.push(colors.red(` ${fatalErrors.length === 1 ? '1 error was not a part of any test' : fatalErrors.length + ' errors were not a part of any test'}, see above for details`));
  170. return tokens.join('\n');
  171. }
  172. generateSummary() {
  173. let didNotRun = 0;
  174. let skipped = 0;
  175. let expected = 0;
  176. const interrupted = [];
  177. const interruptedToPrint = [];
  178. const unexpected = [];
  179. const flaky = [];
  180. this.suite.allTests().forEach(test => {
  181. switch (test.outcome()) {
  182. case 'skipped':
  183. {
  184. if (test.results.some(result => result.status === 'interrupted')) {
  185. if (test.results.some(result => !!result.error)) interruptedToPrint.push(test);
  186. interrupted.push(test);
  187. } else if (!test.results.length) {
  188. ++didNotRun;
  189. } else {
  190. ++skipped;
  191. }
  192. break;
  193. }
  194. case 'expected':
  195. ++expected;
  196. break;
  197. case 'unexpected':
  198. unexpected.push(test);
  199. break;
  200. case 'flaky':
  201. flaky.push(test);
  202. break;
  203. }
  204. });
  205. const failuresToPrint = [...unexpected, ...flaky, ...interruptedToPrint];
  206. return {
  207. didNotRun,
  208. skipped,
  209. expected,
  210. interrupted,
  211. unexpected,
  212. flaky,
  213. failuresToPrint,
  214. fatalErrors: this._fatalErrors
  215. };
  216. }
  217. epilogue(full) {
  218. const summary = this.generateSummary();
  219. const summaryMessage = this.generateSummaryMessage(summary);
  220. if (full && summary.failuresToPrint.length && !this._omitFailures) this._printFailures(summary.failuresToPrint);
  221. this._printSlowTests();
  222. this._printMaxFailuresReached();
  223. this._printSummary(summaryMessage);
  224. }
  225. _printFailures(failures) {
  226. console.log('');
  227. failures.forEach((test, index) => {
  228. console.log(formatFailure(this.config, test, {
  229. index: index + 1
  230. }).message);
  231. });
  232. }
  233. _printSlowTests() {
  234. const slowTests = this.getSlowTests();
  235. slowTests.forEach(([file, duration]) => {
  236. console.log(colors.yellow(' Slow test file: ') + file + colors.yellow(` (${(0, _utilsBundle.ms)(duration)})`));
  237. });
  238. if (slowTests.length) console.log(colors.yellow(' Consider splitting slow test files to speed up parallel execution'));
  239. }
  240. _printMaxFailuresReached() {
  241. if (!this.config.maxFailures) return;
  242. if (this._failureCount < this.config.maxFailures) return;
  243. console.log(colors.yellow(`Testing stopped early after ${this.config.maxFailures} maximum allowed failures.`));
  244. }
  245. _printSummary(summary) {
  246. if (summary.trim()) console.log(summary);
  247. }
  248. willRetry(test) {
  249. return test.outcome() === 'unexpected' && test.results.length <= test.retries;
  250. }
  251. }
  252. exports.BaseReporter = BaseReporter;
  253. function formatFailure(config, test, options = {}) {
  254. const {
  255. index,
  256. includeStdio,
  257. includeAttachments = true
  258. } = options;
  259. const lines = [];
  260. const title = formatTestTitle(config, test);
  261. const annotations = [];
  262. const header = formatTestHeader(config, test, {
  263. indent: ' ',
  264. index,
  265. mode: 'error'
  266. });
  267. lines.push(colors.red(header));
  268. for (const result of test.results) {
  269. const resultLines = [];
  270. const errors = formatResultFailure(test, result, ' ', colors.enabled);
  271. if (!errors.length) continue;
  272. const retryLines = [];
  273. if (result.retry) {
  274. retryLines.push('');
  275. retryLines.push(colors.gray(separator(` Retry #${result.retry}`)));
  276. }
  277. resultLines.push(...retryLines);
  278. resultLines.push(...errors.map(error => '\n' + error.message));
  279. if (includeAttachments) {
  280. for (let i = 0; i < result.attachments.length; ++i) {
  281. const attachment = result.attachments[i];
  282. const hasPrintableContent = attachment.contentType.startsWith('text/') && attachment.body;
  283. if (!attachment.path && !hasPrintableContent) continue;
  284. resultLines.push('');
  285. resultLines.push(colors.cyan(separator(` attachment #${i + 1}: ${attachment.name} (${attachment.contentType})`)));
  286. if (attachment.path) {
  287. const relativePath = _path.default.relative(process.cwd(), attachment.path);
  288. resultLines.push(colors.cyan(` ${relativePath}`));
  289. // Make this extensible
  290. if (attachment.name === 'trace') {
  291. const packageManagerCommand = (0, _utils.getPackageManagerExecCommand)();
  292. resultLines.push(colors.cyan(` Usage:`));
  293. resultLines.push('');
  294. resultLines.push(colors.cyan(` ${packageManagerCommand} playwright show-trace ${relativePath}`));
  295. resultLines.push('');
  296. }
  297. } else {
  298. if (attachment.contentType.startsWith('text/') && attachment.body) {
  299. let text = attachment.body.toString();
  300. if (text.length > 300) text = text.slice(0, 300) + '...';
  301. for (const line of text.split('\n')) resultLines.push(colors.cyan(` ${line}`));
  302. }
  303. }
  304. resultLines.push(colors.cyan(separator(' ')));
  305. }
  306. }
  307. const output = result[kOutputSymbol] || [];
  308. if (includeStdio && output.length) {
  309. const outputText = output.map(({
  310. chunk,
  311. type
  312. }) => {
  313. const text = chunk.toString('utf8');
  314. if (type === 'stderr') return colors.red(stripAnsiEscapes(text));
  315. return text;
  316. }).join('');
  317. resultLines.push('');
  318. resultLines.push(colors.gray(separator('--- Test output')) + '\n\n' + outputText + '\n' + separator());
  319. }
  320. for (const error of errors) {
  321. annotations.push({
  322. location: error.location,
  323. title,
  324. message: [header, ...retryLines, error.message].join('\n')
  325. });
  326. }
  327. lines.push(...resultLines);
  328. }
  329. lines.push('');
  330. return {
  331. message: lines.join('\n'),
  332. annotations
  333. };
  334. }
  335. function formatResultFailure(test, result, initialIndent, highlightCode) {
  336. const errorDetails = [];
  337. if (result.status === 'passed' && test.expectedStatus === 'failed') {
  338. errorDetails.push({
  339. message: indent(colors.red(`Expected to fail, but passed.`), initialIndent)
  340. });
  341. }
  342. if (result.status === 'interrupted') {
  343. errorDetails.push({
  344. message: indent(colors.red(`Test was interrupted.`), initialIndent)
  345. });
  346. }
  347. for (const error of result.errors) {
  348. const formattedError = formatError(error, highlightCode);
  349. errorDetails.push({
  350. message: indent(formattedError.message, initialIndent),
  351. location: formattedError.location
  352. });
  353. }
  354. return errorDetails;
  355. }
  356. function relativeFilePath(config, file) {
  357. return _path.default.relative(config.rootDir, file) || _path.default.basename(file);
  358. }
  359. function relativeTestPath(config, test) {
  360. return relativeFilePath(config, test.location.file);
  361. }
  362. function stepSuffix(step) {
  363. const stepTitles = step ? step.titlePath() : [];
  364. return stepTitles.map(t => ' › ' + t).join('');
  365. }
  366. function formatTestTitle(config, test, step, omitLocation = false) {
  367. var _step$location$line, _step$location, _step$location$column, _step$location2;
  368. // root, project, file, ...describes, test
  369. const [, projectName,, ...titles] = test.titlePath();
  370. let location;
  371. if (omitLocation) location = `${relativeTestPath(config, test)}`;else location = `${relativeTestPath(config, test)}:${(_step$location$line = step === null || step === void 0 ? void 0 : (_step$location = step.location) === null || _step$location === void 0 ? void 0 : _step$location.line) !== null && _step$location$line !== void 0 ? _step$location$line : test.location.line}:${(_step$location$column = step === null || step === void 0 ? void 0 : (_step$location2 = step.location) === null || _step$location2 === void 0 ? void 0 : _step$location2.column) !== null && _step$location$column !== void 0 ? _step$location$column : test.location.column}`;
  372. const projectTitle = projectName ? `[${projectName}] › ` : '';
  373. return `${projectTitle}${location} › ${titles.join(' › ')}${stepSuffix(step)}`;
  374. }
  375. function formatTestHeader(config, test, options = {}) {
  376. const title = formatTestTitle(config, test);
  377. const header = `${options.indent || ''}${options.index ? options.index + ') ' : ''}${title}`;
  378. let fullHeader = header;
  379. // Render the path to the deepest failing test.step.
  380. if (options.mode === 'error') {
  381. const stepPaths = new Set();
  382. for (const result of test.results.filter(r => !!r.errors.length)) {
  383. const stepPath = [];
  384. const visit = steps => {
  385. const errors = steps.filter(s => s.error);
  386. if (errors.length > 1) return;
  387. if (errors.length === 1 && errors[0].category === 'test.step') {
  388. stepPath.push(errors[0].title);
  389. visit(errors[0].steps);
  390. }
  391. };
  392. visit(result.steps);
  393. stepPaths.add(['', ...stepPath].join(' › '));
  394. }
  395. fullHeader = header + (stepPaths.size === 1 ? stepPaths.values().next().value : '');
  396. }
  397. return separator(fullHeader);
  398. }
  399. function formatError(error, highlightCode) {
  400. const message = error.message || error.value || '';
  401. const stack = error.stack;
  402. if (!stack && !error.location) return {
  403. message
  404. };
  405. const tokens = [];
  406. // Now that we filter out internals from our stack traces, we can safely render
  407. // the helper / original exception locations.
  408. const parsedStack = stack ? prepareErrorStack(stack) : undefined;
  409. tokens.push((parsedStack === null || parsedStack === void 0 ? void 0 : parsedStack.message) || message);
  410. if (error.snippet) {
  411. let snippet = error.snippet;
  412. if (!highlightCode) snippet = stripAnsiEscapes(snippet);
  413. tokens.push('');
  414. tokens.push(snippet);
  415. }
  416. if (parsedStack && parsedStack.stackLines.length) {
  417. tokens.push('');
  418. tokens.push(colors.dim(parsedStack.stackLines.join('\n')));
  419. }
  420. let location = error.location;
  421. if (parsedStack && !location) location = parsedStack.location;
  422. return {
  423. location,
  424. message: tokens.join('\n')
  425. };
  426. }
  427. function separator(text = '') {
  428. if (text) text += ' ';
  429. const columns = Math.min(100, ttyWidth || 100);
  430. return text + colors.dim('─'.repeat(Math.max(0, columns - text.length)));
  431. }
  432. function indent(lines, tab) {
  433. return lines.replace(/^(?=.+$)/gm, tab);
  434. }
  435. function prepareErrorStack(stack) {
  436. const lines = stack.split('\n');
  437. let firstStackLine = lines.findIndex(line => line.startsWith(' at '));
  438. if (firstStackLine === -1) firstStackLine = lines.length;
  439. const message = lines.slice(0, firstStackLine).join('\n');
  440. const stackLines = lines.slice(firstStackLine);
  441. let location;
  442. for (const line of stackLines) {
  443. const frame = (0, _utilsBundle.parseStackTraceLine)(line);
  444. if (!frame || !frame.file) continue;
  445. if (belongsToNodeModules(frame.file)) continue;
  446. location = {
  447. file: frame.file,
  448. column: frame.column || 0,
  449. line: frame.line || 0
  450. };
  451. break;
  452. }
  453. return {
  454. message,
  455. stackLines,
  456. location
  457. };
  458. }
  459. const ansiRegex = new RegExp('([\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~])))', 'g');
  460. function stripAnsiEscapes(str) {
  461. return str.replace(ansiRegex, '');
  462. }
  463. // Leaves enough space for the "prefix" to also fit.
  464. function fitToWidth(line, width, prefix) {
  465. const prefixLength = prefix ? stripAnsiEscapes(prefix).length : 0;
  466. width -= prefixLength;
  467. if (line.length <= width) return line;
  468. // Even items are plain text, odd items are control sequences.
  469. const parts = line.split(ansiRegex);
  470. const taken = [];
  471. for (let i = parts.length - 1; i >= 0; i--) {
  472. if (i % 2) {
  473. // Include all control sequences to preserve formatting.
  474. taken.push(parts[i]);
  475. } else {
  476. let part = parts[i].substring(parts[i].length - width);
  477. if (part.length < parts[i].length && part.length > 0) {
  478. // Add ellipsis if we are truncating.
  479. part = '\u2026' + part.substring(1);
  480. }
  481. taken.push(part);
  482. width -= part.length;
  483. }
  484. }
  485. return taken.reverse().join('');
  486. }
  487. function belongsToNodeModules(file) {
  488. return file.includes(`${_path.default.sep}node_modules${_path.default.sep}`);
  489. }