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:
Dmitry Gozman 2021-07-16 12:40:33 -07:00 committed by GitHub
parent 09764a4423
commit 31572fc372
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 124 additions and 162 deletions

View File

@ -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 => {});
}

View File

@ -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);

View File

@ -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);

View File

@ -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>;
}

View File

@ -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');
}

View File

@ -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;

View File

@ -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),
};
}

View File

@ -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))

View File

@ -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();
}

View File

@ -38,7 +38,6 @@ class ListReporter extends BaseReporter {
}
onTestBegin(test: Test) {
super.onTestBegin(test);
if (process.stdout.isTTY) {
if (this._needNewLine) {
this._needNewLine = false;

View File

@ -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);
}
}

View File

@ -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}`;
}

View File

@ -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,

View File

@ -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;

View File

@ -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;

View File

@ -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,

View File

@ -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);