diff --git a/packages/playwright-test/src/common/config.ts b/packages/playwright-test/src/common/config.ts index 390548470d..feb7050a6d 100644 --- a/packages/playwright-test/src/common/config.ts +++ b/packages/playwright-test/src/common/config.ts @@ -153,10 +153,6 @@ export class FullProjectInternal { id = ''; deps: FullProjectInternal[] = []; - static from(project: FullProject): FullProjectInternal { - return (project as any)[projectInternalSymbol]; - } - constructor(configDir: string, config: Config, fullConfig: FullConfigInternal, projectConfig: Project, configCLIOverrides: ConfigCLIOverrides, throwawayArtifactsPath: string) { this.fullConfig = fullConfig; const testDir = takeFirst(pathResolve(configDir, projectConfig.testDir), pathResolve(configDir, config.testDir), fullConfig.configDir); diff --git a/packages/playwright-test/src/common/globals.ts b/packages/playwright-test/src/common/globals.ts index fa4821a87b..16a4673b1d 100644 --- a/packages/playwright-test/src/common/globals.ts +++ b/packages/playwright-test/src/common/globals.ts @@ -16,7 +16,6 @@ import type { TestInfoImpl } from '../worker/testInfo'; import type { Suite } from './test'; -import { FullProjectInternal } from './config'; import type { FullConfigInternal } from './config'; let currentTestInfoValue: TestInfoImpl | null = null; @@ -39,7 +38,7 @@ export function currentExpectTimeout(options: { timeout?: number }) { const testInfo = currentTestInfo(); if (options.timeout !== undefined) return options.timeout; - let defaultExpectTimeout = testInfo?.project ? FullProjectInternal.from(testInfo.project).expect?.timeout : undefined; + let defaultExpectTimeout = testInfo?._projectInternal?.expect?.timeout; if (typeof defaultExpectTimeout === 'undefined') defaultExpectTimeout = 5000; return defaultExpectTimeout; diff --git a/packages/playwright-test/src/common/test.ts b/packages/playwright-test/src/common/test.ts index fe79121074..ce05cb897a 100644 --- a/packages/playwright-test/src/common/test.ts +++ b/packages/playwright-test/src/common/test.ts @@ -51,7 +51,7 @@ export class Suite extends Base implements SuitePrivate { _staticAnnotations: Annotation[] = []; _modifiers: Modifier[] = []; _parallelMode: 'default' | 'serial' | 'parallel' = 'default'; - _projectConfig: FullProjectInternal | undefined; + _fullProject: FullProjectInternal | undefined; _fileId: string | undefined; readonly _type: 'root' | 'project' | 'file' | 'describe'; @@ -194,12 +194,12 @@ export class Suite extends Base implements SuitePrivate { const suite = Suite._parse(data); suite._use = this._use.slice(); suite._hooks = this._hooks.slice(); - suite._projectConfig = this._projectConfig; + suite._fullProject = this._fullProject; return suite; } project(): FullProject | undefined { - return this._projectConfig?.project || this.parent?.project(); + return this._fullProject?.project || this.parent?.project(); } } diff --git a/packages/playwright-test/src/reporters/base.ts b/packages/playwright-test/src/reporters/base.ts index e8f6477b9b..c60be1ba98 100644 --- a/packages/playwright-test/src/reporters/base.ts +++ b/packages/playwright-test/src/reporters/base.ts @@ -21,7 +21,7 @@ import type { FullConfig, TestCase, Suite, TestResult, TestError, FullResult, Te import type { SuitePrivate } from '../../types/reporterPrivate'; import { codeFrameColumns } from '../common/babelBundle'; import { monotonicTime } from 'playwright-core/lib/utils'; - +import type { FullProject } from '../../types/test'; export type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' }; export const kOutputSymbol = Symbol('output'); @@ -523,6 +523,23 @@ function fitToWidth(line: string, width: number, prefix?: string): string { return taken.reverse().join(''); } +export function uniqueProjectIds(projects: FullProject[]): Map { + const usedNames = new Set(); + const result = new Map(); + for (const p of projects) { + const name = p.name || ''; + for (let i = 0; i < projects.length; ++i) { + const candidate = name + (i ? i : ''); + if (usedNames.has(candidate)) + continue; + result.set(p, candidate); + usedNames.add(candidate); + break; + } + } + return result; +} + function belongsToNodeModules(file: string) { return file.includes(`${path.sep}node_modules${path.sep}`); } diff --git a/packages/playwright-test/src/reporters/json.ts b/packages/playwright-test/src/reporters/json.ts index 33fd3a58cd..bfe462b9e5 100644 --- a/packages/playwright-test/src/reporters/json.ts +++ b/packages/playwright-test/src/reporters/json.ts @@ -17,10 +17,10 @@ import fs from 'fs'; import path from 'path'; import type { FullConfig, TestCase, Suite, TestResult, TestError, TestStep, FullResult, Location, Reporter, JSONReport, JSONReportSuite, JSONReportSpec, JSONReportTest, JSONReportTestResult, JSONReportTestStep, JSONReportError } from '../../types/testReporter'; -import { formatError, prepareErrorStack } from './base'; +import { formatError, prepareErrorStack, uniqueProjectIds } from './base'; import { MultiMap } from 'playwright-core/lib/utils'; import { assert } from 'playwright-core/lib/utils'; -import { FullProjectInternal } from '../common/config'; +import type { FullProject } from '../../types/test'; export function toPosixPath(aPath: string): string { return aPath.split(path.sep).join(path.posix.sep); @@ -54,6 +54,7 @@ class JSONReporter implements Reporter { } private _serializeReport(): JSONReport { + const projectIds = uniqueProjectIds(this.config.projects); return { config: { ...removePrivateFields(this.config), @@ -64,7 +65,7 @@ class JSONReporter implements Reporter { repeatEach: project.repeatEach, retries: project.retries, metadata: project.metadata, - id: FullProjectInternal.from(project).id, + id: projectIds.get(project)!, name: project.name, testDir: toPosixPath(project.testDir), testIgnore: serializePatterns(project.testIgnore), @@ -73,15 +74,15 @@ class JSONReporter implements Reporter { }; }) }, - suites: this._mergeSuites(this.suite.suites), + suites: this._mergeSuites(this.suite.suites, projectIds), errors: this._errors }; } - private _mergeSuites(suites: Suite[]): JSONReportSuite[] { + private _mergeSuites(suites: Suite[], projectIds: Map): JSONReportSuite[] { const fileSuites = new MultiMap(); for (const projectSuite of suites) { - const projectId = FullProjectInternal.from(projectSuite.project()!).id; + const projectId = projectIds.get(projectSuite.project()!)!; const projectName = projectSuite.project()!.name; for (const fileSuite of projectSuite.suites) { const file = fileSuite.location!.file; diff --git a/packages/playwright-test/src/reporters/teleEmitter.ts b/packages/playwright-test/src/reporters/teleEmitter.ts index b232850d41..d467abcfe2 100644 --- a/packages/playwright-test/src/reporters/teleEmitter.ts +++ b/packages/playwright-test/src/reporters/teleEmitter.ts @@ -18,10 +18,12 @@ import type { FullConfig, FullResult, Reporter, TestError, TestResult, TestStep, import type { Suite, TestCase } from '../common/test'; import type { JsonConfig, JsonProject, JsonSuite, JsonTestCase, JsonTestEnd, JsonTestResultEnd, JsonTestResultStart, JsonTestStepEnd, JsonTestStepStart } from '../isomorphic/teleReceiver'; import type { SuitePrivate } from '../../types/reporterPrivate'; -import { FullConfigInternal, FullProjectInternal } from '../common/config'; +import { FullConfigInternal } from '../common/config'; import { createGuid } from 'playwright-core/lib/utils'; import { serializeRegexPatterns } from '../isomorphic/teleReceiver'; import path from 'path'; +import type { FullProject } from '../../types/test'; +import { uniqueProjectIds } from './base'; export class TeleReporterEmitter implements Reporter { private _messageSink: (message: any) => void; @@ -34,8 +36,9 @@ export class TeleReporterEmitter implements Reporter { onBegin(config: FullConfig, suite: Suite) { this._rootDir = config.rootDir; const projects: any[] = []; + const projectIds = uniqueProjectIds(config.projects); for (const projectSuite of suite.suites) { - const report = this._serializeProject(projectSuite); + const report = this._serializeProject(projectSuite, projectIds); projects.push(report); } this._messageSink({ method: 'onBegin', params: { config: this._serializeConfig(config), projects } }); @@ -128,10 +131,10 @@ export class TeleReporterEmitter implements Reporter { }; } - private _serializeProject(suite: Suite): JsonProject { + private _serializeProject(suite: Suite, projectIds: Map): JsonProject { const project = suite.project()!; const report: JsonProject = { - id: FullProjectInternal.from(project).id, + id: projectIds.get(project)!, metadata: project.metadata, name: project.name, outputDir: this._relativePath(project.outputDir), diff --git a/packages/playwright-test/src/runner/loadUtils.ts b/packages/playwright-test/src/runner/loadUtils.ts index c5eee1310d..8a5b9a7b77 100644 --- a/packages/playwright-test/src/runner/loadUtils.ts +++ b/packages/playwright-test/src/runner/loadUtils.ts @@ -172,7 +172,7 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho { // Filtering only and sharding might have reduced the number of top-level projects. // Build the project closure to only include dependencies that are still needed. - const projectClosure = new Map(buildProjectsClosure(rootSuite.suites.map(suite => suite._projectConfig!))); + const projectClosure = new Map(buildProjectsClosure(rootSuite.suites.map(suite => suite._fullProject!))); // Clone file suites for dependency projects. for (const [project, fileSuites] of testRun.projectSuites) { @@ -186,7 +186,7 @@ export async function createRootSuite(testRun: TestRun, errors: TestError[], sho async function createProjectSuite(fileSuites: Suite[], project: FullProjectInternal, options: { cliFileFilters: TestFileFilter[], cliTitleMatcher?: Matcher, testIdMatcher?: Matcher }): Promise { const projectSuite = new Suite(project.project.name, 'project'); - projectSuite._projectConfig = project; + projectSuite._fullProject = project; if (project.fullyParallel) projectSuite._parallelMode = 'parallel'; for (const fileSuite of fileSuites) { diff --git a/packages/playwright-test/src/runner/tasks.ts b/packages/playwright-test/src/runner/tasks.ts index 9b76c8da82..41fa04ae92 100644 --- a/packages/playwright-test/src/runner/tasks.ts +++ b/packages/playwright-test/src/runner/tasks.ts @@ -180,7 +180,7 @@ function createPhasesTask(): Task { let maxConcurrentTestGroups = 0; const processed = new Set(); - const projectToSuite = new Map(testRun.rootSuite!.suites.map(suite => [suite._projectConfig!, suite])); + const projectToSuite = new Map(testRun.rootSuite!.suites.map(suite => [suite._fullProject!, suite])); for (let i = 0; i < projectToSuite.size; i++) { // Find all projects that have all their dependencies processed by previous phases. const phaseProjects: FullProjectInternal[] = [];