Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/src/test-cli-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ npx playwright test --ui
| `--quiet` | Suppress stdio. |
| `--repeat-each <N>` | Run each test `N` times (default: 1). |
| `--reporter <reporter>` | Reporter to use, comma-separated, can be "dot", "line", "list", or others (default: "list" locally and "dot" on CI). You can also pass a path to a custom reporter file. |
| `--reporter-only-failures` | Report only failed, flaky, and interrupted tests. Terminal summaries retain the full run counts. HTML, JSON, JUnit, and Perfetto reports contain only the included results; blob reports remain complete. Custom reporters receive the `onlyFailures` constructor option. `--list` output is unchanged. |
| `--retries <retries>` | Maximum retry count for flaky tests, zero for no retries (default: no retries). |
| `--shard <shard>` | Shard tests and execute only the selected shard, specified in the form "current/all", 1-based, e.g., "3/5". |
| `--test-list <file>` | Path to a file containing a list of tests to run. See [test list](#test-list) for details. |
Expand Down Expand Up @@ -318,6 +319,7 @@ npx playwright merge-reports ./reports
| :--- | :--- |
| `-c, --config <file>` | Configuration file. Can be used to specify additional configuration for the output report |
| `--reporter <reporter>` | Reporter to use, comma-separated, can be "list", "line", "dot", "json", "junit", "null", "github", "html", "blob" (default: "list" locally and "dot" on CI) |
| `--reporter-only-failures` | Report only failed, flaky, and interrupted tests in terminal, HTML, JSON, JUnit, and Perfetto output. Blob reports remain complete. Custom reporters receive the `onlyFailures` constructor option. |

### Clear Cache

Expand Down
14 changes: 12 additions & 2 deletions docs/src/test-reporter-api/class-reporter.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ module.exports = MyReporter;

```js tab=js-ts title="my-awesome-reporter.ts"
import type {
Reporter, FullConfig, Suite, TestCase, TestResult, FullResult
Reporter, ReporterOptions, FullConfig, Suite, TestCase, TestResult, FullResult
} from '@playwright/test/reporter';

class MyReporter implements Reporter {
constructor(options: { customOption?: string } = {}) {
constructor(options: ReporterOptions & { customOption?: string } = {}) {
console.log(`my-awesome-reporter setup with customOption set to ${options.customOption}`);
}

Expand Down Expand Up @@ -74,6 +74,16 @@ export default defineConfig({
});
```

**Reporter options**

The reporter constructor receives its configured options together with common options described by the `ReporterOptions` type:

| Option | Description |
|---|---|
| `onlyFailures` | Set to `true` by `--reporter-only-failures`, overriding a configured value of `false`. Custom reporters can use it to limit their output to failed, flaky, and interrupted tests. |

This option does not filter or delay reporter callbacks. Reporters still receive the complete suite, every test result, and all timing information.

Here is a typical order of reporter calls:
* [`method: Reporter.onBegin`] is called once with a root suite that contains all other suites and tests. Learn more about [suites hierarchy][Suite].
* [`method: Reporter.onTestBegin`] is called for each test run. It is given a [TestCase] that is executed, and a [TestResult] that is almost empty. Test result will be populated while the test runs (for example, with steps and stdio) and will get final `status` once the test finishes.
Expand Down
54 changes: 54 additions & 0 deletions docs/src/test-reporters-js.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ export default defineConfig({
});
```

### Report only failures

Use `--reporter-only-failures` to report only failed, flaky, and interrupted tests:

```bash
npx playwright test --reporter-only-failures
npx playwright merge-reports --reporter=html --reporter-only-failures ./blob-report
```

Terminal reporters hide progress output and retain the full run summary and timing information. Generated HTML, JSON, JUnit, and Perfetto reports omit passing tests, tests with expected failures, and skipped tests. Their test counts describe the included results.

Blob reports always retain every test, result, and attachment so they can be replayed into either complete or filtered reports. Apply `--reporter-only-failures` when merging to filter the generated reports without changing the blob data.

The flag overrides terminal reporter options and environment variables. Custom reporters receive `onlyFailures: true` in their constructor options and can choose how to respect it; their callbacks still receive all tests and results. `--list` output is unchanged. Test stdout and stderr remain controlled by [`property: TestConfig.quiet`].

## Built-in reporters

All built-in reporters show detailed information about failures, and mostly differ in verbosity for successful runs.
Expand Down Expand Up @@ -107,6 +122,18 @@ export default defineConfig({
});
```

You can hide progress output while keeping failure details and the final summary:

```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test';

export default defineConfig({
reporter: [['list', { printOnlyFailures: true }]],
});
```

Failed attempts are printed when they finish, without displaying an initial progress row. This can be combined with `printFailuresInline`. It takes precedence over `printSteps` and does not suppress stdout or stderr from tests.

You can omit test tags that are automatically appended to test titles:

```js title="playwright.config.ts"
Expand All @@ -123,6 +150,7 @@ List report supports the following configuration options and environment variabl
|---|---|---|---|
| `PLAYWRIGHT_LIST_PRINT_STEPS` | `printSteps` | Whether to print each step on its own line. | `false`
| `PLAYWRIGHT_LIST_PRINT_FAILURES_INLINE` | `printFailuresInline` | Whether to print failure details immediately after a failed test instead of at the end. | `false`
| `PLAYWRIGHT_LIST_PRINT_ONLY_FAILURES` | `printOnlyFailures` | Whether to hide progress output while keeping failure details and the final summary. | `false`
| `PLAYWRIGHT_LIST_OMIT_TAGS` | `omitTags` | Whether to omit test tags that are automatically appended to test titles. | `false`
| `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. Supports `true`, `1`, `false`, `0`, `[WIDTH]`, and `[WIDTH]x[HEIGHT]`. `[WIDTH]` and `[WIDTH]x[HEIGHT]` specifies the TTY dimensions. | `true` when terminal is in TTY mode, `false` otherwise.
| `FORCE_COLOR` | | Whether to produce colored output. | `true` when terminal is in TTY mode, `false` otherwise.
Expand Down Expand Up @@ -159,10 +187,23 @@ Running 124 tests using 6 workers
[23/124] gitignore.spec.ts - should respect nested .gitignore
```

You can hide progress output while keeping failure details and the final summary:

```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test';

export default defineConfig({
reporter: [['line', { printOnlyFailures: true }]],
});
```

This does not suppress stdout or stderr from tests. Use [`property: TestConfig.quiet`] to suppress that output.

Line report supports the following configuration options and environment variables:

| Environment Variable Name | Reporter Config Option| Description | Default
|---|---|---|---|
| `PLAYWRIGHT_LINE_PRINT_ONLY_FAILURES` | `printOnlyFailures` | Whether to hide progress output while keeping failure details and the final summary. | `false`
| `PLAYWRIGHT_LINE_OMIT_TAGS` | `omitTags` | Whether to omit test tags that are automatically appended to test titles. | `false`
| `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. Supports `true`, `1`, `false`, `0`, `[WIDTH]`, and `[WIDTH]x[HEIGHT]`. `[WIDTH]` and `[WIDTH]x[HEIGHT]` specifies the TTY dimensions. | `true` when terminal is in TTY mode, `false` otherwise.
| `FORCE_COLOR` | | Whether to produce colored output. | `true` when terminal is in TTY mode, `false` otherwise.
Expand Down Expand Up @@ -203,10 +244,23 @@ One character is displayed for each test that has run, indicating its status:
| `T` | Timed out
| `°` | Skipped

You can hide progress output while keeping failure details and the final summary:

```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test';

export default defineConfig({
reporter: [['dot', { printOnlyFailures: true }]],
});
```

This does not suppress stdout or stderr from tests. Use [`property: TestConfig.quiet`] to suppress that output.

Dot report supports the following configuration options and environment variables:

| Environment Variable Name | Reporter Config Option| Description | Default
|---|---|---|---|
| `PLAYWRIGHT_DOT_PRINT_ONLY_FAILURES` | `printOnlyFailures` | Whether to hide progress output while keeping failure details and the final summary. | `false`
| `PLAYWRIGHT_DOT_OMIT_TAGS` | `omitTags` | Whether to omit test tags that are automatically appended to test titles. | `false`
| `PLAYWRIGHT_FORCE_TTY` | | Whether to produce output suitable for a live terminal. Supports `true`, `1`, `false`, `0`, `[WIDTH]`, and `[WIDTH]x[HEIGHT]`. `[WIDTH]` and `[WIDTH]x[HEIGHT]` specifies the TTY dimensions. | `true` when terminal is in TTY mode, `false` otherwise.
| `FORCE_COLOR` | | Whether to produce colored output. | `true` when terminal is in TTY mode, `false` otherwise.
Expand Down
3 changes: 2 additions & 1 deletion packages/playwright/src/cli/reportActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export async function showReport(report: string | undefined, host: string, port:

export async function mergeReports(reportDir: string | undefined, opts: { [key: string]: any }) {
const configFile = opts.config;
const config = configFile ? await configLoader.loadConfigFromFile(configFile) : await configLoader.loadEmptyConfigForMergeReports();
const overrides = { reporterOnlyFailures: opts.reporterOnlyFailures ? true : undefined };
const config = configFile ? await configLoader.loadConfigFromFile(configFile, overrides) : await configLoader.loadEmptyConfigForMergeReports(overrides);

const dir = path.resolve(process.cwd(), reportDir || '');
const dirStat = await fs.promises.stat(dir).catch(e => null);
Expand Down
1 change: 1 addition & 0 deletions packages/playwright/src/cli/testActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ function overridesFromOptions(options: { [key: string]: any }): ipc.ConfigCLIOve
repeatEach: options.repeatEach ? parseInt(options.repeatEach, 10) : undefined,
retries: options.retries ? parseInt(options.retries, 10) : undefined,
reporter: resolveReporterOption(options.reporter),
reporterOnlyFailures: options.reporterOnlyFailures ? true : undefined,
additionalReporters: resolveReporterOption(options.addReporter),
shard: resolveShardOption(options.shard),
timeout: options.timeout ? parseInt(options.timeout, 10) : undefined,
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright/src/common/configLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ export async function loadConfigFromFile(configFile: string | undefined, overrid
return await loadConfig(resolveConfigLocation(configFile), overrides, ignoreDeps);
}

export async function loadEmptyConfigForMergeReports() {
export async function loadEmptyConfigForMergeReports(overrides?: ConfigCLIOverrides) {
// Merge reports is "different" for no good reason. It should not pick up local config from the cwd.
return await loadConfig({ configDir: process.cwd() });
return await loadConfig({ configDir: process.cwd() }, overrides);
}
1 change: 1 addition & 0 deletions packages/playwright/src/common/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export type ConfigCLIOverrides = {
repeatEach?: number;
retries?: number;
reporter?: ReporterDescription[];
reporterOnlyFailures?: boolean;
additionalReporters?: ReporterDescription[];
shard?: { current: number, total: number };
timeout?: number;
Expand Down
2 changes: 2 additions & 0 deletions packages/playwright/src/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ function addMergeReportsCommand(program: Command) {
});
command.option('-c, --config <file>', `Configuration file. Can be used to specify additional configuration for the output report.`);
command.option('--reporter <reporter>', `Reporter to use, comma-separated, can be ${builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${config.defaultReporter}")`);
command.option('--reporter-only-failures', 'Only report failures with built-in reporters');
command.addHelpText('afterAll', `
Arguments [dir]:
Directory containing blob reports.
Expand Down Expand Up @@ -230,6 +231,7 @@ const testOptions: [string, { description: string, choices?: string[], preset?:
['--quiet', { description: `Suppress stdio` }],
['--repeat-each <N>', { description: `Run each test N times (default: 1)` }],
['--reporter <reporter>', { description: `Reporter to use, comma-separated, can be ${builtInReporters.map(name => `"${name}"`).join(', ')} (default: "${config.defaultReporter}")` }],
['--reporter-only-failures', { description: `Only report failures with built-in reporters` }],
['--retries <retries>', { description: `Maximum retry count for flaky tests, zero for no retries (default: no retries)` }],
['--run-agents <mode>', { description: `Run agents to generate the code for page.perform`, choices: ['missing', 'all', 'none'], preset: 'none' }],
['--shard <shard>', { description: `Shard tests and execute only the selected shard, specify in the form "current/all", 1-based, for example "3/5"` }],
Expand Down
34 changes: 31 additions & 3 deletions packages/playwright/src/reporters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { fitToWidth } from '@utils/stringWidth';
import { resolveReporterOutputPath, stripAnsiEscapes } from '../util';

import type { ReporterV2 } from './reporterV2';
import type { FullConfig, FullResult, Location, Suite, TestCase, TestError, TestResult, TestStep } from '../../types/testReporter';
import type { FullConfig, FullResult, Location, ReporterOptions, Suite, TestCase, TestError, TestResult, TestStep } from '../../types/testReporter';
import type { Colors } from '@isomorphic/colors';

export type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' };
Expand All @@ -49,7 +49,7 @@ type TestSummary = {
fatalErrors: TestError[];
};

export type CommonReporterOptions = {
export type CommonReporterOptions = ReporterOptions & {
configDir: string,
_mode?: 'list' | 'test' | 'merge',
_commandHash?: string,
Expand Down Expand Up @@ -159,7 +159,7 @@ export const internalScreen: Screen = {
resolveFiles: 'rootDir',
};

export type TerminalReporterOptions = {
export type TerminalReporterOptions = ReporterOptions & {
screen?: TerminalScreen;
omitFailures?: boolean;
includeTestId?: boolean;
Expand Down Expand Up @@ -388,6 +388,34 @@ export class TerminalReporter implements ReporterV2 {
}
}

export function isFailure(test: TestCase): boolean {
const outcome = test.outcome();
return outcome === 'unexpected' || outcome === 'flaky' || test.results.some(result => result.status === 'interrupted');
}

export type TestFilter = (test: TestCase) => boolean;

export function createTestFilter(options: ReporterOptions): TestFilter | undefined {
return options.onlyFailures ? isFailure : undefined;
}

export function* visitTests(suite: Suite, filter?: TestFilter): Generator<TestCase> {
for (const entry of suite.entries()) {
if (entry.type !== 'test')
yield* visitTests(entry, filter);
else if (!filter || filter(entry))
yield entry;
}
}

export function filterSuites(suites: Suite[], filter?: TestFilter): Suite[] {
return filter ? suites.filter(suite => !visitTests(suite, filter).next().done) : suites;
}

export function filterSuiteEntries(suite: Suite, filter?: TestFilter): (Suite | TestCase)[] {
return filter ? suite.entries().filter(entry => entry.type === 'test' ? filter(entry) : !visitTests(entry, filter).next().done) : suite.entries();
}

function formatResultErrors(screen: Screen, test: TestCase, result: TestResult): string {
const lines: string[] = [];
if (test.outcome() === 'unexpected') {
Expand Down
7 changes: 6 additions & 1 deletion packages/playwright/src/reporters/dot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,17 @@ import type { CommonReporterOptions, TerminalReporterOptions } from './base';

class DotReporter extends TerminalReporter {
private _counter = 0;
private _printOnlyFailures: boolean;

constructor(options?: DotReporterOptions & CommonReporterOptions & TerminalReporterOptions) {
super({ ...options, omitTags: getAsBooleanFromENV('PLAYWRIGHT_DOT_OMIT_TAGS', options?.omitTags) });
this._printOnlyFailures = !!options?.onlyFailures || getAsBooleanFromENV('PLAYWRIGHT_DOT_PRINT_ONLY_FAILURES', options?.printOnlyFailures);
}

override onBegin(suite: Suite) {
super.onBegin(suite);
this.writeLine(this.generateStartingMessage());
if (!this._printOnlyFailures)
this.writeLine(this.generateStartingMessage());
}

override onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult) {
Expand All @@ -48,6 +51,8 @@ class DotReporter extends TerminalReporter {

override onTestEnd(test: TestCase, result: TestResult) {
super.onTestEnd(test, result);
if (this._printOnlyFailures)
return;
if (this._counter === 80) {
this.screen.stdout.write('\n');
this._counter = 0;
Expand Down
13 changes: 8 additions & 5 deletions packages/playwright/src/reporters/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,12 @@ import { extractZip } from '@utils/third_party/extractZip';
// `import('vite')`) is DCE'd in release builds.
declare const __PW_HMR__: boolean;

import { CommonReporterOptions, formatError, formatResultFailure, internalScreen } from './base';
import { createTestFilter, filterSuiteEntries, filterSuites, formatError, formatResultFailure, internalScreen } from './base';
import * as babel from '../transform/babelBundle';
import { resolveReporterOutputPath, stripAnsiEscapes } from '../util';

import type { ReportConfigureParams, ReportEndParams, ReporterV2 } from './reporterV2';
import type { CommonReporterOptions, TestFilter } from './base';
import type { HtmlReporterOptions as HtmlReporterConfigOptions, Metadata, TestAnnotation } from '../../types/test';
import type * as api from '../../types/testReporter';
import type { HTMLReport, HTMLReportOptions, Location, Stats, TestAttachment, TestCase, TestCaseSummary, TestFile, TestFileSummary, TestResult, TestStep } from '@html-reporter/types';
Expand Down Expand Up @@ -164,7 +165,7 @@ class HtmlReporter implements ReporterV2 {
noSnippets,
noCopyPrompt,
mergeFiles,
});
}, createTestFilter(this._options));
this._buildResult = await builder.build(this.config.metadata, projectSuites, result, this._topLevelErrors, this._machines);
}

Expand Down Expand Up @@ -336,13 +337,15 @@ class HtmlBuilder {
private _attachmentsBaseURL: string;
private _options: HTMLReportOptions;
private _doNotInlineAssets: boolean;
private _testFilter: TestFilter | undefined;

constructor(yazl: typeof import('yazl'), config: api.FullConfig, outputDir: string, attachmentsBaseURL: string, doNotInlineAssets: boolean, options: HTMLReportOptions) {
constructor(yazl: typeof import('yazl'), config: api.FullConfig, outputDir: string, attachmentsBaseURL: string, doNotInlineAssets: boolean, options: HTMLReportOptions, testFilter?: TestFilter) {
this._dataZipFile = new yazl.ZipFile();
this._config = config;
this._reportFolder = outputDir;
this._options = options;
this._doNotInlineAssets = doNotInlineAssets;
this._testFilter = testFilter;
fs.mkdirSync(this._reportFolder, { recursive: true });
this._attachmentsBaseURL = attachmentsBaseURL;
}
Expand All @@ -351,7 +354,7 @@ class HtmlBuilder {
const data: DataMap = new Map();
for (const projectSuite of projectSuites) {
const projectName = projectSuite.project()!.name;
for (const fileSuite of projectSuite.suites) {
for (const fileSuite of filterSuites(projectSuite.suites, this._testFilter)) {
const fileName = this._relativeLocation(fileSuite.location)!.file;
this._createEntryForSuite(data, projectName, fileSuite, fileName, true);
}
Expand Down Expand Up @@ -502,7 +505,7 @@ class HtmlBuilder {

private _processSuite(suite: api.Suite, projectName: string, path: string[], deep: boolean, outTests: TestEntry[]) {
const newPath = [...path, suite.title];
suite.entries().forEach(e => {
filterSuiteEntries(suite, this._testFilter).forEach(e => {
if (e.type === 'test')
outTests.push(this._createTestEntry(e, projectName, newPath));
else if (deep)
Expand Down
Loading
Loading