feat(runner): project.stopOnFailure (#18009)
This commit is contained in:
parent
d5c4291a89
commit
3b8f63d703
|
|
@ -267,6 +267,13 @@ An integer number that defines when the project should run relative to other pro
|
||||||
one stage. By default all projects run in stage 0. Stages with lower number run first. Several projects can run in
|
one stage. By default all projects run in stage 0. Stages with lower number run first. Several projects can run in
|
||||||
each stage. Exeution order between projecs in the same stage is undefined.
|
each stage. Exeution order between projecs in the same stage is undefined.
|
||||||
|
|
||||||
|
## property: TestProject.stopOnFailure
|
||||||
|
* since: v1.28
|
||||||
|
- type: ?<[boolean]>
|
||||||
|
|
||||||
|
If set to true and the any test in the project fails all subsequent projects in the same playwright test run will
|
||||||
|
be skipped.
|
||||||
|
|
||||||
## property: TestProject.testDir
|
## property: TestProject.testDir
|
||||||
* since: v1.10
|
* since: v1.10
|
||||||
- type: ?<[string]>
|
- type: ?<[string]>
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export type TestGroup = {
|
||||||
requireFile: string;
|
requireFile: string;
|
||||||
repeatEachIndex: number;
|
repeatEachIndex: number;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
stopOnFailure: boolean;
|
||||||
tests: TestCase[];
|
tests: TestCase[];
|
||||||
watchMode: boolean;
|
watchMode: boolean;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -278,6 +278,7 @@ export class Loader {
|
||||||
const snapshotDir = takeFirst(projectConfig.snapshotDir, config.snapshotDir, testDir);
|
const snapshotDir = takeFirst(projectConfig.snapshotDir, config.snapshotDir, testDir);
|
||||||
const name = takeFirst(projectConfig.name, config.name, '');
|
const name = takeFirst(projectConfig.name, config.name, '');
|
||||||
const stage = takeFirst(projectConfig.stage, 0);
|
const stage = takeFirst(projectConfig.stage, 0);
|
||||||
|
const stopOnFailure = takeFirst(projectConfig.stopOnFailure, false);
|
||||||
|
|
||||||
let screenshotsDir = takeFirst((projectConfig as any).screenshotsDir, (config as any).screenshotsDir, path.join(testDir, '__screenshots__', process.platform, name));
|
let screenshotsDir = takeFirst((projectConfig as any).screenshotsDir, (config as any).screenshotsDir, path.join(testDir, '__screenshots__', process.platform, name));
|
||||||
if (process.env.PLAYWRIGHT_DOCKER) {
|
if (process.env.PLAYWRIGHT_DOCKER) {
|
||||||
|
|
@ -298,6 +299,7 @@ export class Loader {
|
||||||
name,
|
name,
|
||||||
testDir,
|
testDir,
|
||||||
stage,
|
stage,
|
||||||
|
stopOnFailure,
|
||||||
_respectGitIgnore: respectGitIgnore,
|
_respectGitIgnore: respectGitIgnore,
|
||||||
snapshotDir,
|
snapshotDir,
|
||||||
_screenshotsDir: screenshotsDir,
|
_screenshotsDir: screenshotsDir,
|
||||||
|
|
@ -609,6 +611,16 @@ function validateProject(file: string, project: Project, title: string) {
|
||||||
throw errorWithFile(file, `${title}.retries must be a non-negative number`);
|
throw errorWithFile(file, `${title}.retries must be a non-negative number`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ('stage' in project && project.stage !== undefined) {
|
||||||
|
if (typeof project.stage !== 'number' || Math.floor(project.stage) !== project.stage)
|
||||||
|
throw errorWithFile(file, `${title}.stage must be an integer`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('stopOnFailure' in project && project.stopOnFailure !== undefined) {
|
||||||
|
if (typeof project.stopOnFailure !== 'boolean')
|
||||||
|
throw errorWithFile(file, `${title}.stopOnFailure must be a boolean`);
|
||||||
|
}
|
||||||
|
|
||||||
if ('testDir' in project && project.testDir !== undefined) {
|
if ('testDir' in project && project.testDir !== undefined) {
|
||||||
if (typeof project.testDir !== 'string')
|
if (typeof project.testDir !== 'string')
|
||||||
throw errorWithFile(file, `${title}.testDir must be a string`);
|
throw errorWithFile(file, `${title}.testDir must be a string`);
|
||||||
|
|
|
||||||
|
|
@ -426,7 +426,7 @@ export class Runner {
|
||||||
|
|
||||||
let hasWorkerErrors = false;
|
let hasWorkerErrors = false;
|
||||||
for (const testGroups of concurrentTestGroups) {
|
for (const testGroups of concurrentTestGroups) {
|
||||||
const dispatcher = new Dispatcher(this._loader, testGroups, this._reporter);
|
const dispatcher = new Dispatcher(this._loader, [...testGroups], this._reporter);
|
||||||
sigintWatcher = new SigIntWatcher();
|
sigintWatcher = new SigIntWatcher();
|
||||||
await Promise.race([dispatcher.run(), sigintWatcher.promise()]);
|
await Promise.race([dispatcher.run(), sigintWatcher.promise()]);
|
||||||
if (!sigintWatcher.hadSignal()) {
|
if (!sigintWatcher.hadSignal()) {
|
||||||
|
|
@ -438,7 +438,8 @@ export class Runner {
|
||||||
hasWorkerErrors = dispatcher.hasWorkerErrors();
|
hasWorkerErrors = dispatcher.hasWorkerErrors();
|
||||||
if (hasWorkerErrors)
|
if (hasWorkerErrors)
|
||||||
break;
|
break;
|
||||||
if (testGroups.some(testGroup => testGroup.tests.some(test => !test.ok())))
|
const stopOnFailureGroups = testGroups.filter(group => group.stopOnFailure);
|
||||||
|
if (stopOnFailureGroups.some(testGroup => testGroup.tests.some(test => !test.ok())))
|
||||||
break;
|
break;
|
||||||
if (sigintWatcher.hadSignal())
|
if (sigintWatcher.hadSignal())
|
||||||
break;
|
break;
|
||||||
|
|
@ -747,6 +748,7 @@ function createTestGroups(projectSuites: Suite[], workers: number): TestGroup[]
|
||||||
requireFile: test._requireFile,
|
requireFile: test._requireFile,
|
||||||
repeatEachIndex: test.repeatEachIndex,
|
repeatEachIndex: test.repeatEachIndex,
|
||||||
projectId: test._projectId,
|
projectId: test._projectId,
|
||||||
|
stopOnFailure: test.parent.project()!.stopOnFailure,
|
||||||
tests: [],
|
tests: [],
|
||||||
watchMode: false,
|
watchMode: false,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
11
packages/playwright-test/types/test.d.ts
vendored
11
packages/playwright-test/types/test.d.ts
vendored
|
|
@ -262,6 +262,11 @@ export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
|
||||||
* stage. Exeution order between projecs in the same stage is undefined.
|
* stage. Exeution order between projecs in the same stage is undefined.
|
||||||
*/
|
*/
|
||||||
stage: number;
|
stage: number;
|
||||||
|
/**
|
||||||
|
* If set to true and the any test in the project fails all subsequent projects in the same playwright test run will be
|
||||||
|
* skipped.
|
||||||
|
*/
|
||||||
|
stopOnFailure: boolean;
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
*
|
*
|
||||||
|
|
@ -4471,6 +4476,12 @@ interface TestProject {
|
||||||
*/
|
*/
|
||||||
stage?: number;
|
stage?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If set to true and the any test in the project fails all subsequent projects in the same playwright test run will be
|
||||||
|
* skipped.
|
||||||
|
*/
|
||||||
|
stopOnFailure?: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -480,3 +480,60 @@ test('should have correct types for the config', async ({ runTSC }) => {
|
||||||
});
|
});
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should throw when project.stage is not a number', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = {
|
||||||
|
projects: [
|
||||||
|
{ name: 'a', stage: 'foo' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'a.test.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('pass', async () => {});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.output).toContain(`config.projects[0].stage must be an integer`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw when project.stage is not an integer', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = {
|
||||||
|
projects: [
|
||||||
|
{ name: 'a', stage: 3.14 },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'a.test.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('pass', async () => {});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.output).toContain(`config.projects[0].stage must be an integer`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should throw when project.stopOnFailure is not a boolean', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'playwright.config.ts': `
|
||||||
|
module.exports = {
|
||||||
|
projects: [
|
||||||
|
{ name: 'a', stopOnFailure: 'yes' },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
`,
|
||||||
|
'a.test.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('pass', async () => {});
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.output).toContain(`config.projects[0].stopOnFailure must be a boolean`);
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -207,3 +207,81 @@ test('should work with project filter', async ({ runGroups }, testInfo) => {
|
||||||
expectRunBefore(timeline, ['c'], ['b']); // 0 < 10
|
expectRunBefore(timeline, ['c'], ['b']); // 0 < 10
|
||||||
expect(passed).toBe(3);
|
expect(passed).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should continue after failures', async ({ runGroups }, testInfo) => {
|
||||||
|
const projectTemplates = {
|
||||||
|
'a': {
|
||||||
|
stage: 1
|
||||||
|
},
|
||||||
|
'b': {
|
||||||
|
stage: 2
|
||||||
|
},
|
||||||
|
'c': {
|
||||||
|
stage: 2
|
||||||
|
},
|
||||||
|
'd': {
|
||||||
|
stage: 4
|
||||||
|
},
|
||||||
|
'e': {
|
||||||
|
stage: 4
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const configWithFiles = createConfigWithProjects(['a', 'b', 'c', 'd', 'e'], testInfo, projectTemplates);
|
||||||
|
configWithFiles[`b/b.spec.ts`] = `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('b test', async () => {
|
||||||
|
expect(1).toBe(2);
|
||||||
|
});`;
|
||||||
|
configWithFiles[`d/d.spec.ts`] = `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('d test', async () => {
|
||||||
|
expect(1).toBe(2);
|
||||||
|
});`;
|
||||||
|
const { exitCode, passed, failed, timeline } = await runGroups(configWithFiles);
|
||||||
|
expect(exitCode).toBe(1);
|
||||||
|
expect(failed).toBe(2);
|
||||||
|
expect(passed).toBe(3);
|
||||||
|
expect(projectNames(timeline)).toEqual(['a', 'b', 'c', 'd', 'e']);
|
||||||
|
expectRunBefore(timeline, ['a'], ['b', 'c', 'd', 'e']); // 1 < 2
|
||||||
|
expectRunBefore(timeline, ['b', 'c'], ['d', 'e']); // 2 < 4
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should support stopOnFailire', async ({ runGroups }, testInfo) => {
|
||||||
|
const projectTemplates = {
|
||||||
|
'a': {
|
||||||
|
stage: 1
|
||||||
|
},
|
||||||
|
'b': {
|
||||||
|
stage: 2,
|
||||||
|
stopOnFailure: true
|
||||||
|
},
|
||||||
|
'c': {
|
||||||
|
stage: 2
|
||||||
|
},
|
||||||
|
'd': {
|
||||||
|
stage: 4,
|
||||||
|
stopOnFailure: true // this is not important as the test is skipped
|
||||||
|
},
|
||||||
|
'e': {
|
||||||
|
stage: 4
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const configWithFiles = createConfigWithProjects(['a', 'b', 'c', 'd', 'e'], testInfo, projectTemplates);
|
||||||
|
configWithFiles[`b/b.spec.ts`] = `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('b test', async () => {
|
||||||
|
expect(1).toBe(2);
|
||||||
|
});`;
|
||||||
|
configWithFiles[`d/d.spec.ts`] = `
|
||||||
|
const { test } = pwt;
|
||||||
|
test('d test', async () => {
|
||||||
|
expect(1).toBe(2);
|
||||||
|
});`;
|
||||||
|
const { exitCode, passed, failed, skipped, timeline } = await runGroups(configWithFiles);
|
||||||
|
expect(exitCode).toBe(1);
|
||||||
|
expect(failed).toBe(1);
|
||||||
|
expect(passed).toBeLessThanOrEqual(2); // 'c' may either pass or be skipped.
|
||||||
|
expect(passed + skipped).toBe(4);
|
||||||
|
expect(projectNames(timeline)).not.toContainEqual(['d', 'e']);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
|
||||||
1
utils/generate_types/overrides-test.d.ts
vendored
1
utils/generate_types/overrides-test.d.ts
vendored
|
|
@ -47,6 +47,7 @@ export interface FullProject<TestArgs = {}, WorkerArgs = {}> {
|
||||||
repeatEach: number;
|
repeatEach: number;
|
||||||
retries: number;
|
retries: number;
|
||||||
stage: number;
|
stage: number;
|
||||||
|
stopOnFailure: boolean;
|
||||||
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…
Reference in a new issue