mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
feat: project.teardown that runs after all dependents have finished (#22696)
This replicates globalTeardown in the deps world. Fixes #21914.
This commit is contained in:
parent
2a675026de
commit
dbb218a9d5
@ -57,7 +57,7 @@ behaves as if they were not specified.
|
|||||||
Using dependencies allows global setup to produce traces and other artifacts,
|
Using dependencies allows global setup to produce traces and other artifacts,
|
||||||
see the setup steps in the test report, etc.
|
see the setup steps in the test report, etc.
|
||||||
|
|
||||||
For example:
|
**Usage**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// playwright.config.ts
|
// playwright.config.ts
|
||||||
@ -198,6 +198,52 @@ Use [`method: Test.describe.configure`] to change the number of retries for a sp
|
|||||||
|
|
||||||
Use [`property: TestConfig.retries`] to change this option for all projects.
|
Use [`property: TestConfig.retries`] to change this option for all projects.
|
||||||
|
|
||||||
|
## property: TestProject.teardown
|
||||||
|
* 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.
|
||||||
|
|
||||||
|
Passing `--no-deps` argument ignores [`property: TestProject.teardown`] and behaves as if it was not specified.
|
||||||
|
|
||||||
|
**Usage**
|
||||||
|
|
||||||
|
A common pattern is a "setup" dependency that has a corresponding "teardown":
|
||||||
|
|
||||||
|
```js
|
||||||
|
// playwright.config.ts
|
||||||
|
import { defineConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'setup',
|
||||||
|
testMatch: /global.setup\.ts/,
|
||||||
|
teardown: 'teardown',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'teardown',
|
||||||
|
testMatch: /global.teardown\.ts/,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: devices['Desktop Chrome'],
|
||||||
|
dependencies: ['setup'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'firefox',
|
||||||
|
use: devices['Desktop Firefox'],
|
||||||
|
dependencies: ['setup'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'webkit',
|
||||||
|
use: devices['Desktop Safari'],
|
||||||
|
dependencies: ['setup'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## property: TestProject.testDir
|
## property: TestProject.testDir
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[string]>
|
- type: ?<[string]>
|
||||||
|
|||||||
@ -152,6 +152,7 @@ export class FullProjectInternal {
|
|||||||
readonly snapshotPathTemplate: string;
|
readonly snapshotPathTemplate: string;
|
||||||
id = '';
|
id = '';
|
||||||
deps: FullProjectInternal[] = [];
|
deps: FullProjectInternal[] = [];
|
||||||
|
teardown: FullProjectInternal | undefined;
|
||||||
|
|
||||||
constructor(configDir: string, config: Config, fullConfig: FullConfigInternal, projectConfig: Project, configCLIOverrides: ConfigCLIOverrides, throwawayArtifactsPath: string) {
|
constructor(configDir: string, config: Config, fullConfig: FullConfigInternal, projectConfig: Project, configCLIOverrides: ConfigCLIOverrides, throwawayArtifactsPath: string) {
|
||||||
this.fullConfig = fullConfig;
|
this.fullConfig = fullConfig;
|
||||||
@ -174,6 +175,7 @@ export class FullProjectInternal {
|
|||||||
timeout: takeFirst(configCLIOverrides.timeout, projectConfig.timeout, config.timeout, defaultTimeout),
|
timeout: takeFirst(configCLIOverrides.timeout, projectConfig.timeout, config.timeout, defaultTimeout),
|
||||||
use: mergeObjects(config.use, projectConfig.use, configCLIOverrides.use),
|
use: mergeObjects(config.use, projectConfig.use, configCLIOverrides.use),
|
||||||
dependencies: projectConfig.dependencies || [],
|
dependencies: projectConfig.dependencies || [],
|
||||||
|
teardown: projectConfig.teardown,
|
||||||
};
|
};
|
||||||
(this.project as any)[projectInternalSymbol] = this;
|
(this.project as any)[projectInternalSymbol] = this;
|
||||||
this.fullyParallel = takeFirst(configCLIOverrides.fullyParallel, projectConfig.fullyParallel, config.fullyParallel, undefined);
|
this.fullyParallel = takeFirst(configCLIOverrides.fullyParallel, projectConfig.fullyParallel, config.fullyParallel, undefined);
|
||||||
@ -205,6 +207,7 @@ function resolveReporters(reporters: Config['reporter'], rootDir: string): Repor
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveProjectDependencies(projects: FullProjectInternal[]) {
|
function resolveProjectDependencies(projects: FullProjectInternal[]) {
|
||||||
|
const teardownToSetup = new Map<FullProjectInternal, FullProjectInternal>();
|
||||||
for (const project of projects) {
|
for (const project of projects) {
|
||||||
for (const dependencyName of project.project.dependencies) {
|
for (const dependencyName of project.project.dependencies) {
|
||||||
const dependencies = projects.filter(p => p.project.name === dependencyName);
|
const dependencies = projects.filter(p => p.project.name === dependencyName);
|
||||||
@ -214,6 +217,28 @@ function resolveProjectDependencies(projects: FullProjectInternal[]) {
|
|||||||
throw new Error(`Project dependencies should have unique names, reading ${dependencyName}`);
|
throw new Error(`Project dependencies should have unique names, reading ${dependencyName}`);
|
||||||
project.deps.push(...dependencies);
|
project.deps.push(...dependencies);
|
||||||
}
|
}
|
||||||
|
if (project.project.teardown) {
|
||||||
|
const teardowns = projects.filter(p => p.project.name === project.project.teardown);
|
||||||
|
if (!teardowns.length)
|
||||||
|
throw new Error(`Project '${project.project.name}' has unknown teardown project '${project.project.teardown}'`);
|
||||||
|
if (teardowns.length > 1)
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const teardown of teardownToSetup.keys()) {
|
||||||
|
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))
|
||||||
|
throw new Error(`Project ${project.project.name} must not depend on a teardown project ${dep.project.name}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -55,8 +55,10 @@ export class ConfigLoader {
|
|||||||
const fullConfig = await this._loadConfig(config, path.dirname(file), file);
|
const fullConfig = await this._loadConfig(config, path.dirname(file), file);
|
||||||
setCurrentConfig(fullConfig);
|
setCurrentConfig(fullConfig);
|
||||||
if (ignoreProjectDependencies) {
|
if (ignoreProjectDependencies) {
|
||||||
for (const project of fullConfig.projects)
|
for (const project of fullConfig.projects) {
|
||||||
project.deps = [];
|
project.deps = [];
|
||||||
|
project.teardown = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this._fullConfig = fullConfig;
|
this._fullConfig = fullConfig;
|
||||||
return fullConfig;
|
return fullConfig;
|
||||||
|
|||||||
@ -48,6 +48,7 @@ export type JsonProject = {
|
|||||||
repeatEach: number;
|
repeatEach: number;
|
||||||
retries: number;
|
retries: number;
|
||||||
suites: JsonSuite[];
|
suites: JsonSuite[];
|
||||||
|
teardown?: string;
|
||||||
testDir: string;
|
testDir: string;
|
||||||
testIgnore: JsonPattern[];
|
testIgnore: JsonPattern[];
|
||||||
testMatch: JsonPattern[];
|
testMatch: JsonPattern[];
|
||||||
@ -303,6 +304,7 @@ export class TeleReporterReceiver {
|
|||||||
grep: parseRegexPatterns(project.grep) as RegExp[],
|
grep: parseRegexPatterns(project.grep) as RegExp[],
|
||||||
grepInvert: parseRegexPatterns(project.grepInvert) as RegExp[],
|
grepInvert: parseRegexPatterns(project.grepInvert) as RegExp[],
|
||||||
dependencies: project.dependencies,
|
dependencies: project.dependencies,
|
||||||
|
teardown: project.teardown,
|
||||||
snapshotDir: this._absolutePath(project.snapshotDir),
|
snapshotDir: this._absolutePath(project.snapshotDir),
|
||||||
use: {},
|
use: {},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -151,6 +151,7 @@ export class TeleReporterEmitter implements Reporter {
|
|||||||
grepInvert: serializeRegexPatterns(project.grepInvert || []),
|
grepInvert: serializeRegexPatterns(project.grepInvert || []),
|
||||||
dependencies: project.dependencies,
|
dependencies: project.dependencies,
|
||||||
snapshotDir: this._relativePath(project.snapshotDir),
|
snapshotDir: this._relativePath(project.snapshotDir),
|
||||||
|
teardown: project.teardown,
|
||||||
};
|
};
|
||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,6 +49,15 @@ export function filterProjects(projects: FullProjectInternal[], projectNames?: s
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildTeardownToSetupMap(projects: FullProjectInternal[]): Map<FullProjectInternal, FullProjectInternal> {
|
||||||
|
const result = new Map<FullProjectInternal, FullProjectInternal>();
|
||||||
|
for (const project of projects) {
|
||||||
|
if (project.teardown)
|
||||||
|
result.set(project.teardown, project);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export function buildProjectsClosure(projects: FullProjectInternal[]): Map<FullProjectInternal, 'top-level' | 'dependency'> {
|
export function buildProjectsClosure(projects: FullProjectInternal[]): Map<FullProjectInternal, 'top-level' | 'dependency'> {
|
||||||
const result = new Map<FullProjectInternal, 'top-level' | 'dependency'>();
|
const result = new Map<FullProjectInternal, 'top-level' | 'dependency'>();
|
||||||
const visit = (depth: number, project: FullProjectInternal) => {
|
const visit = (depth: number, project: FullProjectInternal) => {
|
||||||
@ -59,6 +68,8 @@ export function buildProjectsClosure(projects: FullProjectInternal[]): Map<FullP
|
|||||||
}
|
}
|
||||||
result.set(project, depth ? 'dependency' : 'top-level');
|
result.set(project, depth ? 'dependency' : 'top-level');
|
||||||
project.deps.map(visit.bind(undefined, depth + 1));
|
project.deps.map(visit.bind(undefined, depth + 1));
|
||||||
|
if (project.teardown)
|
||||||
|
visit(depth + 1, project.teardown);
|
||||||
};
|
};
|
||||||
for (const p of projects)
|
for (const p of projects)
|
||||||
result.set(p, 'top-level');
|
result.set(p, 'top-level');
|
||||||
@ -67,6 +78,29 @@ export function buildProjectsClosure(projects: FullProjectInternal[]): Map<FullP
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildDependentProjects(forProject: FullProjectInternal, projects: FullProjectInternal[]): Set<FullProjectInternal> {
|
||||||
|
const reverseDeps = new Map<FullProjectInternal, FullProjectInternal[]>(projects.map(p => ([p, []])));
|
||||||
|
for (const project of projects) {
|
||||||
|
for (const dep of project.deps)
|
||||||
|
reverseDeps.get(dep)!.push(project);
|
||||||
|
}
|
||||||
|
const result = new Set<FullProjectInternal>();
|
||||||
|
const visit = (depth: number, project: FullProjectInternal) => {
|
||||||
|
if (depth > 100) {
|
||||||
|
const error = new Error('Circular dependency detected between projects.');
|
||||||
|
error.stack = '';
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
result.add(project);
|
||||||
|
for (const reverseDep of reverseDeps.get(project)!)
|
||||||
|
visit(depth + 1, reverseDep);
|
||||||
|
if (project.teardown)
|
||||||
|
visit(depth + 1, project.teardown);
|
||||||
|
};
|
||||||
|
visit(0, forProject);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export async function collectFilesForProject(project: FullProjectInternal, fsCache = new Map<string, string[]>()): Promise<string[]> {
|
export async function collectFilesForProject(project: FullProjectInternal, fsCache = new Map<string, string[]>()): Promise<string[]> {
|
||||||
const extensions = ['.js', '.ts', '.mjs', '.tsx', '.jsx'];
|
const extensions = ['.js', '.ts', '.mjs', '.tsx', '.jsx'];
|
||||||
const testFileExtension = (file: string) => extensions.includes(path.extname(file));
|
const testFileExtension = (file: string) => extensions.includes(path.extname(file));
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import type { FullConfigInternal, FullProjectInternal } from '../common/config';
|
|||||||
import { collectProjectsAndTestFiles, createRootSuite, loadFileSuites, loadGlobalHook } from './loadUtils';
|
import { collectProjectsAndTestFiles, createRootSuite, loadFileSuites, loadGlobalHook } from './loadUtils';
|
||||||
import type { Matcher } from '../util';
|
import type { Matcher } from '../util';
|
||||||
import type { Suite } from '../common/test';
|
import type { Suite } from '../common/test';
|
||||||
|
import { buildDependentProjects, buildTeardownToSetupMap } from './projectUtils';
|
||||||
|
|
||||||
const removeFolderAsync = promisify(rimraf);
|
const removeFolderAsync = promisify(rimraf);
|
||||||
const readDirAsync = promisify(fs.readdir);
|
const readDirAsync = promisify(fs.readdir);
|
||||||
@ -182,13 +183,23 @@ function createPhasesTask(): Task<TestRun> {
|
|||||||
|
|
||||||
const processed = new Set<FullProjectInternal>();
|
const processed = new Set<FullProjectInternal>();
|
||||||
const projectToSuite = new Map(testRun.rootSuite!.suites.map(suite => [suite._fullProject!, suite]));
|
const projectToSuite = new Map(testRun.rootSuite!.suites.map(suite => [suite._fullProject!, suite]));
|
||||||
|
const allProjects = [...projectToSuite.keys()];
|
||||||
|
const teardownToSetup = buildTeardownToSetupMap(allProjects);
|
||||||
|
const teardownToSetupDependents = new Map<FullProjectInternal, FullProjectInternal[]>();
|
||||||
|
for (const [teardown, setup] of teardownToSetup) {
|
||||||
|
const closure = buildDependentProjects(setup, allProjects);
|
||||||
|
closure.delete(teardown);
|
||||||
|
teardownToSetupDependents.set(teardown, [...closure]);
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < projectToSuite.size; i++) {
|
for (let i = 0; i < projectToSuite.size; i++) {
|
||||||
// Find all projects that have all their dependencies processed by previous phases.
|
// Find all projects that have all their dependencies processed by previous phases.
|
||||||
const phaseProjects: FullProjectInternal[] = [];
|
const phaseProjects: FullProjectInternal[] = [];
|
||||||
for (const project of projectToSuite.keys()) {
|
for (const project of projectToSuite.keys()) {
|
||||||
if (processed.has(project))
|
if (processed.has(project))
|
||||||
continue;
|
continue;
|
||||||
if (project.deps.find(p => !processed.has(p)))
|
const projectsThatShouldFinishFirst = [...project.deps, ...(teardownToSetupDependents.get(project) || [])];
|
||||||
|
if (projectsThatShouldFinishFirst.find(p => !processed.has(p)))
|
||||||
continue;
|
continue;
|
||||||
phaseProjects.push(project);
|
phaseProjects.push(project);
|
||||||
}
|
}
|
||||||
@ -229,6 +240,7 @@ function createRunTestsTask(): Task<TestRun> {
|
|||||||
const { phases } = testRun;
|
const { phases } = testRun;
|
||||||
const successfulProjects = new Set<FullProjectInternal>();
|
const successfulProjects = new Set<FullProjectInternal>();
|
||||||
const extraEnvByProjectId: EnvByProjectId = new Map();
|
const extraEnvByProjectId: EnvByProjectId = new Map();
|
||||||
|
const teardownToSetup = buildTeardownToSetupMap(phases.map(phase => phase.projects.map(p => p.project)).flat());
|
||||||
|
|
||||||
for (const { dispatcher, projects } of phases) {
|
for (const { dispatcher, projects } of phases) {
|
||||||
// Each phase contains dispatcher and a set of test groups.
|
// Each phase contains dispatcher and a set of test groups.
|
||||||
@ -240,6 +252,9 @@ function createRunTestsTask(): Task<TestRun> {
|
|||||||
let extraEnv: Record<string, string | undefined> = {};
|
let extraEnv: Record<string, string | undefined> = {};
|
||||||
for (const dep of project.deps)
|
for (const dep of project.deps)
|
||||||
extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(dep.id) };
|
extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(dep.id) };
|
||||||
|
const setupForTeardown = teardownToSetup.get(project);
|
||||||
|
if (setupForTeardown)
|
||||||
|
extraEnv = { ...extraEnv, ...extraEnvByProjectId.get(setupForTeardown.id) };
|
||||||
extraEnvByProjectId.set(project.id, extraEnv);
|
extraEnvByProjectId.set(project.id, extraEnv);
|
||||||
|
|
||||||
const hasFailedDeps = project.deps.some(p => !successfulProjects.has(p));
|
const hasFailedDeps = project.deps.some(p => !successfulProjects.has(p));
|
||||||
|
|||||||
@ -44,8 +44,10 @@ class UIMode {
|
|||||||
process.env.PW_LIVE_TRACE_STACKS = '1';
|
process.env.PW_LIVE_TRACE_STACKS = '1';
|
||||||
config.cliListOnly = false;
|
config.cliListOnly = false;
|
||||||
config.cliPassWithNoTests = true;
|
config.cliPassWithNoTests = true;
|
||||||
for (const project of config.projects)
|
for (const project of config.projects) {
|
||||||
project.deps = [];
|
project.deps = [];
|
||||||
|
project.teardown = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
for (const p of config.projects) {
|
for (const p of config.projects) {
|
||||||
p.project.retries = 0;
|
p.project.retries = 0;
|
||||||
|
|||||||
@ -308,6 +308,8 @@ function affectedProjectsClosure(projectClosure: FullProjectInternal[], affected
|
|||||||
if (result.has(dep))
|
if (result.has(dep))
|
||||||
result.add(p);
|
result.add(p);
|
||||||
}
|
}
|
||||||
|
if (p.teardown && result.has(p.teardown))
|
||||||
|
result.add(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
101
packages/playwright-test/types/test.d.ts
vendored
101
packages/playwright-test/types/test.d.ts
vendored
@ -193,7 +193,7 @@ export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
|
|||||||
* Using dependencies allows global setup to produce traces and other artifacts, see the setup steps in the test
|
* Using dependencies allows global setup to produce traces and other artifacts, see the setup steps in the test
|
||||||
* report, etc.
|
* report, etc.
|
||||||
*
|
*
|
||||||
* For example:
|
* **Usage**
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
* // playwright.config.ts
|
* // playwright.config.ts
|
||||||
@ -284,6 +284,54 @@ export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
|
|||||||
* option for all projects.
|
* option for all projects.
|
||||||
*/
|
*/
|
||||||
retries: number;
|
retries: number;
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* Passing `--no-deps` argument ignores
|
||||||
|
* [testProject.teardown](https://playwright.dev/docs/api/class-testproject#test-project-teardown) and behaves as if
|
||||||
|
* it was not specified.
|
||||||
|
*
|
||||||
|
* **Usage**
|
||||||
|
*
|
||||||
|
* A common pattern is a "setup" dependency that has a corresponding "teardown":
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* // playwright.config.ts
|
||||||
|
* import { defineConfig } from '@playwright/test';
|
||||||
|
*
|
||||||
|
* export default defineConfig({
|
||||||
|
* projects: [
|
||||||
|
* {
|
||||||
|
* name: 'setup',
|
||||||
|
* testMatch: /global.setup\.ts/,
|
||||||
|
* teardown: 'teardown',
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* name: 'teardown',
|
||||||
|
* testMatch: /global.teardown\.ts/,
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* name: 'chromium',
|
||||||
|
* use: devices['Desktop Chrome'],
|
||||||
|
* dependencies: ['setup'],
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* name: 'firefox',
|
||||||
|
* use: devices['Desktop Firefox'],
|
||||||
|
* dependencies: ['setup'],
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* name: 'webkit',
|
||||||
|
* use: devices['Desktop Safari'],
|
||||||
|
* dependencies: ['setup'],
|
||||||
|
* },
|
||||||
|
* ],
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
teardown?: string;
|
||||||
/**
|
/**
|
||||||
* Directory that will be recursively scanned for test files. Defaults to the directory of the configuration file.
|
* Directory that will be recursively scanned for test files. Defaults to the directory of the configuration file.
|
||||||
*
|
*
|
||||||
@ -5932,7 +5980,7 @@ interface TestProject {
|
|||||||
* Using dependencies allows global setup to produce traces and other artifacts, see the setup steps in the test
|
* Using dependencies allows global setup to produce traces and other artifacts, see the setup steps in the test
|
||||||
* report, etc.
|
* report, etc.
|
||||||
*
|
*
|
||||||
* For example:
|
* **Usage**
|
||||||
*
|
*
|
||||||
* ```js
|
* ```js
|
||||||
* // playwright.config.ts
|
* // playwright.config.ts
|
||||||
@ -6245,6 +6293,55 @@ interface TestProject {
|
|||||||
*/
|
*/
|
||||||
snapshotPathTemplate?: string;
|
snapshotPathTemplate?: 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.
|
||||||
|
*
|
||||||
|
* Passing `--no-deps` argument ignores
|
||||||
|
* [testProject.teardown](https://playwright.dev/docs/api/class-testproject#test-project-teardown) and behaves as if
|
||||||
|
* it was not specified.
|
||||||
|
*
|
||||||
|
* **Usage**
|
||||||
|
*
|
||||||
|
* A common pattern is a "setup" dependency that has a corresponding "teardown":
|
||||||
|
*
|
||||||
|
* ```js
|
||||||
|
* // playwright.config.ts
|
||||||
|
* import { defineConfig } from '@playwright/test';
|
||||||
|
*
|
||||||
|
* export default defineConfig({
|
||||||
|
* projects: [
|
||||||
|
* {
|
||||||
|
* name: 'setup',
|
||||||
|
* testMatch: /global.setup\.ts/,
|
||||||
|
* teardown: 'teardown',
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* name: 'teardown',
|
||||||
|
* testMatch: /global.teardown\.ts/,
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* name: 'chromium',
|
||||||
|
* use: devices['Desktop Chrome'],
|
||||||
|
* dependencies: ['setup'],
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* name: 'firefox',
|
||||||
|
* use: devices['Desktop Firefox'],
|
||||||
|
* dependencies: ['setup'],
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* name: 'webkit',
|
||||||
|
* use: devices['Desktop Safari'],
|
||||||
|
* dependencies: ['setup'],
|
||||||
|
* },
|
||||||
|
* ],
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
teardown?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directory that will be recursively scanned for test files. Defaults to the directory of the configuration file.
|
* Directory that will be recursively scanned for test files. Defaults to the directory of the configuration file.
|
||||||
*
|
*
|
||||||
|
|||||||
@ -43,9 +43,10 @@ test('should inherit env changes from dependencies', async ({ runInlineTest }) =
|
|||||||
'playwright.config.ts': `
|
'playwright.config.ts': `
|
||||||
module.exports = { projects: [
|
module.exports = { projects: [
|
||||||
{ name: 'A', testMatch: '**/a.spec.ts' },
|
{ name: 'A', testMatch: '**/a.spec.ts' },
|
||||||
{ name: 'B', testMatch: '**/b.spec.ts' },
|
{ name: 'B', testMatch: '**/b.spec.ts', teardown: 'E' },
|
||||||
{ name: 'C', testMatch: '**/c.spec.ts', dependencies: ['A'] },
|
{ name: 'C', testMatch: '**/c.spec.ts', dependencies: ['A'] },
|
||||||
{ name: 'D', testMatch: '**/d.spec.ts', dependencies: ['B'] },
|
{ name: 'D', testMatch: '**/d.spec.ts', dependencies: ['B'] },
|
||||||
|
{ name: 'E', testMatch: '**/e.spec.ts' },
|
||||||
] };
|
] };
|
||||||
`,
|
`,
|
||||||
'a.spec.ts': `
|
'a.spec.ts': `
|
||||||
@ -75,11 +76,17 @@ test('should inherit env changes from dependencies', async ({ runInlineTest }) =
|
|||||||
console.log('\\n%%D-' + process.env.SET_IN_A + '-' + process.env.SET_IN_B + '-' + process.env.SET_OUTSIDE);
|
console.log('\\n%%D-' + process.env.SET_IN_A + '-' + process.env.SET_IN_B + '-' + process.env.SET_OUTSIDE);
|
||||||
});
|
});
|
||||||
`,
|
`,
|
||||||
|
'e.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('pass', async ({}, testInfo) => {
|
||||||
|
console.log('\\n%%E-' + process.env.SET_IN_A + '-' + process.env.SET_IN_B + '-' + process.env.SET_OUTSIDE);
|
||||||
|
});
|
||||||
|
`,
|
||||||
}, {}, { SET_OUTSIDE: 'outside' });
|
}, {}, { SET_OUTSIDE: 'outside' });
|
||||||
expect(result.passed).toBe(4);
|
expect(result.passed).toBe(5);
|
||||||
expect(result.failed).toBe(0);
|
expect(result.failed).toBe(0);
|
||||||
expect(result.skipped).toBe(0);
|
expect(result.skipped).toBe(0);
|
||||||
expect(result.outputLines.sort()).toEqual(['A', 'B', 'C-valuea-undefined-undefined', 'D-undefined-valueb-outside']);
|
expect(result.outputLines.sort()).toEqual(['A', 'B', 'C-valuea-undefined-undefined', 'D-undefined-valueb-outside', 'E-undefined-valueb-outside']);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not run projects with dependencies when --no-deps is passed', async ({ runInlineTest }) => {
|
test('should not run projects with dependencies when --no-deps is passed', async ({ runInlineTest }) => {
|
||||||
@ -423,3 +430,132 @@ test('should run setup project with zero tests recursively', async ({ runInlineT
|
|||||||
expect(result.passed).toBe(2);
|
expect(result.passed).toBe(2);
|
||||||
expect(result.outputLines).toEqual(['A', 'C']);
|
expect(result.outputLines).toEqual(['A', 'C']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should run project with teardown', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = {
|
||||||
|
projects: [
|
||||||
|
{ name: 'A', teardown: 'B' },
|
||||||
|
{ name: 'B' },
|
||||||
|
],
|
||||||
|
};`,
|
||||||
|
'test.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({}, testInfo) => {
|
||||||
|
console.log('\\n%%' + testInfo.project.name);
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { workers: 1 }, undefined, { additionalArgs: ['--project=A'] });
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(2);
|
||||||
|
expect(result.outputLines).toEqual(['A', 'B']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should run teardown after depedents', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = {
|
||||||
|
projects: [
|
||||||
|
{ name: 'A', teardown: 'E' },
|
||||||
|
{ name: 'B', dependencies: ['A'] },
|
||||||
|
{ name: 'C', dependencies: ['B'], teardown: 'D' },
|
||||||
|
{ name: 'D' },
|
||||||
|
{ name: 'E' },
|
||||||
|
],
|
||||||
|
};`,
|
||||||
|
'test.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({}, testInfo) => {
|
||||||
|
console.log('\\n%%' + testInfo.project.name);
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { workers: 1 }, undefined, { additionalArgs: ['--project=C'] });
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
expect(result.passed).toBe(5);
|
||||||
|
expect(result.outputLines).toEqual(['A', 'B', 'C', 'D', 'E']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should run teardown after failure', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = {
|
||||||
|
projects: [
|
||||||
|
{ name: 'A', teardown: 'D' },
|
||||||
|
{ name: 'B', dependencies: ['A'] },
|
||||||
|
{ name: 'C', dependencies: ['B'] },
|
||||||
|
{ name: 'D' },
|
||||||
|
],
|
||||||
|
};`,
|
||||||
|
'test.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', async ({}, testInfo) => {
|
||||||
|
console.log('\\n%%' + testInfo.project.name);
|
||||||
|
if (testInfo.project.name === 'A')
|
||||||
|
throw new Error('ouch');
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { workers: 1 }, undefined, { additionalArgs: ['--project=C'] });
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
expect(result.skipped).toBe(2);
|
||||||
|
expect(result.outputLines).toEqual(['A', 'D']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should complain about teardown being a dependency', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = {
|
||||||
|
projects: [
|
||||||
|
{ name: 'A', teardown: 'B' },
|
||||||
|
{ name: 'B' },
|
||||||
|
{ name: 'C', dependencies: ['B'] },
|
||||||
|
],
|
||||||
|
};`,
|
||||||
|
'test.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', () => {});
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.output).toContain(`Project C must not depend on a teardown project B`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should complain about teardown having a dependency', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = {
|
||||||
|
projects: [
|
||||||
|
{ name: 'A', teardown: 'B' },
|
||||||
|
{ name: 'B', dependencies: ['C'] },
|
||||||
|
{ name: 'C' },
|
||||||
|
],
|
||||||
|
};`,
|
||||||
|
'test.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', () => {});
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.output).toContain(`Teardown project B must not have dependencies`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should complain about 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' },
|
||||||
|
],
|
||||||
|
};`,
|
||||||
|
'test.spec.ts': `
|
||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
test('test', () => {});
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.output).toContain(`Project C can not be designated as teardown to multiple projects (A and B)`);
|
||||||
|
});
|
||||||
|
|||||||
3
utils/generate_types/overrides-test.d.ts
vendored
3
utils/generate_types/overrides-test.d.ts
vendored
@ -36,7 +36,7 @@ export interface Project<TestArgs = {}, WorkerArgs = {}> extends TestProject {
|
|||||||
|
|
||||||
// [internal] !!! DO NOT ADD TO THIS !!!
|
// [internal] !!! DO NOT ADD TO THIS !!!
|
||||||
// [internal] It is part of the public API and is computed from the user's config.
|
// [internal] It is part of the public API and is computed from the user's config.
|
||||||
// [internal] If you need new fields internally, add them to FullConfigInternal instead.
|
// [internal] If you need new fields internally, add them to FullProjectInternal instead.
|
||||||
export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
|
export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
|
||||||
grep: RegExp | RegExp[];
|
grep: RegExp | RegExp[];
|
||||||
grepInvert: RegExp | RegExp[] | null;
|
grepInvert: RegExp | RegExp[] | null;
|
||||||
@ -47,6 +47,7 @@ export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
|
|||||||
outputDir: string;
|
outputDir: string;
|
||||||
repeatEach: number;
|
repeatEach: number;
|
||||||
retries: number;
|
retries: number;
|
||||||
|
teardown?: string;
|
||||||
testDir: string;
|
testDir: string;
|
||||||
testIgnore: string | RegExp | (string | RegExp)[];
|
testIgnore: string | RegExp | (string | RegExp)[];
|
||||||
testMatch: string | RegExp | (string | RegExp)[];
|
testMatch: string | RegExp | (string | RegExp)[];
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user