mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-15 14:11:50 +03:00
276 lines
11 KiB
JavaScript
276 lines
11 KiB
JavaScript
/**
|
|
* Copyright 2017 Google Inc. All rights reserved.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const colors = require('colors/safe');
|
|
const {MatchError} = require('./Matchers.js');
|
|
|
|
class Reporter {
|
|
constructor(runner, options = {}) {
|
|
const {
|
|
showSlowTests = 3,
|
|
showMarkedAsFailingTests = Infinity,
|
|
verbose = false,
|
|
summary = true,
|
|
} = options;
|
|
this._filePathToLines = new Map();
|
|
this._runner = runner;
|
|
this._showSlowTests = showSlowTests;
|
|
this._showMarkedAsFailingTests = showMarkedAsFailingTests;
|
|
this._verbose = verbose;
|
|
this._summary = summary;
|
|
this._testCounter = 0;
|
|
runner.on('started', this._onStarted.bind(this));
|
|
runner.on('finished', this._onFinished.bind(this));
|
|
runner.on('teststarted', this._onTestStarted.bind(this));
|
|
runner.on('testfinished', this._onTestFinished.bind(this));
|
|
}
|
|
|
|
_onStarted(runnableTests) {
|
|
this._testCounter = 0;
|
|
this._timestamp = Date.now();
|
|
const allTests = this._runner.tests();
|
|
if (allTests.length === runnableTests.length) {
|
|
console.log(`Running all ${colors.yellow(runnableTests.length)} tests on ${colors.yellow(this._runner.parallel())} worker${this._runner.parallel() > 1 ? 's' : ''}:\n`);
|
|
} else {
|
|
console.log(`Running ${colors.yellow(runnableTests.length)} focused tests out of total ${colors.yellow(allTests.length)} on ${colors.yellow(this._runner.parallel())} worker${this._runner.parallel() > 1 ? 's' : ''}`);
|
|
console.log('');
|
|
const focusedSuites = this._runner.focusedSuites().map(suite => ({
|
|
id: suite.location.filePath + ':' + suite.location.lineNumber + ':' + suite.location.columnNumber,
|
|
fullName: suite.fullName,
|
|
location: suite.location,
|
|
}));
|
|
const focusedTests = this._runner.focusedTests().map(test => ({
|
|
id: test.location.filePath + ':' + test.location.lineNumber + ':' + test.location.columnNumber,
|
|
fullName: test.fullName,
|
|
location: test.location,
|
|
}));
|
|
const focusedEntities = new Map([
|
|
...focusedSuites.map(suite => ([suite.id, suite])),
|
|
...focusedTests.map(test => ([test.id, test])),
|
|
]);
|
|
if (focusedEntities.size) {
|
|
console.log('Focused Suites and Tests:');
|
|
const entities = [...focusedEntities.values()];
|
|
for (let i = 0; i < entities.length; ++i)
|
|
console.log(` ${i + 1}) ${entities[i].fullName} (${formatLocation(entities[i].location)})`);
|
|
console.log('');
|
|
}
|
|
}
|
|
}
|
|
|
|
_printFailedResult(result) {
|
|
console.log(colors.red(`## ${result.result.toUpperCase()} ##`));
|
|
if (result.message) {
|
|
console.log('Message:');
|
|
console.log(` ${colors.red(result.message)}`);
|
|
}
|
|
|
|
for (let i = 0; i < result.errors.length; i++) {
|
|
const { message, error, workerId, tests } = result.errors[i];
|
|
console.log(`\n${colors.magenta('NON-TEST ERROR #' + i)}: ${message}`);
|
|
if (error && error.stack)
|
|
console.log(padLines(error.stack, 2));
|
|
const lastTests = tests.slice(tests.length - Math.min(10, tests.length));
|
|
if (lastTests.length)
|
|
console.log(`WORKER STATE`);
|
|
for (let j = 0; j < lastTests.length; j++)
|
|
this._printVerboseTestResult(j, lastTests[j], workerId);
|
|
}
|
|
console.log('');
|
|
console.log('');
|
|
}
|
|
|
|
_onFinished(result) {
|
|
this._printTestResults();
|
|
if (!result.ok())
|
|
this._printFailedResult(result);
|
|
process.exitCode = result.exitCode;
|
|
}
|
|
|
|
_printTestResults() {
|
|
// 2 newlines after completing all tests.
|
|
console.log('\n');
|
|
|
|
const failedTests = this._runner.failedTests();
|
|
if (this._summary && failedTests.length > 0) {
|
|
console.log('\nFailures:');
|
|
for (let i = 0; i < failedTests.length; ++i) {
|
|
const test = failedTests[i];
|
|
this._printVerboseTestResult(i + 1, test);
|
|
console.log('');
|
|
}
|
|
}
|
|
|
|
const skippedTests = this._runner.skippedTests();
|
|
const markedAsFailingTests = this._runner.markedAsFailingTests();
|
|
if (this._showMarkedAsFailingTests && this._summary && markedAsFailingTests.length) {
|
|
if (markedAsFailingTests.length > 0) {
|
|
console.log('\nMarked as failing:');
|
|
markedAsFailingTests.slice(0, this._showMarkedAsFailingTests).forEach((test, index) => {
|
|
console.log(`${index + 1}) ${test.fullName} (${formatLocation(test.location)})`);
|
|
});
|
|
}
|
|
if (this._showMarkedAsFailingTests < markedAsFailingTests.length) {
|
|
console.log('');
|
|
console.log(`... and ${colors.yellow(markedAsFailingTests.length - this._showMarkedAsFailingTests)} more marked as failing tests ...`);
|
|
}
|
|
}
|
|
|
|
if (this._showSlowTests) {
|
|
const slowTests = this._runner.passedTests().sort((a, b) => {
|
|
const aDuration = a.endTimestamp - a.startTimestamp;
|
|
const bDuration = b.endTimestamp - b.startTimestamp;
|
|
return bDuration - aDuration;
|
|
}).slice(0, this._showSlowTests);
|
|
console.log(`\nSlowest tests:`);
|
|
for (let i = 0; i < slowTests.length; ++i) {
|
|
const test = slowTests[i];
|
|
const duration = test.endTimestamp - test.startTimestamp;
|
|
console.log(` (${i + 1}) ${colors.yellow((duration / 1000) + 's')} - ${test.fullName} (${formatLocation(test.location)})`);
|
|
}
|
|
}
|
|
|
|
const tests = this._runner.tests();
|
|
const executedTests = tests.filter(test => test.result);
|
|
const okTestsLength = executedTests.length - failedTests.length - markedAsFailingTests.length - skippedTests.length;
|
|
let summaryText = '';
|
|
if (failedTests.length || markedAsFailingTests.length) {
|
|
const summary = [`ok - ${colors.green(okTestsLength)}`];
|
|
if (failedTests.length)
|
|
summary.push(`failed - ${colors.red(failedTests.length)}`);
|
|
if (markedAsFailingTests.length)
|
|
summary.push(`marked as failing - ${colors.yellow(markedAsFailingTests.length)}`);
|
|
if (skippedTests.length)
|
|
summary.push(`skipped - ${colors.yellow(skippedTests.length)}`);
|
|
summaryText = ` (${summary.join(', ')})`;
|
|
}
|
|
|
|
console.log(`\nRan ${executedTests.length}${summaryText} of ${tests.length} test${tests.length > 1 ? 's' : ''}`);
|
|
const milliseconds = Date.now() - this._timestamp;
|
|
const seconds = milliseconds / 1000;
|
|
console.log(`Finished in ${colors.yellow(seconds)} seconds`);
|
|
}
|
|
|
|
_onTestStarted(test, workerId) {
|
|
}
|
|
|
|
_onTestFinished(test, workerId) {
|
|
if (this._verbose) {
|
|
++this._testCounter;
|
|
this._printVerboseTestResult(this._testCounter, test, workerId);
|
|
} else {
|
|
if (test.result === 'ok')
|
|
process.stdout.write(colors.green('\u00B7'));
|
|
else if (test.result === 'skipped')
|
|
process.stdout.write(colors.yellow('\u00B7'));
|
|
else if (test.result === 'markedAsFailing')
|
|
process.stdout.write(colors.yellow('\u00D7'));
|
|
else if (test.result === 'failed')
|
|
process.stdout.write(colors.red('F'));
|
|
else if (test.result === 'crashed')
|
|
process.stdout.write(colors.red('C'));
|
|
else if (test.result === 'terminated')
|
|
process.stdout.write(colors.magenta('.'));
|
|
else if (test.result === 'timedout')
|
|
process.stdout.write(colors.red('T'));
|
|
}
|
|
}
|
|
|
|
_printVerboseTestResult(resultIndex, test, workerId = undefined) {
|
|
let prefix = `${resultIndex})`;
|
|
if (this._runner.parallel() > 1 && workerId !== undefined)
|
|
prefix += ' ' + colors.gray(`[worker = ${workerId}]`);
|
|
if (test.result === 'ok') {
|
|
console.log(`${prefix} ${colors.green('[OK]')} ${test.fullName} (${formatLocation(test.location)})`);
|
|
} else if (test.result === 'terminated') {
|
|
console.log(`${prefix} ${colors.magenta('[TERMINATED]')} ${test.fullName} (${formatLocation(test.location)})`);
|
|
} else if (test.result === 'crashed') {
|
|
console.log(`${prefix} ${colors.red('[CRASHED]')} ${test.fullName} (${formatLocation(test.location)})`);
|
|
} else if (test.result === 'skipped') {
|
|
} else if (test.result === 'markedAsFailing') {
|
|
console.log(`${prefix} ${colors.yellow('[MARKED AS FAILING]')} ${test.fullName} (${formatLocation(test.location)})`);
|
|
} else if (test.result === 'timedout') {
|
|
console.log(`${prefix} ${colors.red(`[TIMEOUT ${test.timeout}ms]`)} ${test.fullName} (${formatLocation(test.location)})`);
|
|
if (test.output) {
|
|
console.log(' Output:');
|
|
for (const line of test.output)
|
|
console.log(' ' + line);
|
|
}
|
|
} else if (test.result === 'failed') {
|
|
console.log(`${prefix} ${colors.red('[FAIL]')} ${test.fullName} (${formatLocation(test.location)})`);
|
|
if (test.error instanceof MatchError) {
|
|
let lines = this._filePathToLines.get(test.error.location.filePath);
|
|
if (!lines) {
|
|
try {
|
|
lines = fs.readFileSync(test.error.location.filePath, 'utf8').split('\n');
|
|
} catch (e) {
|
|
lines = [];
|
|
}
|
|
this._filePathToLines.set(test.error.location.filePath, lines);
|
|
}
|
|
const lineNumber = test.error.location.lineNumber;
|
|
if (lineNumber < lines.length) {
|
|
const lineNumberLength = (lineNumber + 1 + '').length;
|
|
const FROM = Math.max(test.location.lineNumber - 1, lineNumber - 5);
|
|
const snippet = lines.slice(FROM, lineNumber).map((line, index) => ` ${(FROM + index + 1 + '').padStart(lineNumberLength, ' ')} | ${line}`).join('\n');
|
|
const pointer = ` ` + ' '.repeat(lineNumberLength) + ' ' + '~'.repeat(test.error.location.columnNumber - 1) + '^';
|
|
console.log('\n' + snippet + '\n' + colors.grey(pointer) + '\n');
|
|
}
|
|
console.log(padLines(test.error.formatter(), 4));
|
|
console.log('');
|
|
} else {
|
|
console.log(' Message:');
|
|
let message = '' + (test.error.message || test.error);
|
|
if (test.error.stack && message.includes(test.error.stack))
|
|
message = message.substring(0, message.indexOf(test.error.stack));
|
|
if (message)
|
|
console.log(` ${colors.red(message)}`);
|
|
if (test.error.stack) {
|
|
console.log(' Stack:');
|
|
let stack = test.error.stack;
|
|
// Highlight first test location, if any.
|
|
const match = stack.match(new RegExp(test.location.filePath + ':(\\d+):(\\d+)'));
|
|
if (match) {
|
|
const [, line, column] = match;
|
|
const fileName = `${test.location.fileName}:${line}:${column}`;
|
|
stack = stack.substring(0, match.index) + stack.substring(match.index).replace(fileName, colors.yellow(fileName));
|
|
}
|
|
console.log(padLines(stack, 4));
|
|
}
|
|
}
|
|
if (test.output) {
|
|
console.log(' Output:');
|
|
for (const line of test.output)
|
|
console.log(' ' + line);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function formatLocation(location) {
|
|
if (!location)
|
|
return '';
|
|
return colors.yellow(`${location.fileName}:${location.lineNumber}:${location.columnNumber}`);
|
|
}
|
|
|
|
function padLines(text, spaces = 0) {
|
|
const indent = ' '.repeat(spaces);
|
|
return text.split('\n').map(line => indent + line).join('\n');
|
|
}
|
|
|
|
module.exports = Reporter;
|