Skip to main content

Reporters

How to customize reporter output and write your own.

Contents

  1. Built-in reporters
  2. Reporter combinations
  3. HTML report customization
  4. JUnit, JSON, blob
  5. Custom attachments
  6. Writing a custom reporter
  7. Third-party reporters

Built-in reporters

ReporterWhat it doesUse when
listReal-time stream of test results in terminalDefault for dev — see progress
lineCompact single-line updatesLong runs where list is too verbose
dotOne character per test resultCI logs you don't read normally
htmlSelf-contained interactive web reportAlways — primary debugging artifact
jsonMachine-readable result dumpDashboards, custom tooling
junitXML in JUnit formatMost CI systems display this natively
githubInline GitHub Actions annotationsPR workflows
blobIntermediate format for merging across shardsSharded CI
nullSilentProgrammatic runs that parse stdout themselves

Reporter combinations

// playwright.config.ts
reporter: [
['list'],
['html', { outputFolder: 'playwright-report', open: 'never' }],
['json', { outputFile: 'results.json' }],
['junit', { outputFile: 'junit.xml' }],
['github'],
],

Order matters slightly — first reporter is "primary" for some plugins. Compose multiple freely.

Conditional on env

reporter: process.env.CI
? [['github'], ['html', { open: 'never' }], ['junit', { outputFile: 'results.xml' }]]
: [['list'], ['html', { open: 'on-failure' }]],

Sharded CI — use blob

reporter: process.env.CI ? [['blob']] : [['list'], ['html']],

Then merge after all shards finish:

npx playwright merge-reports --reporter=html ./all-blob-reports

HTML report

The HTML reporter is the gold standard — embedded videos, screenshots, traces, network logs, step-by-step actions.

['html', {
outputFolder: 'playwright-report', // where to write
open: 'always', // 'always' | 'on-failure' | 'never'
host: 'localhost', // for `playwright show-report`
port: 9323, // default port for the report server
attachmentsBaseURL: 'https://...', // if hosting attachments elsewhere
}],

Opening

npm run report # uses this project's helper
npx playwright show-report # opens default folder
npx playwright show-report path/to/dir # specific folder

Filtering and searching

The HTML report's search bar supports:

SyntaxMeaning
textMatch in test title
@smokeMatch tag
s:passedStatus filter (passed, failed, skipped, flaky)
p:chromiumProject filter
f:login.spec.tsFile filter

Step tree

Every action and assertion appears as a step. Failed steps highlight in red. Click any step to see:

  • The exact line in your test
  • DOM snapshot (if a locator action)
  • Network requests during that step

Machine-readable

JSON

['json', { outputFile: 'results.json' }],

Schema is documented at https://playwright.dev/docs/test-reporter-api. Useful keys: stats, suites[*].specs[*].tests[*].results[*].

# Quick failure summary from CI logs
jq '[.suites[].specs[].tests[] | select(.results[0].status == "failed") | .title]' results.json

JUnit

['junit', { outputFile: 'junit.xml' }],

Most CI systems (GitHub Actions, GitLab, Jenkins, CircleCI) display this in their test-results UI automatically.

Blob

Used only for sharding — see ci.md.

Attachments

Attach arbitrary files to a test result — visible in the HTML report:

test('with custom attachment', async ({ page }, testInfo) => {
await page.goto('/');

// Attach a screenshot
await testInfo.attach('homepage-screenshot', {
body: await page.screenshot(),
contentType: 'image/png',
});

// Attach a JSON payload
await testInfo.attach('api-response', {
body: JSON.stringify({ users: 100 }, null, 2),
contentType: 'application/json',
});

// Attach a file from disk
await testInfo.attach('export.csv', {
path: 'fixtures/data.csv',
contentType: 'text/csv',
});
});

Attach via fixture (universal capture)

test.extend<{}>({
page: async ({ page }, use, testInfo) => {
await use(page);

// On failure, attach DOM HTML for post-mortem
if (testInfo.status !== testInfo.expectedStatus) {
const html = await page.content();
await testInfo.attach('page-html', {
body: html,
contentType: 'text/html',
});
}
},
});

Custom reporter

Implement the Reporter interface from @playwright/test/reporter:

// reporters/slack-reporter.ts
import type { Reporter, TestCase, TestResult, FullResult } from '@playwright/test/reporter';

export default class SlackReporter implements Reporter {
private failures: { title: string; error: string }[] = [];

onTestEnd(test: TestCase, result: TestResult) {
if (result.status === 'failed' || result.status === 'timedOut') {
this.failures.push({
title: test.title,
error: result.error?.message ?? 'unknown',
});
}
}

async onEnd(result: FullResult) {
if (this.failures.length === 0) return;

await fetch(process.env.SLACK_WEBHOOK!, {
method: 'POST',
body: JSON.stringify({
text: `${this.failures.length} Playwright tests failed`,
attachments: this.failures.map(f => ({
color: 'danger',
title: f.title,
text: f.error,
})),
}),
});
}
}

Register:

// playwright.config.ts
reporter: [
['list'],
['./reporters/slack-reporter.ts'],
],

Reporter lifecycle hooks

HookWhenUse
onBegin(config, suite)Before any test runsLog run start, send "starting" notifications
onTestBegin(test, result)Before each testPer-test logging
onStepBegin(test, result, step)Before each step (locator action, expect)Verbose tracing
onStepEnd(test, result, step)After each stepStep-level analytics
onTestEnd(test, result)After each testPer-test reporting
onEnd(result)After all tests doneSummary, notifications, uploads
onError(error)Reporter / runner errorCrash handling
printsToStdio()Returns booleantrue if your reporter writes to stdout

Third-party

Notable community reporters:

ReporterPurpose
@currents/playwrightCloud dashboard with cross-run analytics
playwright-qase-reporterSync with Qase test management
allure-playwrightAllure framework report — richer than HTML
monocart-reporterHTML report with code coverage
playwright-tesults-reporterUpload to Tesults
playwright-bddCucumber/Gherkin integration

Install: npm install -D <package>, add to reporter array in config.

Recipes

Slack only on main branch failures

reporter: [
['list'],
['html', { open: 'never' }],
...(process.env.GITHUB_REF === 'refs/heads/main' ? [['./reporters/slack.ts']] : []),
],

Email digest of flaky tests

// reporters/flaky-digest.ts
export default class FlakyDigest implements Reporter {
private flaky: string[] = [];

onTestEnd(test: TestCase, result: TestResult) {
if (result.retry > 0 && result.status === 'passed') {
this.flaky.push(test.title);
}
}

async onEnd() {
if (this.flaky.length > 0) {
await sendEmail({
subject: `${this.flaky.length} flaky tests in last run`,
body: this.flaky.join('\n'),
});
}
}
}

Report only failures to stdout

// Set `quiet: true` in config to suppress stdout from test process
// AND use the dot reporter for compact terminal output
quiet: true,
reporter: [['dot'], ['html']],