diff --git a/src/test/dispatcher.ts b/src/test/dispatcher.ts index 17c3eb6aa4..9c1f9fe4c3 100644 --- a/src/test/dispatcher.ts +++ b/src/test/dispatcher.ts @@ -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>(); 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 => {}); } diff --git a/src/test/loader.ts b/src/test/loader.ts index a01c071bae..222b5461ac 100644 --- a/src/test/loader.ts +++ b/src/test/loader.ts @@ -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); diff --git a/src/test/project.ts b/src/test/project.ts index 69cf406384..50dc75e613 100644 --- a/src/test/project.ts +++ b/src/test/project.ts @@ -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); diff --git a/src/test/reporter.ts b/src/test/reporter.ts index b054bb2020..ff755db44b 100644 --- a/src/test/reporter.ts +++ b/src/test/reporter.ts @@ -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; + 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; } diff --git a/src/test/reporters/base.ts b/src/test/reporters/base.ts index a174e1ac86..20771a8093 100644 --- a/src/test/reporters/base.ts +++ b/src/test/reporters/base.ts @@ -33,18 +33,12 @@ export class BaseReporter implements Reporter { fileDurations = new Map(); 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'); } diff --git a/src/test/reporters/empty.ts b/src/test/reporters/empty.ts index 234531c1ea..382d1de43d 100644 --- a/src/test/reporters/empty.ts +++ b/src/test/reporters/empty.ts @@ -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; diff --git a/src/test/reporters/json.ts b/src/test/reporters/json.ts index c96def7920..25557afcb2 100644 --- a/src/test/reporters/json.ts +++ b/src/test/reporters/json.ts @@ -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 & { @@ -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(); 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), }; } diff --git a/src/test/reporters/junit.ts b/src/test/reporters/junit.ts index 0b71d30318..a35c314ab8 100644 --- a/src/test/reporters/junit.ts +++ b/src/test/reporters/junit.ts @@ -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)) diff --git a/src/test/reporters/line.ts b/src/test/reporters/line.ts index 5da7f60ab8..d6eef00559 100644 --- a/src/test/reporters/line.ts +++ b/src/test/reporters/line.ts @@ -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(); } diff --git a/src/test/reporters/list.ts b/src/test/reporters/list.ts index 9d12221150..f7437c4ace 100644 --- a/src/test/reporters/list.ts +++ b/src/test/reporters/list.ts @@ -38,7 +38,6 @@ class ListReporter extends BaseReporter { } onTestBegin(test: Test) { - super.onTestBegin(test); if (process.stdout.isTTY) { if (this._needNewLine) { this._needNewLine = false; diff --git a/src/test/reporters/multiplexer.ts b/src/test/reporters/multiplexer.ts index 4c96dd8ab7..3355260e93 100644 --- a/src/test/reporters/multiplexer.ts +++ b/src/test/reporters/multiplexer.ts @@ -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); } } diff --git a/src/test/runner.ts b/src/test/runner.ts index d661bf100d..22b22c5d74 100644 --- a/src/test/runner.ts +++ b/src/test/runner.ts @@ -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 { } 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}`; } diff --git a/src/test/test.ts b/src/test/test.ts index 5fbf8adf10..4cc802f7d3 100644 --- a/src/test/test.ts +++ b/src/test/test.ts @@ -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, diff --git a/src/test/testType.ts b/src/test/testType.ts index 2b2d99332d..a16da74f44 100644 --- a/src/test/testType.ts +++ b/src/test/testType.ts @@ -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; diff --git a/src/test/types.ts b/src/test/types.ts index 12591f86f6..d9bf3b2898 100644 --- a/src/test/types.ts +++ b/src/test/types.ts @@ -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; diff --git a/src/test/workerRunner.ts b/src/test/workerRunner.ts index 53b3200345..8a354f7fce 100644 --- a/src/test/workerRunner.ts +++ b/src/test/workerRunner.ts @@ -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, diff --git a/tests/playwright-test/junit-reporter.spec.ts b/tests/playwright-test/junit-reporter.spec.ts index c6962d4eee..b975544e34 100644 --- a/tests/playwright-test/junit-reporter.spec.ts +++ b/tests/playwright-test/junit-reporter.spec.ts @@ -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);