diff --git a/docs/src/test-reporter-api/class-suite.md b/docs/src/test-reporter-api/class-suite.md index dd43dbc82d..184d322663 100644 --- a/docs/src/test-reporter-api/class-suite.md +++ b/docs/src/test-reporter-api/class-suite.md @@ -29,6 +29,16 @@ Returns the list of all test cases in this suite and its descendants, as opposit Location in the source where the suite is defined. Missing for root and project suites. +## property: Suite.parent +- type: <[void]|[Suite]> + +Parent suite or [void] for the root suite. + +## property: Suite.project +- type: <[void]|[TestProject]> + +Configuration of the project this suite belongs to, or [void] for the root suite. + ## property: Suite.suites - type: <[Array]<[Suite]>> diff --git a/docs/src/test-reporter-api/class-testcase.md b/docs/src/test-reporter-api/class-testcase.md index cd0e3d192c..6c53705e1b 100644 --- a/docs/src/test-reporter-api/class-testcase.md +++ b/docs/src/test-reporter-api/class-testcase.md @@ -53,6 +53,11 @@ The maximum number of retries given to this test in the configuration. Learn more about [test retries](./test-retries.md#retries). +## property: TestCase.suite +- type: <[Suite]> + +Suite this test case belongs to. + ## property: TestCase.timeout - type: <[float]> diff --git a/packages/playwright-test/src/dispatcher.ts b/packages/playwright-test/src/dispatcher.ts index 2ff2b065bb..2272a83bbd 100644 --- a/packages/playwright-test/src/dispatcher.ts +++ b/packages/playwright-test/src/dispatcher.ts @@ -230,7 +230,7 @@ export class Dispatcher { retryCandidates.add(failedTestId); let outermostSerialSuite: Suite | undefined; - for (let parent = this._testById.get(failedTestId)!.test.parent; parent; parent = parent.parent) { + for (let parent: Suite | undefined = this._testById.get(failedTestId)!.test.parent; parent; parent = parent.parent) { if (parent._parallelMode === 'serial') outermostSerialSuite = parent; } @@ -241,7 +241,7 @@ export class Dispatcher { // We have failed tests that belong to a serial suite. // We should skip all future tests from the same serial suite. remaining = remaining.filter(test => { - let parent = test.parent; + let parent: Suite | undefined = test.parent; while (parent && !serialSuitesWithFailures.has(parent)) parent = parent.parent; diff --git a/packages/playwright-test/src/loader.ts b/packages/playwright-test/src/loader.ts index 0163a0ffe4..d442fa61a7 100644 --- a/packages/playwright-test/src/loader.ts +++ b/packages/playwright-test/src/loader.ts @@ -40,7 +40,7 @@ export class Loader { constructor(defaultConfig: Config, configOverrides: Config) { this._defaultConfig = defaultConfig; this._configOverrides = configOverrides; - this._fullConfig = baseFullConfig; + this._fullConfig = { ...baseFullConfig }; } static async deserialize(data: SerializedLoaderData): Promise { @@ -426,6 +426,7 @@ const baseFullConfig: FullConfig = { quiet: false, shard: null, updateSnapshots: 'missing', + version: require('../package.json').version, workers: 1, webServer: null, }; diff --git a/packages/playwright-test/src/project.ts b/packages/playwright-test/src/project.ts index d1383b1532..e94304f443 100644 --- a/packages/playwright-test/src/project.ts +++ b/packages/playwright-test/src/project.ts @@ -58,7 +58,7 @@ export class ProjectImpl { let pool = this.buildTestTypePool(test._testType); const parents: Suite[] = []; - for (let parent = test.parent; parent; parent = parent.parent) + for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent) parents.push(parent); parents.reverse(); @@ -82,7 +82,6 @@ export class ProjectImpl { private _cloneEntries(from: Suite, to: Suite, repeatEachIndex: number, filter: (test: TestCase) => boolean): boolean { for (const hook of from._allHooks) { const clone = hook._clone(); - clone.projectName = this.config.name; clone._pool = this.buildPool(hook); clone._projectIndex = this.index; to._addAllHook(clone); @@ -98,7 +97,6 @@ export class ProjectImpl { } else { const pool = this.buildPool(entry); const test = entry._clone(); - test.projectName = this.config.name; test.retries = this.config.retries; test._workerHash = `run${this.index}-${pool.digest}-repeat${repeatEachIndex}`; test._id = `${entry._ordinalInFile}@${entry._requireFile}#run${this.index}-repeat${repeatEachIndex}`; diff --git a/packages/playwright-test/src/reporters/raw.ts b/packages/playwright-test/src/reporters/raw.ts index 9c3b789509..bc1d9369de 100644 --- a/packages/playwright-test/src/reporters/raw.ts +++ b/packages/playwright-test/src/reporters/raw.ts @@ -16,7 +16,6 @@ import fs from 'fs'; import path from 'path'; -import { FullProject } from '../types'; import { FullConfig, Location, Suite, TestCase, TestResult, TestStatus, TestStep } from '../../types/testReporter'; import { assert, calculateSha1 } from 'playwright-core/src/utils/utils'; import { sanitizeForFilePath } from '../util'; @@ -109,7 +108,7 @@ class RawReporter { async onEnd() { const projectSuites = this.suite.suites; for (const suite of projectSuites) { - const project = (suite as any)._projectConfig as FullProject; + const project = suite.project(); assert(project, 'Internal Error: Invalid project structure'); const reportFolder = path.join(project.outputDir, 'report'); fs.mkdirSync(reportFolder, { recursive: true }); @@ -132,7 +131,7 @@ class RawReporter { generateProjectReport(config: FullConfig, suite: Suite): JsonReport { this.config = config; - const project = (suite as any)._projectConfig as FullProject; + const project = suite.project(); assert(project, 'Internal Error: Invalid project structure'); const report: JsonReport = { config, diff --git a/packages/playwright-test/src/runner.ts b/packages/playwright-test/src/runner.ts index 4e897438ac..efcdc80467 100644 --- a/packages/playwright-test/src/runner.ts +++ b/packages/playwright-test/src/runner.ts @@ -509,7 +509,7 @@ function createTestGroups(rootSuite: Suite): TestGroup[] { } let insideParallel = false; - for (let parent = test.parent; parent; parent = parent.parent) + for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent) insideParallel = insideParallel || parent._parallelMode === 'parallel'; if (insideParallel) { diff --git a/packages/playwright-test/src/test.ts b/packages/playwright-test/src/test.ts index a4503d8ad3..3e666bed71 100644 --- a/packages/playwright-test/src/test.ts +++ b/packages/playwright-test/src/test.ts @@ -22,20 +22,12 @@ import { FullProject } from './types'; class Base { title: string; - parent?: Suite; - _only = false; _requireFile: string = ''; constructor(title: string) { this.title = title; } - - titlePath(): string[] { - const titlePath = this.parent ? this.parent.titlePath() : []; - titlePath.push(this.title); - return titlePath; - } } export type Modifier = { @@ -49,6 +41,7 @@ export class Suite extends Base implements reporterTypes.Suite { suites: Suite[] = []; tests: TestCase[] = []; location?: Location; + parent?: Suite; _use: FixturesWithLocation[] = []; _isDescribe = false; _entries: (Suite | TestCase)[] = []; @@ -91,6 +84,12 @@ export class Suite extends Base implements reporterTypes.Suite { return result; } + titlePath(): string[] { + const titlePath = this.parent ? this.parent.titlePath() : []; + titlePath.push(this.title); + return titlePath; + } + _getOnlyItems(): (TestCase | Suite)[] { const items: (TestCase | Suite)[] = []; if (this._only) @@ -113,19 +112,24 @@ export class Suite extends Base implements reporterTypes.Suite { suite._modifiers = this._modifiers.slice(); suite._isDescribe = this._isDescribe; suite._parallelMode = this._parallelMode; + suite._projectConfig = this._projectConfig; return suite; } + + project(): FullProject | undefined { + return this._projectConfig || this.parent?.project(); + } } export class TestCase extends Base implements reporterTypes.TestCase { fn: Function; results: reporterTypes.TestResult[] = []; location: Location; + parent!: Suite; expectedStatus: reporterTypes.TestStatus = 'passed'; timeout = 0; annotations: Annotations = []; - projectName = ''; retries = 0; _type: 'beforeAll' | 'afterAll' | 'test'; @@ -146,6 +150,12 @@ export class TestCase extends Base implements reporterTypes.TestCase { this.location = location; } + titlePath(): string[] { + const titlePath = this.parent ? this.parent.titlePath() : []; + titlePath.push(this.title); + return titlePath; + } + outcome(): 'skipped' | 'expected' | 'unexpected' | 'flaky' { const nonSkipped = this.results.filter(result => result.status !== 'skipped'); if (!nonSkipped.length) diff --git a/packages/playwright-test/src/workerRunner.ts b/packages/playwright-test/src/workerRunner.ts index bfc9d693ff..b7344dc2b7 100644 --- a/packages/playwright-test/src/workerRunner.ts +++ b/packages/playwright-test/src/workerRunner.ts @@ -323,7 +323,7 @@ export class WorkerRunner extends EventEmitter { }; // Inherit test.setTimeout() from parent suites. - for (let suite = test.parent; suite; suite = suite.parent) { + for (let suite: Suite | undefined = test.parent; suite; suite = suite.parent) { if (suite._timeout !== undefined) { testInfo.setTimeout(suite._timeout); break; @@ -420,7 +420,7 @@ export class WorkerRunner extends EventEmitter { private async _runBeforeHooks(test: TestCase, testInfo: TestInfoImpl) { try { const beforeEachModifiers: Modifier[] = []; - for (let s = test.parent; s; s = s.parent) { + for (let s: Suite | undefined = test.parent; s; s = s.parent) { const modifiers = s._modifiers.filter(modifier => !this._fixtureRunner.dependsOnWorkerFixturesOnly(modifier.fn, modifier.location)); beforeEachModifiers.push(...modifiers.reverse()); } diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index fb9d1381a4..b426f86a92 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -698,6 +698,7 @@ export interface FullConfig { * Also available in the [command line](https://playwright.dev/docs/test-cli) with the `--max-failures` and `-x` options. */ maxFailures: number; + version: string; /** * Whether to preserve test output in the * [testConfig.outputDir](https://playwright.dev/docs/api/class-testconfig#test-config-output-dir). Defaults to `'always'`. diff --git a/packages/playwright-test/types/testReporter.d.ts b/packages/playwright-test/types/testReporter.d.ts index 4dd0a91936..86acc7603d 100644 --- a/packages/playwright-test/types/testReporter.d.ts +++ b/packages/playwright-test/types/testReporter.d.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import type { FullConfig, TestStatus, TestError } from './test'; +import type { FullConfig, FullProject, TestStatus, TestError } from './test'; export type { FullConfig, TestStatus, TestError } from './test'; /** @@ -57,6 +57,10 @@ export interface Location { * [reporter.onBegin(config, suite)](https://playwright.dev/docs/api/class-reporter#reporter-on-begin) method. */ export interface Suite { + /** + * Parent suite or [void] for the root suite. + */ + parent?: Suite; /** * Suite title. * - Empty for root suite. @@ -89,6 +93,10 @@ export interface Suite { * [suite.tests](https://playwright.dev/docs/api/class-suite#suite-tests). */ allTests(): TestCase[]; + /** + * Configuration of the project this suite belongs to, or [void] for the root suite. + */ + project(): FullProject | undefined; } /** @@ -98,6 +106,7 @@ export interface Suite { * or repeated multiple times, it will have multiple `TestCase` objects in corresponding projects' suites. */ export interface TestCase { + parent: Suite; /** * Test title as passed to the [test.(call)(title, testFunction)](https://playwright.dev/docs/api/class-test#test-call) * call. diff --git a/tests/playwright-test/reporter.spec.ts b/tests/playwright-test/reporter.spec.ts index 9bec348d48..8529a083fa 100644 --- a/tests/playwright-test/reporter.spec.ts +++ b/tests/playwright-test/reporter.spec.ts @@ -71,9 +71,16 @@ test('should work with custom reporter', async ({ runInlineTest }) => { } onBegin(config, suite) { console.log('\\n%%reporter-begin-' + this.options.begin + '%%'); + console.log('\\n%%version-' + config.version); } onTestBegin(test) { - console.log('\\n%%reporter-testbegin-' + test.title + '-' + test.titlePath()[1] + '%%'); + const projectName = test.titlePath()[1]; + console.log('\\n%%reporter-testbegin-' + test.title + '-' + projectName + '%%'); + const suite = test.parent; + if (!suite.tests.includes(test)) + console.log('\\n%%error-inconsistent-parent'); + if (test.parent.project().name !== projectName) + console.log('\\n%%error-inconsistent-project-name'); } onStdOut() { console.log('\\n%%reporter-stdout%%'); @@ -126,6 +133,7 @@ test('should work with custom reporter', async ({ runInlineTest }) => { expect(result.exitCode).toBe(0); expect(result.output.split('\n').filter(line => line.startsWith('%%'))).toEqual([ '%%reporter-begin-begin%%', + '%%version-' + require('../../packages/playwright-test/package.json').version, '%%reporter-testbegin-is run-foo%%', '%%reporter-stdout%%', '%%reporter-stderr%%', diff --git a/utils/generate_types/overrides-test.d.ts b/utils/generate_types/overrides-test.d.ts index 840cbf626f..c34943ef6b 100644 --- a/utils/generate_types/overrides-test.d.ts +++ b/utils/generate_types/overrides-test.d.ts @@ -142,6 +142,7 @@ export interface FullConfig { grep: RegExp | RegExp[]; grepInvert: RegExp | RegExp[] | null; maxFailures: number; + version: string; preserveOutput: PreserveOutput; projects: FullProject[]; reporter: ReporterDescription[]; diff --git a/utils/generate_types/overrides-testReporter.d.ts b/utils/generate_types/overrides-testReporter.d.ts index 5fa2d75df8..4a72b7b788 100644 --- a/utils/generate_types/overrides-testReporter.d.ts +++ b/utils/generate_types/overrides-testReporter.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import type { FullConfig, TestStatus, TestError } from './test'; +import type { FullConfig, FullProject, TestStatus, TestError } from './test'; export type { FullConfig, TestStatus, TestError } from './test'; export interface Location { @@ -24,15 +24,18 @@ export interface Location { } export interface Suite { + parent?: Suite; title: string; location?: Location; suites: Suite[]; tests: TestCase[]; titlePath(): string[]; allTests(): TestCase[]; + project(): FullProject | undefined; } export interface TestCase { + parent: Suite; title: string; location: Location; titlePath(): string[];