feat(config): failOnFlakyTests option

This patch adds a configuration option to make the test runner
exit with a non-zero status when any test is flaky. The config
option does not take precedence over the CLI flag.

Fixes #34397
This commit is contained in:
Jon Hermansen 2025-02-11 21:43:11 -05:00
parent bd74fc4964
commit 39a2e9cf3e
8 changed files with 143 additions and 1 deletions

View file

@ -10,6 +10,12 @@ Resolved configuration which is accessible via [`property: TestInfo.config`] and
Path to the configuration file used to run the tests. The value is an empty string if no config file was used. Path to the configuration file used to run the tests. The value is an empty string if no config file was used.
## property: FullConfig.failOnFlakyTests
* since: v1.51
- type: <[boolean]>
See [`property: TestConfig.failOnFlakyTests`].
## property: FullConfig.forbidOnly ## property: FullConfig.forbidOnly
* since: v1.10 * since: v1.10
- type: <[boolean]> - type: <[boolean]>

View file

@ -76,6 +76,24 @@ export default defineConfig({
}); });
``` ```
## property: TestConfig.failOnFlakyTests
* since: v1.51
- type: ?<[boolean]>
Whether to exit with an error if any tests are marked as flaky. Useful on CI.
Also available in the [command line](../test-cli.md) with the `--fail-on-flaky-tests` option.
**Usage**
```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test';
export default defineConfig({
failOnFlakyTests: process.env.CI ? true : false,
});
```
## property: TestConfig.forbidOnly ## property: TestConfig.forbidOnly
* since: v1.10 * since: v1.10
- type: ?<[boolean]> - type: ?<[boolean]>

View file

@ -90,6 +90,7 @@ export class FullConfigInternal {
this.config = { this.config = {
configFile: resolvedConfigFile, configFile: resolvedConfigFile,
rootDir: pathResolve(configDir, userConfig.testDir) || configDir, rootDir: pathResolve(configDir, userConfig.testDir) || configDir,
failOnFlakyTests: takeFirst(configCLIOverrides.failOnFlakyTests, userConfig.failOnFlakyTests, false),
forbidOnly: takeFirst(configCLIOverrides.forbidOnly, userConfig.forbidOnly, false), forbidOnly: takeFirst(configCLIOverrides.forbidOnly, userConfig.forbidOnly, false),
fullyParallel: takeFirst(configCLIOverrides.fullyParallel, userConfig.fullyParallel, false), fullyParallel: takeFirst(configCLIOverrides.fullyParallel, userConfig.fullyParallel, false),
globalSetup: this.globalSetups[0] ?? null, globalSetup: this.globalSetups[0] ?? null,

View file

@ -25,6 +25,7 @@ import type { SerializedCompilationCache } from '../transform/compilationCache'
export type ConfigCLIOverrides = { export type ConfigCLIOverrides = {
debug?: boolean; debug?: boolean;
failOnFlakyTests?: boolean;
forbidOnly?: boolean; forbidOnly?: boolean;
fullyParallel?: boolean; fullyParallel?: boolean;
globalTimeout?: number; globalTimeout?: number;

View file

@ -592,6 +592,7 @@ export class TeleTestResult implements reporterTypes.TestResult {
export type TeleFullProject = reporterTypes.FullProject; export type TeleFullProject = reporterTypes.FullProject;
export const baseFullConfig: reporterTypes.FullConfig = { export const baseFullConfig: reporterTypes.FullConfig = {
failOnFlakyTests: false,
forbidOnly: false, forbidOnly: false,
fullyParallel: false, fullyParallel: false,
globalSetup: null, globalSetup: null,

View file

@ -49,7 +49,7 @@ export class FailureTracker {
} }
result(): 'failed' | 'passed' { result(): 'failed' | 'passed' {
return this._hasWorkerErrors || this.hasReachedMaxFailures() || this.hasFailedTests() || (this._config.cliFailOnFlakyTests && this.hasFlakyTests()) ? 'failed' : 'passed'; return this._hasWorkerErrors || this.hasReachedMaxFailures() || this.hasFailedTests() || (this.failOnFlakyTests() && this.hasFlakyTests()) ? 'failed' : 'passed';
} }
hasFailedTests() { hasFailedTests() {
@ -63,4 +63,8 @@ export class FailureTracker {
maxFailures() { maxFailures() {
return this._config.config.maxFailures; return this._config.config.maxFailures;
} }
failOnFlakyTests() {
return this._config.config.failOnFlakyTests || this._config.cliFailOnFlakyTests;
}
} }

View file

@ -1096,6 +1096,25 @@ interface TestConfig<TestArgs = {}, WorkerArgs = {}> {
}; };
}; };
/**
* Whether to exit with an error if any tests are marked as flaky. Useful on CI.
*
* Also available in the [command line](https://playwright.dev/docs/test-cli) with the `--fail-on-flaky-tests` option.
*
* **Usage**
*
* ```js
* // playwright.config.ts
* import { defineConfig } from '@playwright/test';
*
* export default defineConfig({
* failOnFlakyTests: process.env.CI ? true : false,
* });
* ```
*
*/
failOnFlakyTests?: boolean;
/** /**
* Whether to exit with an error if any tests or groups are marked as * Whether to exit with an error if any tests or groups are marked as
* [test.only(title[, details, body])](https://playwright.dev/docs/api/class-test#test-only) or * [test.only(title[, details, body])](https://playwright.dev/docs/api/class-test#test-only) or
@ -1855,6 +1874,12 @@ export interface FullConfig<TestArgs = {}, WorkerArgs = {}> {
*/ */
configFile?: string; configFile?: string;
/**
* See
* [testConfig.failOnFlakyTests](https://playwright.dev/docs/api/class-testconfig#test-config-fail-on-flaky-tests).
*/
failOnFlakyTests: boolean;
/** /**
* See [testConfig.forbidOnly](https://playwright.dev/docs/api/class-testconfig#test-config-forbid-only). * See [testConfig.forbidOnly](https://playwright.dev/docs/api/class-testconfig#test-config-forbid-only).
*/ */

View file

@ -72,6 +72,92 @@ test('should prioritize command line timeout over project timeout', async ({ run
expect(result.output).toContain('Test timeout of 500ms exceeded.'); expect(result.output).toContain('Test timeout of 500ms exceeded.');
}); });
test('should default to failOnFlakyTests false', async ({ runInlineTest }) => {
const result = await runInlineTest({
'a.test.js': `
import { test, expect } from '@playwright/test';
test('flake', async ({}, testInfo) => {
expect(testInfo.retry).toBe(1);
});
`,
});
expect(result.exitCode).not.toBe(0);
expect(result.flaky).toBe(0);
});
test('should prioritize command line --fail-on-flaky-tests flag over config failOnFlakyTests', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
failOnFlakyTests: false
};
`,
'a.test.js': `
import { test, expect } from '@playwright/test';
test('flake', async ({}, testInfo) => {
expect(testInfo.retry).toBe(1);
});
`,
}, { 'retries': 1, 'fail-on-flaky-tests': true });
expect(result.exitCode).not.toBe(0);
expect(result.flaky).toBe(1);
});
test('should support failOnFlakyTests config option', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
failOnFlakyTests: true,
retries: 1
};
`,
'a.test.js': `
import { test, expect } from '@playwright/test';
test('flake', async ({}, testInfo) => {
expect(testInfo.retry).toBe(1);
});
`,
}, { 'retries': 1 });
expect(result.exitCode).not.toBe(0);
expect(result.flaky).toBe(1);
});
test('should support failOnFlakyTests config option + retries CLI flag', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
failOnFlakyTests: true,
};
`,
'a.test.js': `
import { test, expect } from '@playwright/test';
test('flake', async ({}, testInfo) => {
expect(testInfo.retry).toBe(1);
});
`,
}, { 'retries': 1 });
expect(result.exitCode).not.toBe(0);
expect(result.flaky).toBe(1);
});
test('should support fail-on-flaky-tests CLI flag + retries config option', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
module.exports = {
retries: 1,
};
`,
'a.test.js': `
import { test, expect } from '@playwright/test';
test('flake', async ({}, testInfo) => {
expect(testInfo.retry).toBe(1);
});
`,
}, { 'fail-on-flaky-tests': true });
expect(result.exitCode).not.toBe(0);
expect(result.flaky).toBe(1);
});
test('should read config from --config, resolve relative testDir', async ({ runInlineTest }) => { test('should read config from --config, resolve relative testDir', async ({ runInlineTest }) => {
const result = await runInlineTest({ const result = await runInlineTest({
'my.config.ts': ` 'my.config.ts': `