diff --git a/docs/src/test-api/class-testproject.md b/docs/src/test-api/class-testproject.md index c19d113e42..afd1e7bcab 100644 --- a/docs/src/test-api/class-testproject.md +++ b/docs/src/test-api/class-testproject.md @@ -200,7 +200,7 @@ Use [`property: TestConfig.retries`] to change this option for all projects. * since: v1.34 - type: ?<[string]> -Name of a project that needs to run after this and any dependent projects have finished. Teardown is useful to cleanup any resources acquired by this project. +Name of a project that needs to run after this and all dependent projects have finished. Teardown is useful to cleanup any resources acquired by this project. Passing `--no-deps` argument ignores [`property: TestProject.teardown`] and behaves as if it was not specified. diff --git a/packages/playwright-test/src/common/config.ts b/packages/playwright-test/src/common/config.ts index bd4a7d4473..ac608920d8 100644 --- a/packages/playwright-test/src/common/config.ts +++ b/packages/playwright-test/src/common/config.ts @@ -209,7 +209,7 @@ function resolveReporters(reporters: Config['reporter'], rootDir: string): Repor } function resolveProjectDependencies(projects: FullProjectInternal[]) { - const teardownToSetup = new Map(); + const teardownSet = new Set(); for (const project of projects) { for (const dependencyName of project.project.dependencies) { const dependencies = projects.filter(p => p.project.name === dependencyName); @@ -227,18 +227,16 @@ function resolveProjectDependencies(projects: FullProjectInternal[]) { throw new Error(`Project teardowns should have unique names, reading ${project.project.teardown}`); const teardown = teardowns[0]; project.teardown = teardown; - if (teardownToSetup.has(teardown)) - throw new Error(`Project ${teardown.project.name} can not be designated as teardown to multiple projects (${teardownToSetup.get(teardown)!.project.name} and ${project.project.name})`); - teardownToSetup.set(teardown, project); + teardownSet.add(teardown); } } - for (const teardown of teardownToSetup.keys()) { + for (const teardown of teardownSet) { if (teardown.deps.length) throw new Error(`Teardown project ${teardown.project.name} must not have dependencies`); } for (const project of projects) { for (const dep of project.deps) { - if (teardownToSetup.has(dep)) + if (teardownSet.has(dep)) throw new Error(`Project ${project.project.name} must not depend on a teardown project ${dep.project.name}`); } } diff --git a/packages/playwright-test/src/runner/projectUtils.ts b/packages/playwright-test/src/runner/projectUtils.ts index 5c9e3794dc..d91f83a4bc 100644 --- a/packages/playwright-test/src/runner/projectUtils.ts +++ b/packages/playwright-test/src/runner/projectUtils.ts @@ -49,11 +49,14 @@ export function filterProjects(projects: FullProjectInternal[], projectNames?: s return result; } -export function buildTeardownToSetupMap(projects: FullProjectInternal[]): Map { - const result = new Map(); +export function buildTeardownToSetupsMap(projects: FullProjectInternal[]): Map { + const result = new Map(); for (const project of projects) { - if (project.teardown) - result.set(project.teardown, project); + if (project.teardown) { + const setups = result.get(project.teardown) || []; + setups.push(project); + result.set(project.teardown, setups); + } } return result; } @@ -78,7 +81,7 @@ export function buildProjectsClosure(projects: FullProjectInternal[]): Map { +export function buildDependentProjects(forProjects: FullProjectInternal[], projects: FullProjectInternal[]): Set { const reverseDeps = new Map(projects.map(p => ([p, []]))); for (const project of projects) { for (const dep of project.deps) @@ -97,7 +100,8 @@ export function buildDependentProjects(forProject: FullProjectInternal, projects if (project.teardown) visit(depth + 1, project.teardown); }; - visit(0, forProject); + for (const forProject of forProjects) + visit(0, forProject); return result; } diff --git a/packages/playwright-test/src/runner/tasks.ts b/packages/playwright-test/src/runner/tasks.ts index e771982dc9..1e9e112230 100644 --- a/packages/playwright-test/src/runner/tasks.ts +++ b/packages/playwright-test/src/runner/tasks.ts @@ -28,7 +28,7 @@ import type { FullConfigInternal, FullProjectInternal } from '../common/config'; import { collectProjectsAndTestFiles, createRootSuite, loadFileSuites, loadGlobalHook } from './loadUtils'; import type { Matcher } from '../util'; import type { Suite } from '../common/test'; -import { buildDependentProjects, buildTeardownToSetupMap } from './projectUtils'; +import { buildDependentProjects, buildTeardownToSetupsMap } from './projectUtils'; const removeFolderAsync = promisify(rimraf); const readDirAsync = promisify(fs.readdir); @@ -184,12 +184,12 @@ function createPhasesTask(): Task { const processed = new Set(); const projectToSuite = new Map(testRun.rootSuite!.suites.map(suite => [suite._fullProject!, suite])); const allProjects = [...projectToSuite.keys()]; - const teardownToSetup = buildTeardownToSetupMap(allProjects); - const teardownToSetupDependents = new Map(); - for (const [teardown, setup] of teardownToSetup) { - const closure = buildDependentProjects(setup, allProjects); + const teardownToSetups = buildTeardownToSetupsMap(allProjects); + const teardownToSetupsDependents = new Map(); + for (const [teardown, setups] of teardownToSetups) { + const closure = buildDependentProjects(setups, allProjects); closure.delete(teardown); - teardownToSetupDependents.set(teardown, [...closure]); + teardownToSetupsDependents.set(teardown, [...closure]); } for (let i = 0; i < projectToSuite.size; i++) { @@ -198,7 +198,7 @@ function createPhasesTask(): Task { for (const project of projectToSuite.keys()) { if (processed.has(project)) continue; - const projectsThatShouldFinishFirst = [...project.deps, ...(teardownToSetupDependents.get(project) || [])]; + const projectsThatShouldFinishFirst = [...project.deps, ...(teardownToSetupsDependents.get(project) || [])]; if (projectsThatShouldFinishFirst.find(p => !processed.has(p))) continue; phaseProjects.push(project); @@ -240,7 +240,7 @@ function createRunTestsTask(): Task { const { phases } = testRun; const successfulProjects = new Set(); const extraEnvByProjectId: EnvByProjectId = new Map(); - const teardownToSetup = buildTeardownToSetupMap(phases.map(phase => phase.projects.map(p => p.project)).flat()); + const teardownToSetups = buildTeardownToSetupsMap(phases.map(phase => phase.projects.map(p => p.project)).flat()); for (const { dispatcher, projects } of phases) { // Each phase contains dispatcher and a set of test groups. @@ -252,9 +252,8 @@ function createRunTestsTask(): Task { let extraEnv: Record = {}; for (const dep of project.deps) extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(dep.id) }; - const setupForTeardown = teardownToSetup.get(project); - if (setupForTeardown) - extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(setupForTeardown.id) }; + for (const setup of teardownToSetups.get(project) || []) + extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(setup.id) }; extraEnvByProjectId.set(project.id, extraEnv); const hasFailedDeps = project.deps.some(p => !successfulProjects.has(p)); diff --git a/packages/playwright-test/types/test.d.ts b/packages/playwright-test/types/test.d.ts index 0e4639682d..5a5e1828f0 100644 --- a/packages/playwright-test/types/test.d.ts +++ b/packages/playwright-test/types/test.d.ts @@ -285,7 +285,7 @@ export interface FullProject { */ retries: number; /** - * Name of a project that needs to run after this and any dependent projects have finished. Teardown is useful to + * Name of a project that needs to run after this and all dependent projects have finished. Teardown is useful to * cleanup any resources acquired by this project. * * Passing `--no-deps` argument ignores @@ -6325,7 +6325,7 @@ interface TestProject { snapshotPathTemplate?: string; /** - * Name of a project that needs to run after this and any dependent projects have finished. Teardown is useful to + * Name of a project that needs to run after this and all dependent projects have finished. Teardown is useful to * cleanup any resources acquired by this project. * * Passing `--no-deps` argument ignores diff --git a/tests/playwright-test/deps.spec.ts b/tests/playwright-test/deps.spec.ts index 5c10b4b850..f1cae8d5cc 100644 --- a/tests/playwright-test/deps.spec.ts +++ b/tests/playwright-test/deps.spec.ts @@ -541,23 +541,26 @@ test('should complain about teardown having a dependency', async ({ runInlineTes expect(result.output).toContain(`Teardown project B must not have dependencies`); }); -test('should complain about teardown used multiple times', async ({ runInlineTest }) => { +test('should support the same teardown used multiple times', async ({ runInlineTest }) => { const result = await runInlineTest({ 'playwright.config.ts': ` module.exports = { projects: [ - { name: 'A', teardown: 'C' }, - { name: 'B', teardown: 'C' }, - { name: 'C' }, + { name: 'A', teardown: 'D' }, + { name: 'B', teardown: 'D' }, + { name: 'D' }, ], };`, 'test.spec.ts': ` import { test, expect } from '@playwright/test'; - test('test', () => {}); + test('test', async ({}, testInfo) => { + console.log('\\n%%' + testInfo.project.name); + }); `, - }); - expect(result.exitCode).toBe(1); - expect(result.output).toContain(`Project C can not be designated as teardown to multiple projects (A and B)`); + }, { workers: 1 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(3); + expect(result.outputLines).toEqual(['A', 'B', 'D']); }); test('should only apply --repeat-each to top-level', async ({ runInlineTest }) => {