mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-13 17:14:02 +03:00
chore(test-runner): misc changes to reporter api (#7664)
- `Location` with `file`, `line` and `column`. - `fullTitle` does not include project name. - `titlePath` method. - All methods of `Reporter` are optional. - Removed `Test.skipped` property that is superseeded by `Test.status()`. - Replaced `Suite.findTest()` with `Suite.allTests()`. - Removed `Test.suite` property.
This commit is contained in:
parent
09764a4423
commit
31572fc372
@ -50,7 +50,7 @@ export class Dispatcher {
|
||||
|
||||
this._suite = suite;
|
||||
for (const suite of this._suite.suites) {
|
||||
for (const test of suite._allTests())
|
||||
for (const test of suite.allTests())
|
||||
this._testById.set(test._id, { test, result: test._appendTestResult() });
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@ export class Dispatcher {
|
||||
// Shard tests.
|
||||
const shard = this._loader.fullConfig().shard;
|
||||
if (shard) {
|
||||
let total = this._suite.totalTestCount();
|
||||
let total = this._suite.allTests().length;
|
||||
const shardSize = Math.ceil(total / shard.total);
|
||||
const from = shardSize * shard.current;
|
||||
const to = shardSize * (shard.current + 1);
|
||||
@ -81,7 +81,7 @@ export class Dispatcher {
|
||||
const entriesByWorkerHashAndFile = new Map<string, Map<string, DispatcherEntry>>();
|
||||
for (const fileSuite of this._suite.suites) {
|
||||
const file = fileSuite._requireFile;
|
||||
for (const test of fileSuite._allTests()) {
|
||||
for (const test of fileSuite.allTests()) {
|
||||
let entriesByFile = entriesByWorkerHashAndFile.get(test._workerHash);
|
||||
if (!entriesByFile) {
|
||||
entriesByFile = new Map();
|
||||
@ -275,8 +275,6 @@ export class Dispatcher {
|
||||
test.expectedStatus = params.expectedStatus;
|
||||
test.annotations = params.annotations;
|
||||
test.timeout = params.timeout;
|
||||
if (params.expectedStatus === 'skipped' && params.status === 'skipped')
|
||||
test.skipped = true;
|
||||
this._reportTestEnd(test, result, params.status);
|
||||
});
|
||||
worker.on('stdOut', (params: TestOutputPayload) => {
|
||||
@ -284,18 +282,18 @@ export class Dispatcher {
|
||||
const pair = params.testId ? this._testById.get(params.testId) : undefined;
|
||||
if (pair)
|
||||
pair.result.stdout.push(chunk);
|
||||
this._reporter.onStdOut(chunk, pair ? pair.test : undefined);
|
||||
this._reporter.onStdOut?.(chunk, pair ? pair.test : undefined);
|
||||
});
|
||||
worker.on('stdErr', (params: TestOutputPayload) => {
|
||||
const chunk = chunkFromParams(params);
|
||||
const pair = params.testId ? this._testById.get(params.testId) : undefined;
|
||||
if (pair)
|
||||
pair.result.stderr.push(chunk);
|
||||
this._reporter.onStdErr(chunk, pair ? pair.test : undefined);
|
||||
this._reporter.onStdErr?.(chunk, pair ? pair.test : undefined);
|
||||
});
|
||||
worker.on('teardownError', ({error}) => {
|
||||
this._hasWorkerErrors = true;
|
||||
this._reporter.onError(error);
|
||||
this._reporter.onError?.(error);
|
||||
});
|
||||
worker.on('exit', () => {
|
||||
this._workers.delete(worker);
|
||||
@ -322,7 +320,7 @@ export class Dispatcher {
|
||||
return;
|
||||
const maxFailures = this._loader.fullConfig().maxFailures;
|
||||
if (!maxFailures || this._failureCount < maxFailures)
|
||||
this._reporter.onTestBegin(test);
|
||||
this._reporter.onTestBegin?.(test);
|
||||
}
|
||||
|
||||
private _reportTestEnd(test: Test, result: TestResult, status: TestStatus) {
|
||||
@ -333,7 +331,7 @@ export class Dispatcher {
|
||||
++this._failureCount;
|
||||
const maxFailures = this._loader.fullConfig().maxFailures;
|
||||
if (!maxFailures || this._failureCount <= maxFailures)
|
||||
this._reporter.onTestEnd(test, result);
|
||||
this._reporter.onTestEnd?.(test, result);
|
||||
if (maxFailures && this._failureCount === maxFailures)
|
||||
this.stop().catch(e => {});
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ export class Loader {
|
||||
try {
|
||||
const suite = new Suite('');
|
||||
suite._requireFile = file;
|
||||
suite.file = file;
|
||||
suite.location.file = file;
|
||||
setCurrentlyLoadingFileSuite(suite);
|
||||
await this._requireOrImport(file);
|
||||
this._fileSuites.set(file, suite);
|
||||
|
@ -60,17 +60,14 @@ export class ProjectImpl {
|
||||
if (Object.entries(overrides).length) {
|
||||
const overridesWithLocation = {
|
||||
fixtures: overrides,
|
||||
location: {
|
||||
file: test.file,
|
||||
line: 1, // TODO: capture location
|
||||
column: 1, // TODO: capture location
|
||||
}
|
||||
// TODO: pass location from test.use() callsite.
|
||||
location: test.location,
|
||||
};
|
||||
pool = new FixturePool([overridesWithLocation], pool);
|
||||
}
|
||||
this.testPools.set(test, pool);
|
||||
|
||||
pool.validateFunction(test.fn, 'Test', true, test);
|
||||
pool.validateFunction(test.fn, 'Test', true, test.location);
|
||||
for (let parent = test.parent; parent; parent = parent.parent) {
|
||||
for (const hook of parent._hooks)
|
||||
pool.validateFunction(hook.fn, hook.type + ' hook', hook.type === 'beforeEach' || hook.type === 'afterEach', hook.location);
|
||||
@ -98,7 +95,7 @@ export class ProjectImpl {
|
||||
test._workerHash = `run${this.index}-${pool.digest}-repeat${repeatEachIndex}`;
|
||||
test._id = `${entry._ordinalInFile}@${entry._requireFile}#run${this.index}-repeat${repeatEachIndex}`;
|
||||
test._pool = pool;
|
||||
test._buildFullTitle(suite.fullTitle());
|
||||
test._buildTitlePath(suite._titlePath);
|
||||
if (!filter(test))
|
||||
continue;
|
||||
result._addTest(test);
|
||||
|
@ -17,29 +17,30 @@
|
||||
import type { FullConfig, TestStatus, TestError } from './types';
|
||||
export type { FullConfig, TestStatus, TestError } from './types';
|
||||
|
||||
export interface Location {
|
||||
file: string;
|
||||
line: number;
|
||||
column: number;
|
||||
}
|
||||
export interface Suite {
|
||||
title: string;
|
||||
file: string;
|
||||
line: number;
|
||||
column: number;
|
||||
location: Location;
|
||||
suites: Suite[];
|
||||
tests: Test[];
|
||||
findTest(fn: (test: Test) => boolean | void): boolean;
|
||||
totalTestCount(): number;
|
||||
titlePath(): string[];
|
||||
fullTitle(): string;
|
||||
allTests(): Test[];
|
||||
}
|
||||
export interface Test {
|
||||
suite: Suite;
|
||||
title: string;
|
||||
file: string;
|
||||
line: number;
|
||||
column: number;
|
||||
location: Location;
|
||||
results: TestResult[];
|
||||
skipped: boolean;
|
||||
expectedStatus: TestStatus;
|
||||
timeout: number;
|
||||
annotations: { type: string, description?: string }[];
|
||||
projectName: string;
|
||||
retries: number;
|
||||
titlePath(): string[];
|
||||
fullTitle(): string;
|
||||
status(): 'skipped' | 'expected' | 'unexpected' | 'flaky';
|
||||
ok(): boolean;
|
||||
@ -58,11 +59,11 @@ export interface FullResult {
|
||||
status: 'passed' | 'failed' | 'timedout' | 'interrupted';
|
||||
}
|
||||
export interface Reporter {
|
||||
onBegin(config: FullConfig, suite: Suite): void;
|
||||
onTestBegin(test: Test): void;
|
||||
onStdOut(chunk: string | Buffer, test?: Test): void;
|
||||
onStdErr(chunk: string | Buffer, test?: Test): void;
|
||||
onTestEnd(test: Test, result: TestResult): void;
|
||||
onError(error: TestError): void;
|
||||
onEnd(result: FullResult): void | Promise<void>;
|
||||
onBegin?(config: FullConfig, suite: Suite): void;
|
||||
onTestBegin?(test: Test): void;
|
||||
onStdOut?(chunk: string | Buffer, test?: Test): void;
|
||||
onStdErr?(chunk: string | Buffer, test?: Test): void;
|
||||
onTestEnd?(test: Test, result: TestResult): void;
|
||||
onError?(error: TestError): void;
|
||||
onEnd?(result: FullResult): void | Promise<void>;
|
||||
}
|
||||
|
@ -33,18 +33,12 @@ export class BaseReporter implements Reporter {
|
||||
fileDurations = new Map<string, number>();
|
||||
monotonicStartTime: number = 0;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
onBegin(config: FullConfig, suite: Suite) {
|
||||
this.monotonicStartTime = monotonicTime();
|
||||
this.config = config;
|
||||
this.suite = suite;
|
||||
}
|
||||
|
||||
onTestBegin(test: Test) {
|
||||
}
|
||||
|
||||
onStdOut(chunk: string | Buffer) {
|
||||
if (!this.config.quiet)
|
||||
process.stdout.write(chunk);
|
||||
@ -91,7 +85,7 @@ export class BaseReporter implements Reporter {
|
||||
const unexpected: Test[] = [];
|
||||
const flaky: Test[] = [];
|
||||
|
||||
this.suite.findTest(test => {
|
||||
this.suite.allTests().forEach(test => {
|
||||
switch (test.status()) {
|
||||
case 'skipped': ++skipped; break;
|
||||
case 'expected': ++expected; break;
|
||||
@ -158,13 +152,14 @@ export function formatFailure(config: FullConfig, test: Test, index?: number): s
|
||||
}
|
||||
|
||||
function relativeTestPath(config: FullConfig, test: Test): string {
|
||||
return path.relative(config.rootDir, test.file) || path.basename(test.file);
|
||||
return path.relative(config.rootDir, test.location.file) || path.basename(test.location.file);
|
||||
}
|
||||
|
||||
export function formatTestTitle(config: FullConfig, test: Test): string {
|
||||
let relativePath = relativeTestPath(config, test);
|
||||
relativePath += ':' + test.line + ':' + test.column;
|
||||
return `${relativePath} › ${test.fullTitle()}`;
|
||||
relativePath += ':' + test.location.line + ':' + test.location.column;
|
||||
const title = (test.projectName ? `[${test.projectName}] ` : '') + test.fullTitle();
|
||||
return `${relativePath} › ${title}`;
|
||||
}
|
||||
|
||||
function formatTestHeader(config: FullConfig, test: Test, indent: string, index?: number): string {
|
||||
@ -182,9 +177,9 @@ function formatFailedResult(test: Test, result: TestResult): string {
|
||||
tokens.push('');
|
||||
tokens.push(indent(colors.red(`Timeout of ${test.timeout}ms exceeded.`), ' '));
|
||||
if (result.error !== undefined)
|
||||
tokens.push(indent(formatError(result.error, test.file), ' '));
|
||||
tokens.push(indent(formatError(result.error, test.location.file), ' '));
|
||||
} else {
|
||||
tokens.push(indent(formatError(result.error!, test.file), ' '));
|
||||
tokens.push(indent(formatError(result.error!, test.location.file), ' '));
|
||||
}
|
||||
return tokens.join('\n');
|
||||
}
|
||||
|
@ -14,16 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { FullConfig, TestResult, Test, Suite, TestError, Reporter, FullResult } from '../reporter';
|
||||
import { Reporter } from '../reporter';
|
||||
|
||||
class EmptyReporter implements Reporter {
|
||||
onBegin(config: FullConfig, suite: Suite) {}
|
||||
onTestBegin(test: Test) {}
|
||||
onStdOut(chunk: string | Buffer, test?: Test) {}
|
||||
onStdErr(chunk: string | Buffer, test?: Test) {}
|
||||
onTestEnd(test: Test, result: TestResult) {}
|
||||
onError(error: TestError) {}
|
||||
async onEnd(result: FullResult) {}
|
||||
}
|
||||
|
||||
export default EmptyReporter;
|
||||
|
@ -16,8 +16,7 @@
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import EmptyReporter from './empty';
|
||||
import { FullConfig, Test, Suite, TestResult, TestError, FullResult, TestStatus } from '../reporter';
|
||||
import { FullConfig, Test, Suite, TestResult, TestError, FullResult, TestStatus, Location, Reporter } from '../reporter';
|
||||
|
||||
export interface JSONReport {
|
||||
config: Omit<FullConfig, 'projects'> & {
|
||||
@ -76,14 +75,13 @@ function toPosixPath(aPath: string): string {
|
||||
return aPath.split(path.sep).join(path.posix.sep);
|
||||
}
|
||||
|
||||
class JSONReporter extends EmptyReporter {
|
||||
class JSONReporter implements Reporter {
|
||||
config!: FullConfig;
|
||||
suite!: Suite;
|
||||
private _errors: TestError[] = [];
|
||||
private _outputFile: string | undefined;
|
||||
|
||||
constructor(options: { outputFile?: string } = {}) {
|
||||
super();
|
||||
this._outputFile = options.outputFile;
|
||||
}
|
||||
|
||||
@ -129,22 +127,35 @@ class JSONReporter extends EmptyReporter {
|
||||
const fileSuites = new Map<string, JSONReportSuite>();
|
||||
const result: JSONReportSuite[] = [];
|
||||
for (const suite of suites) {
|
||||
if (!fileSuites.has(suite.file)) {
|
||||
if (!fileSuites.has(suite.location.file)) {
|
||||
const serialized = this._serializeSuite(suite);
|
||||
if (serialized) {
|
||||
fileSuites.set(suite.file, serialized);
|
||||
fileSuites.set(suite.location.file, serialized);
|
||||
result.push(serialized);
|
||||
}
|
||||
} else {
|
||||
this._mergeTestsFromSuite(fileSuites.get(suite.file)!, suite);
|
||||
this._mergeTestsFromSuite(fileSuites.get(suite.location.file)!, suite);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private _relativeLocation(location: Location): Location {
|
||||
return {
|
||||
file: toPosixPath(path.relative(this.config.rootDir, location.file)),
|
||||
line: location.line,
|
||||
column: location.column,
|
||||
};
|
||||
}
|
||||
|
||||
private _locationMatches(s: JSONReportSuite | JSONReportSpec, location: Location) {
|
||||
const relative = this._relativeLocation(location);
|
||||
return s.file === relative.file && s.line === relative.line && s.column === relative.column;
|
||||
}
|
||||
|
||||
private _mergeTestsFromSuite(to: JSONReportSuite, from: Suite) {
|
||||
for (const fromSuite of from.suites) {
|
||||
const toSuite = (to.suites || []).find(s => s.title === fromSuite.title && s.file === toPosixPath(path.relative(this.config.rootDir, fromSuite.file)) && s.line === fromSuite.line && s.column === fromSuite.column);
|
||||
const toSuite = (to.suites || []).find(s => s.title === fromSuite.title && this._locationMatches(s, from.location));
|
||||
if (toSuite) {
|
||||
this._mergeTestsFromSuite(toSuite, fromSuite);
|
||||
} else {
|
||||
@ -157,7 +168,7 @@ class JSONReporter extends EmptyReporter {
|
||||
}
|
||||
}
|
||||
for (const test of from.tests) {
|
||||
const toSpec = to.specs.find(s => s.title === test.title && s.file === toPosixPath(path.relative(this.config.rootDir, test.file)) && s.line === test.line && s.column === test.column);
|
||||
const toSpec = to.specs.find(s => s.title === test.title && s.file === toPosixPath(path.relative(this.config.rootDir, test.location.file)) && s.line === test.location.line && s.column === test.location.column);
|
||||
if (toSpec)
|
||||
toSpec.tests.push(this._serializeTest(test));
|
||||
else
|
||||
@ -166,14 +177,12 @@ class JSONReporter extends EmptyReporter {
|
||||
}
|
||||
|
||||
private _serializeSuite(suite: Suite): null | JSONReportSuite {
|
||||
if (!suite.findTest(test => true))
|
||||
if (!suite.allTests().length)
|
||||
return null;
|
||||
const suites = suite.suites.map(suite => this._serializeSuite(suite)).filter(s => s) as JSONReportSuite[];
|
||||
return {
|
||||
title: suite.title,
|
||||
file: toPosixPath(path.relative(this.config.rootDir, suite.file)),
|
||||
line: suite.line,
|
||||
column: suite.column,
|
||||
...this._relativeLocation(suite.location),
|
||||
specs: suite.tests.map(test => this._serializeTestSpec(test)),
|
||||
suites: suites.length ? suites : undefined,
|
||||
};
|
||||
@ -184,9 +193,7 @@ class JSONReporter extends EmptyReporter {
|
||||
title: test.title,
|
||||
ok: test.ok(),
|
||||
tests: [ this._serializeTest(test) ],
|
||||
file: toPosixPath(path.relative(this.config.rootDir, test.file)),
|
||||
line: test.line,
|
||||
column: test.column,
|
||||
...this._relativeLocation(test.location),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -16,12 +16,11 @@
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import EmptyReporter from './empty';
|
||||
import { FullConfig, FullResult, Suite, Test } from '../reporter';
|
||||
import { FullConfig, FullResult, Reporter, Suite, Test } from '../reporter';
|
||||
import { monotonicTime } from '../util';
|
||||
import { formatFailure, formatTestTitle, stripAscii } from './base';
|
||||
|
||||
class JUnitReporter extends EmptyReporter {
|
||||
class JUnitReporter implements Reporter {
|
||||
private config!: FullConfig;
|
||||
private suite!: Suite;
|
||||
private timestamp!: number;
|
||||
@ -33,7 +32,6 @@ class JUnitReporter extends EmptyReporter {
|
||||
private stripANSIControlSequences = false;
|
||||
|
||||
constructor(options: { outputFile?: string, stripANSIControlSequences?: boolean } = {}) {
|
||||
super();
|
||||
this.outputFile = options.outputFile;
|
||||
this.stripANSIControlSequences = options.stripANSIControlSequences || false;
|
||||
}
|
||||
@ -85,7 +83,7 @@ class JUnitReporter extends EmptyReporter {
|
||||
let duration = 0;
|
||||
const children: XMLEntry[] = [];
|
||||
|
||||
suite.findTest(test => {
|
||||
suite.allTests().forEach(test => {
|
||||
++tests;
|
||||
if (test.status() === 'skipped')
|
||||
++skipped;
|
||||
@ -102,7 +100,7 @@ class JUnitReporter extends EmptyReporter {
|
||||
const entry: XMLEntry = {
|
||||
name: 'testsuite',
|
||||
attributes: {
|
||||
name: path.relative(this.config.rootDir, suite.file),
|
||||
name: path.relative(this.config.rootDir, suite.location.file),
|
||||
timestamp: this.timestamp,
|
||||
hostname: '',
|
||||
tests,
|
||||
@ -138,7 +136,7 @@ class JUnitReporter extends EmptyReporter {
|
||||
entry.children.push({
|
||||
name: 'failure',
|
||||
attributes: {
|
||||
message: `${path.basename(test.file)}:${test.line}:${test.column} ${test.title}`,
|
||||
message: `${path.basename(test.location.file)}:${test.location.line}:${test.location.column} ${test.title}`,
|
||||
type: 'FAILURE',
|
||||
},
|
||||
text: stripAscii(formatFailure(this.config, test))
|
||||
|
@ -26,7 +26,7 @@ class LineReporter extends BaseReporter {
|
||||
|
||||
onBegin(config: FullConfig, suite: Suite) {
|
||||
super.onBegin(config, suite);
|
||||
this._total = suite.totalTestCount();
|
||||
this._total = suite.allTests().length;
|
||||
console.log();
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,6 @@ class ListReporter extends BaseReporter {
|
||||
}
|
||||
|
||||
onTestBegin(test: Test) {
|
||||
super.onTestBegin(test);
|
||||
if (process.stdout.isTTY) {
|
||||
if (this._needNewLine) {
|
||||
this._needNewLine = false;
|
||||
|
@ -25,36 +25,36 @@ export class Multiplexer implements Reporter {
|
||||
|
||||
onBegin(config: FullConfig, suite: Suite) {
|
||||
for (const reporter of this._reporters)
|
||||
reporter.onBegin(config, suite);
|
||||
reporter.onBegin?.(config, suite);
|
||||
}
|
||||
|
||||
onTestBegin(test: Test) {
|
||||
for (const reporter of this._reporters)
|
||||
reporter.onTestBegin(test);
|
||||
reporter.onTestBegin?.(test);
|
||||
}
|
||||
|
||||
onStdOut(chunk: string | Buffer, test?: Test) {
|
||||
for (const reporter of this._reporters)
|
||||
reporter.onStdOut(chunk, test);
|
||||
reporter.onStdOut?.(chunk, test);
|
||||
}
|
||||
|
||||
onStdErr(chunk: string | Buffer, test?: Test) {
|
||||
for (const reporter of this._reporters)
|
||||
reporter.onStdErr(chunk, test);
|
||||
reporter.onStdErr?.(chunk, test);
|
||||
}
|
||||
|
||||
onTestEnd(test: Test, result: TestResult) {
|
||||
for (const reporter of this._reporters)
|
||||
reporter.onTestEnd(test, result);
|
||||
reporter.onTestEnd?.(test, result);
|
||||
}
|
||||
|
||||
async onEnd(result: FullResult) {
|
||||
for (const reporter of this._reporters)
|
||||
await reporter.onEnd(result);
|
||||
await reporter.onEnd?.(result);
|
||||
}
|
||||
|
||||
onError(error: TestError) {
|
||||
for (const reporter of this._reporters)
|
||||
reporter.onError(error);
|
||||
reporter.onError?.(error);
|
||||
}
|
||||
}
|
||||
|
@ -99,8 +99,8 @@ export class Runner {
|
||||
const { result, timedOut } = await raceAgainstDeadline(this._run(list, filePatternFilters, projectName), globalDeadline);
|
||||
if (timedOut) {
|
||||
if (!this._didBegin)
|
||||
this._reporter.onBegin(config, new Suite(''));
|
||||
await this._reporter.onEnd({ status: 'timedout' });
|
||||
this._reporter.onBegin?.(config, new Suite(''));
|
||||
await this._reporter.onEnd?.({ status: 'timedout' });
|
||||
await this._flushOutput();
|
||||
return 'failed';
|
||||
}
|
||||
@ -205,9 +205,10 @@ export class Runner {
|
||||
for (let repeatEachIndex = 0; repeatEachIndex < project.config.repeatEach; repeatEachIndex++) {
|
||||
const cloned = project.cloneSuite(fileSuite, repeatEachIndex, test => {
|
||||
const fullTitle = test.fullTitle();
|
||||
if (grepInvertMatcher?.(fullTitle))
|
||||
const titleWithProject = (test.projectName ? `[${test.projectName}] ` : '') + fullTitle;
|
||||
if (grepInvertMatcher?.(titleWithProject))
|
||||
return false;
|
||||
return grepMatcher(fullTitle);
|
||||
return grepMatcher(titleWithProject);
|
||||
});
|
||||
if (cloned)
|
||||
rootSuite._addSuite(cloned);
|
||||
@ -216,7 +217,7 @@ export class Runner {
|
||||
outputDirs.add(project.config.outputDir);
|
||||
}
|
||||
|
||||
const total = rootSuite.totalTestCount();
|
||||
const total = rootSuite.allTests().length;
|
||||
if (!total)
|
||||
return { status: 'no-tests' };
|
||||
|
||||
@ -238,7 +239,7 @@ export class Runner {
|
||||
|
||||
if (process.stdout.isTTY) {
|
||||
const workers = new Set();
|
||||
rootSuite.findTest(test => {
|
||||
rootSuite.allTests().forEach(test => {
|
||||
workers.add(test._requireFile + test._workerHash);
|
||||
});
|
||||
console.log();
|
||||
@ -248,7 +249,7 @@ export class Runner {
|
||||
console.log(`Running ${total} test${total > 1 ? 's' : ''} using ${jobs} worker${jobs > 1 ? 's' : ''}${shardDetails}`);
|
||||
}
|
||||
|
||||
this._reporter.onBegin(config, rootSuite);
|
||||
this._reporter.onBegin?.(config, rootSuite);
|
||||
this._didBegin = true;
|
||||
let hasWorkerErrors = false;
|
||||
if (!list) {
|
||||
@ -259,12 +260,12 @@ export class Runner {
|
||||
}
|
||||
|
||||
if (sigint) {
|
||||
await this._reporter.onEnd({ status: 'interrupted' });
|
||||
await this._reporter.onEnd?.({ status: 'interrupted' });
|
||||
return { status: 'sigint' };
|
||||
}
|
||||
|
||||
const failed = hasWorkerErrors || rootSuite.findTest(test => !test.ok());
|
||||
await this._reporter.onEnd({ status: failed ? 'failed' : 'passed' });
|
||||
const failed = hasWorkerErrors || rootSuite.allTests().some(test => !test.ok());
|
||||
await this._reporter.onEnd?.({ status: failed ? 'failed' : 'passed' });
|
||||
return { status: failed ? 'failed' : 'passed' };
|
||||
} finally {
|
||||
if (globalSetupResult && typeof globalSetupResult === 'function')
|
||||
@ -287,8 +288,8 @@ function filterByFocusedLine(suite: Suite, focusedTestFileLines: FilePatternFilt
|
||||
re.lastIndex = 0;
|
||||
return re.test(testFileName) && (line === testLine || line === null);
|
||||
});
|
||||
const suiteFilter = (suite: Suite) => testFileLineMatches(suite.file, suite.line);
|
||||
const testFilter = (test: Test) => testFileLineMatches(test.file, test.line);
|
||||
const suiteFilter = (suite: Suite) => testFileLineMatches(suite.location.file, suite.location.line);
|
||||
const testFilter = (test: Test) => testFileLineMatches(test.location.file, test.location.line);
|
||||
return filterSuite(suite, suiteFilter, testFilter);
|
||||
}
|
||||
|
||||
@ -398,5 +399,5 @@ function getClashingTestsPerSuite(rootSuite: Suite): Map<string, Test[]> {
|
||||
}
|
||||
|
||||
function buildItemLocation(rootDir: string, testOrSuite: Suite | Test) {
|
||||
return `${path.relative(rootDir, testOrSuite.file)}:${testOrSuite.line}`;
|
||||
return `${path.relative(rootDir, testOrSuite.location.file)}:${testOrSuite.location.line}`;
|
||||
}
|
||||
|
@ -21,12 +21,10 @@ import { Annotations, Location } from './types';
|
||||
|
||||
class Base {
|
||||
title: string;
|
||||
file: string = '';
|
||||
line: number = 0;
|
||||
column: number = 0;
|
||||
location: Location = { file: '', line: 0, column: 0 };
|
||||
parent?: Suite;
|
||||
|
||||
_fullTitle: string = '';
|
||||
_titlePath: string[] = [];
|
||||
_only = false;
|
||||
_requireFile: string = '';
|
||||
|
||||
@ -34,15 +32,18 @@ class Base {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
_buildFullTitle(parentFullTitle: string) {
|
||||
_buildTitlePath(parentTitlePath: string[]) {
|
||||
this._titlePath = [...parentTitlePath];
|
||||
if (this.title)
|
||||
this._fullTitle = (parentFullTitle ? parentFullTitle + ' ' : '') + this.title;
|
||||
else
|
||||
this._fullTitle = parentFullTitle;
|
||||
this._titlePath.push(this.title);
|
||||
}
|
||||
|
||||
titlePath(): string[] {
|
||||
return this._titlePath;
|
||||
}
|
||||
|
||||
fullTitle(): string {
|
||||
return this._fullTitle;
|
||||
return this._titlePath.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,7 +72,6 @@ export class Suite extends Base implements reporterTypes.Suite {
|
||||
|
||||
_addTest(test: Test) {
|
||||
test.parent = this;
|
||||
test.suite = this;
|
||||
this.tests.push(test);
|
||||
this._entries.push(test);
|
||||
}
|
||||
@ -82,30 +82,17 @@ export class Suite extends Base implements reporterTypes.Suite {
|
||||
this._entries.push(suite);
|
||||
}
|
||||
|
||||
findTest(fn: (test: Test) => boolean | void): boolean {
|
||||
for (const entry of this._entries) {
|
||||
if (entry instanceof Suite) {
|
||||
if (entry.findTest(fn))
|
||||
return true;
|
||||
} else {
|
||||
if (fn(entry))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
totalTestCount(): number {
|
||||
let total = 0;
|
||||
for (const suite of this.suites)
|
||||
total += suite.totalTestCount();
|
||||
total += this.tests.length;
|
||||
return total;
|
||||
}
|
||||
|
||||
_allTests(): Test[] {
|
||||
allTests(): Test[] {
|
||||
const result: Test[] = [];
|
||||
this.findTest(test => { result.push(test); });
|
||||
const visit = (suite: Suite) => {
|
||||
for (const entry of suite._entries) {
|
||||
if (entry instanceof Suite)
|
||||
visit(entry);
|
||||
else
|
||||
result.push(entry);
|
||||
}
|
||||
};
|
||||
visit(this);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -126,9 +113,7 @@ export class Suite extends Base implements reporterTypes.Suite {
|
||||
_clone(): Suite {
|
||||
const suite = new Suite(this.title);
|
||||
suite._only = this._only;
|
||||
suite.file = this.file;
|
||||
suite.line = this.line;
|
||||
suite.column = this.column;
|
||||
suite.location = this.location;
|
||||
suite._requireFile = this._requireFile;
|
||||
suite._fixtureOverrides = this._fixtureOverrides;
|
||||
suite._hooks = this._hooks.slice();
|
||||
@ -140,11 +125,9 @@ export class Suite extends Base implements reporterTypes.Suite {
|
||||
}
|
||||
|
||||
export class Test extends Base implements reporterTypes.Test {
|
||||
suite!: Suite;
|
||||
fn: Function;
|
||||
results: reporterTypes.TestResult[] = [];
|
||||
|
||||
skipped = false;
|
||||
expectedStatus: reporterTypes.TestStatus = 'passed';
|
||||
timeout = 0;
|
||||
annotations: Annotations = [];
|
||||
@ -165,15 +148,14 @@ export class Test extends Base implements reporterTypes.Test {
|
||||
}
|
||||
|
||||
status(): 'skipped' | 'expected' | 'unexpected' | 'flaky' {
|
||||
if (this.skipped)
|
||||
return 'skipped';
|
||||
// List mode bail out.
|
||||
if (!this.results.length)
|
||||
return 'skipped';
|
||||
if (this.results.length === 1 && this.expectedStatus === this.results[0].status)
|
||||
return 'expected';
|
||||
return this.expectedStatus === 'skipped' ? 'skipped' : 'expected';
|
||||
let hasPassedResults = false;
|
||||
for (const result of this.results) {
|
||||
// TODO: we should not report tests that do not belong to the shard.
|
||||
// Missing status is Ok when running in shards mode.
|
||||
if (!result.status)
|
||||
return 'skipped';
|
||||
@ -193,17 +175,11 @@ export class Test extends Base implements reporterTypes.Test {
|
||||
_clone(): Test {
|
||||
const test = new Test(this.title, this.fn, this._ordinalInFile, this._testType);
|
||||
test._only = this._only;
|
||||
test.file = this.file;
|
||||
test.line = this.line;
|
||||
test.column = this.column;
|
||||
test.location = this.location;
|
||||
test._requireFile = this._requireFile;
|
||||
return test;
|
||||
}
|
||||
|
||||
fullTitle(): string {
|
||||
return (this.projectName ? `[${this.projectName}] ` : '') + this._fullTitle;
|
||||
}
|
||||
|
||||
_appendTestResult(): reporterTypes.TestResult {
|
||||
const result: reporterTypes.TestResult = {
|
||||
retry: this.results.length,
|
||||
|
@ -64,11 +64,9 @@ export class TestTypeImpl {
|
||||
|
||||
const test = new Test(title, fn, ordinalInFile, this);
|
||||
test._requireFile = suite._requireFile;
|
||||
test.file = location.file;
|
||||
test.line = location.line;
|
||||
test.column = location.column;
|
||||
test.location = location;
|
||||
suite._addTest(test);
|
||||
test._buildFullTitle(suite.fullTitle());
|
||||
test._buildTitlePath(suite._titlePath);
|
||||
|
||||
if (type === 'only')
|
||||
test._only = true;
|
||||
@ -81,11 +79,9 @@ export class TestTypeImpl {
|
||||
|
||||
const child = new Suite(title);
|
||||
child._requireFile = suite._requireFile;
|
||||
child.file = location.file;
|
||||
child.line = location.line;
|
||||
child.column = location.column;
|
||||
child.location = location;
|
||||
suite._addSuite(child);
|
||||
child._buildFullTitle(suite.fullTitle());
|
||||
child._buildTitlePath(suite._titlePath);
|
||||
|
||||
if (type === 'only')
|
||||
child._only = true;
|
||||
|
@ -15,9 +15,10 @@
|
||||
*/
|
||||
|
||||
import type { Fixtures } from '../../types/test';
|
||||
import type { Location } from './reporter';
|
||||
export * from '../../types/test';
|
||||
export { Location } from './reporter';
|
||||
|
||||
export type Location = { file: string, line: number, column: number };
|
||||
export type FixturesWithLocation = {
|
||||
fixtures: Fixtures;
|
||||
location: Location;
|
||||
|
@ -218,9 +218,9 @@ export class WorkerRunner extends EventEmitter {
|
||||
const testInfo: TestInfo = {
|
||||
...this._workerInfo,
|
||||
title: test.title,
|
||||
file: test.file,
|
||||
line: test.line,
|
||||
column: test.column,
|
||||
file: test.location.file,
|
||||
line: test.location.line,
|
||||
column: test.location.column,
|
||||
fn: test.fn,
|
||||
repeatEachIndex: this._params.repeatEachIndex,
|
||||
retry: entry.retry,
|
||||
|
@ -211,7 +211,7 @@ test('should render projects', async ({ runInlineTest }) => {
|
||||
expect(xml['testsuites']['testsuite'][0]['$']['tests']).toBe('1');
|
||||
expect(xml['testsuites']['testsuite'][0]['$']['failures']).toBe('0');
|
||||
expect(xml['testsuites']['testsuite'][0]['$']['skipped']).toBe('0');
|
||||
expect(xml['testsuites']['testsuite'][0]['testcase'][0]['$']['name']).toBe('[project1] one');
|
||||
expect(xml['testsuites']['testsuite'][0]['testcase'][0]['$']['name']).toBe('one');
|
||||
expect(xml['testsuites']['testsuite'][0]['testcase'][0]['$']['classname']).toContain('[project1] one');
|
||||
expect(xml['testsuites']['testsuite'][0]['testcase'][0]['$']['classname']).toContain('a.test.js:6:7');
|
||||
|
||||
@ -219,7 +219,7 @@ test('should render projects', async ({ runInlineTest }) => {
|
||||
expect(xml['testsuites']['testsuite'][1]['$']['tests']).toBe('1');
|
||||
expect(xml['testsuites']['testsuite'][1]['$']['failures']).toBe('0');
|
||||
expect(xml['testsuites']['testsuite'][1]['$']['skipped']).toBe('0');
|
||||
expect(xml['testsuites']['testsuite'][1]['testcase'][0]['$']['name']).toBe('[project2] one');
|
||||
expect(xml['testsuites']['testsuite'][1]['testcase'][0]['$']['name']).toBe('one');
|
||||
expect(xml['testsuites']['testsuite'][1]['testcase'][0]['$']['classname']).toContain('[project2] one');
|
||||
expect(xml['testsuites']['testsuite'][1]['testcase'][0]['$']['classname']).toContain('a.test.js:6:7');
|
||||
expect(result.exitCode).toBe(0);
|
||||
|
Loading…
Reference in New Issue
Block a user