folio
Version:
A customizable test framework to build your own test frameworks. Foundation for the [Playwright test runner](https://github.com/microsoft/playwright-test).
237 lines • 8.93 kB
JavaScript
/**
* Copyright (c) Microsoft Corporation.
*
* 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.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.stripAscii = exports.serializeParameters = exports.formatFailure = exports.BaseReporter = void 0;
const code_frame_1 = require("@babel/code-frame");
const safe_1 = __importDefault(require("colors/safe"));
const fs_1 = __importDefault(require("fs"));
const ms_1 = __importDefault(require("ms"));
const path_1 = __importDefault(require("path"));
const stack_utils_1 = __importDefault(require("stack-utils"));
const stackUtils = new stack_utils_1.default();
class BaseReporter {
constructor() {
this.duration = 0;
this.fileDurations = new Map();
}
onBegin(config, suite) {
this.monotonicStartTime = monotonicTime();
this.config = config;
this.suite = suite;
}
onTestBegin(test) {
}
onStdOut(chunk) {
if (!this.config.quiet)
process.stdout.write(chunk);
}
onStdErr(chunk) {
if (!this.config.quiet)
process.stderr.write(chunk);
}
onTestEnd(test, result) {
const spec = test.spec;
let duration = this.fileDurations.get(spec.file) || 0;
duration += result.duration;
this.fileDurations.set(spec.file, duration);
}
onError(error) {
console.log(formatError(error));
}
onTimeout(timeout) {
this.timeout = timeout;
}
onEnd() {
this.duration = monotonicTime() - this.monotonicStartTime;
}
_printSlowTests() {
const fileDurations = [...this.fileDurations.entries()];
fileDurations.sort((a, b) => b[1] - a[1]);
let insertedGap = false;
for (let i = 0; i < 10 && i < fileDurations.length; ++i) {
const baseName = path_1.default.basename(fileDurations[i][0]);
const duration = fileDurations[i][1];
if (duration < 15000)
break;
if (!insertedGap) {
insertedGap = true;
console.log();
}
console.log(safe_1.default.yellow(' Slow test: ') + baseName + safe_1.default.yellow(` (${ms_1.default(duration)})`));
}
console.log();
}
epilogue(full) {
let skipped = 0;
let expected = 0;
const unexpected = [];
const flaky = [];
this.suite.findTest(test => {
switch (test.status()) {
case 'skipped':
++skipped;
break;
case 'expected':
++expected;
break;
case 'unexpected':
unexpected.push(test);
break;
case 'flaky':
flaky.push(test);
break;
}
});
if (expected)
console.log(safe_1.default.green(` ${expected} passed`) + safe_1.default.dim(` (${ms_1.default(this.duration)})`));
if (skipped)
console.log(safe_1.default.yellow(` ${skipped} skipped`));
if (unexpected.length) {
console.log(safe_1.default.red(` ${unexpected.length} failed`));
this._printTestHeaders(unexpected);
}
if (flaky.length) {
console.log(safe_1.default.red(` ${flaky.length} flaky`));
this._printTestHeaders(flaky);
}
if (this.timeout)
console.log(safe_1.default.red(` Timed out waiting ${this.timeout / 1000}s for the entire test run`));
if (full && unexpected.length) {
console.log('');
this._printFailures(unexpected);
}
this._printSlowTests();
}
_printTestHeaders(tests) {
tests.forEach(test => {
console.log(formatTestHeader(this.config, test, ' '));
});
}
_printFailures(failures) {
failures.forEach((test, index) => {
console.log(formatFailure(this.config, test, index + 1));
});
}
hasResultWithStatus(test, status) {
return !!test.results.find(r => r.status === status);
}
willRetry(test, result) {
return result.status !== 'passed' && result.status !== test.expectedStatus && test.results.length <= this.config.retries;
}
}
exports.BaseReporter = BaseReporter;
function formatFailure(config, test, index) {
const tokens = [];
tokens.push(formatTestHeader(config, test, ' ', index));
for (const result of test.results) {
if (result.status === 'passed')
continue;
tokens.push(formatResult(test, result));
}
tokens.push('');
return tokens.join('\n');
}
exports.formatFailure = formatFailure;
function formatTestHeader(config, test, indent, index) {
const tokens = [];
const spec = test.spec;
let relativePath = path_1.default.relative(config.testDir, spec.file) || path_1.default.basename(spec.file);
relativePath += ':' + spec.line + ':' + spec.column;
const passedUnexpectedlySuffix = test.results[0].status === 'passed' ? ' -- passed unexpectedly' : '';
const header = `${indent}${index ? index + ') ' : ''}${relativePath} › ${spec.fullTitle()}${passedUnexpectedlySuffix}`;
tokens.push(safe_1.default.red(pad(header, '=')));
// Print parameters.
if (Object.keys(test.parameters).length)
tokens.push(indent + (index ? ' '.repeat(String(index).length + 2) : '') + safe_1.default.gray(serializeParameters(test.parameters)));
return tokens.join('\n');
}
function formatResult(test, result) {
const tokens = [];
if (result.retry)
tokens.push(safe_1.default.gray(pad(`\n Retry #${result.retry}`, '-')));
if (result.status === 'timedOut') {
tokens.push('');
tokens.push(indent(safe_1.default.red(`Timeout of ${test.timeout}ms exceeded.`), ' '));
}
else {
tokens.push(indent(formatError(result.error, test.spec.file), ' '));
}
return tokens.join('\n');
}
function formatError(error, file) {
const stack = error.stack;
const tokens = [];
if (stack) {
tokens.push('');
const messageLocation = error.stack.indexOf(error.message);
const preamble = error.stack.substring(0, messageLocation + error.message.length);
tokens.push(preamble);
const position = file ? positionInFile(stack, file) : null;
if (position) {
const source = fs_1.default.readFileSync(file, 'utf8');
tokens.push('');
tokens.push(code_frame_1.codeFrameColumns(source, {
start: position,
}, { highlightCode: true }));
}
tokens.push('');
tokens.push(safe_1.default.dim(stack.substring(preamble.length + 1)));
}
else {
tokens.push('');
tokens.push(error.value);
}
return tokens.join('\n');
}
function pad(line, char) {
return line + ' ' + safe_1.default.gray(char.repeat(Math.max(0, 100 - line.length - 1)));
}
function indent(lines, tab) {
return lines.replace(/^(?=.+$)/gm, tab);
}
function positionInFile(stack, file) {
// Stack will have /private/var/folders instead of /var/folders on Mac.
file = fs_1.default.realpathSync(file);
for (const line of stack.split('\n')) {
const parsed = stackUtils.parseLine(line);
if (!parsed)
continue;
if (path_1.default.resolve(process.cwd(), parsed.file) === file)
return { column: parsed.column, line: parsed.line };
}
return null;
}
function serializeParameters(parameters) {
const tokens = [];
for (const name of Object.keys(parameters))
tokens.push(`${name}=${parameters[name]}`);
return tokens.join(', ');
}
exports.serializeParameters = serializeParameters;
function monotonicTime() {
const [seconds, nanoseconds] = process.hrtime();
return seconds * 1000 + (nanoseconds / 1000000 | 0);
}
const asciiRegex = new RegExp('[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', 'g');
function stripAscii(str) {
return str.replace(asciiRegex, '');
}
exports.stripAscii = stripAscii;
//# sourceMappingURL=base.js.map
;