Introduction
Test automation does not end when a test passes or fails.
In a professional QA automation framework, teams also need to know:
- Which tests passed?
- Which tests failed?
- How long did they take?
- Which browser failed?
- Which environment was tested?
- Was the failure a retry?
- What error occurred?
- Where is the screenshot?
- Where is the trace?
- Which worker executed the test?
- Which build generated the result?
Playwright already provides built-in reporters such as list, line, dot, json, junit, html, and blob. For many projects, these are enough. But enterprise teams often need reporting tailored to their workflow.
This is where playwright custom reporter development becomes valuable.
A custom reporter lets you consume Playwright Test Runner events and transform them into your own console output, JSON structure, HTML dashboard, metrics stream, CI artifact, or external reporting integration.
The basic architecture is:
Playwright Test Runner
|
| Reporter API Events
v
+———————–+
| Custom Reporter |
| |
| onBegin() |
| onTestBegin() |
| onStepBegin() |
| onStepEnd() |
| onTestEnd() |
| onEnd() |
| onExit() |
+———–+———–+
|
+—–+——+
| |
JSON HTML
| |
+—–+——+
|
CI Artifacts
This playwright custom reporter development tutorial explains how to build that architecture using Playwright TypeScript, from a simple reporter to an enterprise reporting system.
What Is Playwright Custom Reporter Development?
Playwright custom reporter development means implementing Playwright’s Reporter interface to receive test-run lifecycle events and produce customized reporting output.
A custom reporter is normally a TypeScript or JavaScript class exported as the default export.
For example:
import type {
Reporter,
FullConfig,
Suite,
TestCase,
TestResult,
FullResult
} from ‘@playwright/test/reporter’;
class CustomReporter implements Reporter {
onBegin(config: FullConfig, suite: Suite) {
console.log(`Starting ${suite.allTests().length} tests`);
}
onTestEnd(test: TestCase, result: TestResult) {
console.log(`${test.title}: ${result.status}`);
}
onEnd(result: FullResult) {
console.log(`Run finished: ${result.status}`);
}
}
export default CustomReporter;
Playwright’s Reporter API defines these lifecycle callbacks, and all reporter methods are optional.
The reporter is then configured in playwright.config.ts:
reporter: ‘./reporters/custom-reporter.ts’
Built-In Playwright Reporters vs Custom Reporters
Playwright already provides several reporters.
| Reporter | Best use |
| list | Detailed terminal output |
| line | Compact terminal output |
| dot | Minimal CI output |
| github | GitHub-oriented annotations |
| json | Machine-readable results |
| junit | CI/test-management integration |
| html | Interactive test dashboard |
| blob | Combining distributed/sharded results |
| Custom | Organization-specific reporting |
Built-in reporters are generally preferable when their output already satisfies the team’s needs.
Build a custom reporter when you need something specific.
Examples:
- Custom HTML dashboard
- Business KPI calculations
- Slack/Teams integration
- Build metadata
- Test ownership
- Environment metrics
- Custom artifact links
- Historical trend data
- Internal QA dashboards
- External test-management integration
A good Automation Architect should avoid reinventing a built-in reporter unnecessarily.
Why Build a Custom Playwright Reporter?
Imagine a company wants this output:
Release: 2026.08.22
Environment: staging
Browser: chromium
Total: 420
Passed: 398
Failed: 12
Skipped: 10
Flaky: 6
Critical failures: 3
Slow tests: 8
Duration: 14m 21s
The built-in HTML reporter may provide detailed test information, but the organization’s executive dashboard might need a completely different format.
A custom reporter can transform raw Playwright events into exactly that structure.
The key design principle is:
The test runner executes tests. The reporter interprets the results.
Do not put test logic into a reporter.
Playwright Reporter API and Reporter Lifecycle
Understanding the lifecycle is the most important part of playwright custom reporter development.
A simplified lifecycle is:
onBegin()
|
+–> onTestBegin()
| |
| +–> onStepBegin()
| +–> onStepEnd()
| |
| +–> onTestEnd()
|
+–> more tests
|
onEnd()
|
onExit()
Playwright documents the typical order as onBegin(), test events, onEnd(), and finally onExit(). onTestEnd() receives the completed TestResult, while onEnd() receives the final run status.
Important Reporter Methods
| Method | Purpose |
| onBegin() | Initialize reporting |
| onTestBegin() | Test execution starts |
| onStepBegin() | Step starts |
| onStepEnd() | Step finishes |
| onTestEnd() | Test execution finishes |
| onStdOut() | Capture stdout |
| onStdErr() | Capture stderr |
| onError() | Global errors |
| onEnd() | Finalize report |
| onExit() | Last cleanup/upload stage |
| printsToStdio() | Tell Playwright whether reporter writes terminal output |
You do not need to implement every method.
Creating Your First Custom Reporter
Problem
You want to print a simple result for every test.
Reporter Design
Create:
reporters/
└── basic-reporter.ts
Complete TypeScript Example
import type {
Reporter,
FullConfig,
Suite,
TestCase,
TestResult,
FullResult
} from ‘@playwright/test/reporter’;
class BasicReporter implements Reporter {
onBegin(config: FullConfig, suite: Suite) {
console.log(
`nStarting test run: ${suite.allTests().length} tests`
);
}
onTestBegin(test: TestCase) {
console.log(`START: ${test.title}`);
}
onTestEnd(test: TestCase, result: TestResult) {
console.log(
`END: ${test.title} | ${result.status} | ${result.duration}ms`
);
}
onEnd(result: FullResult) {
console.log(
`nTest run completed: ${result.status}`
);
}
printsToStdio() {
return true;
}
}
export default BasicReporter;
Configure:
import { defineConfig } from ‘@playwright/test’;
export default defineConfig({
reporter: ‘./reporters/basic-reporter.ts’
});
Run:
npx playwright test
Test Execution → Reporter Event → Data Collection → Output
Test starts
↓
onTestBegin()
↓
Test executes
↓
onTestEnd()
↓
Status + duration collected
↓
Console output
Playwright’s official custom reporter example follows this same class-based approach.
Understanding Reporter Methods and Events
onBegin()
Called once before tests run.
Use it for:
- Initializing arrays
- Reading configuration
- Capturing environment metadata
- Creating output directories
- Calculating expected test count
Example:
onBegin(config: FullConfig, suite: Suite) {
this.totalTests = suite.allTests().length;
console.log(
`Running ${this.totalTests} tests`
);
console.log(
`Workers: ${config.workers}`
);
}
The root Suite contains the projects, files, test cases, and nested suites.
onTestBegin()
Use this when a test starts.
onTestBegin(test: TestCase) {
console.log(`Started: ${test.title}`);
}
Avoid expensive operations here.
Remember that large suites may execute many tests concurrently.
onStepBegin() and onStepEnd()
These methods provide step-level visibility.
onStepBegin(test, result, step) {
console.log(
`STEP START: ${test.title} → ${step.title}`
);
}
onStepEnd(test, result, step) {
console.log(
`STEP END: ${test.title} → ${step.title}`
);
}
Use step reporting when your organization needs detailed execution timelines.
Do not store every step indefinitely for extremely large suites unless the business requirement justifies the memory and report size.
onTestEnd()
This is usually the most important event.
At this point, TestResult is complete.
You can inspect:
onTestEnd(test: TestCase, result: TestResult) {
console.log(result.status);
console.log(result.duration);
console.log(result.error);
console.log(result.attachments);
}
Playwright states that onTestEnd() is called after a test run finishes, when TestResult contains its final information.
onEnd()
Use onEnd() to generate the final report.
async onEnd(result: FullResult) {
console.log(
`Final status: ${result.status}`
);
}
onEnd() can return a Promise, so asynchronous report generation is supported.
onExit()
Use onExit() for final operations such as:
- Uploading reports
- Closing external connections
- Final artifact processing
Playwright calls onExit() immediately before the test runner exits, after reporters have received onEnd().
Capturing Test Results, Status, Duration, and Errors
Create a result model:
type TestRecord = {
title: string;
file: string;
project: string;
status: string;
duration: number;
error?: string;
};
Then:
onTestEnd(test: TestCase, result: TestResult) {
this.results.push({
title: test.title,
file: test.location.file,
project: test.parent?.project()?.name ?? ‘unknown’,
status: result.status,
duration: result.duration,
error: result.error?.message
});
}
You can calculate:
Passed
Failed
Skipped
Timed out
Interrupted
Flaky
Remember that a retry can produce multiple TestResult objects for the same TestCase. A robust reporter should decide whether it reports individual attempts or final test outcome.
Creating a Custom Console Reporter
A production-friendly console reporter can highlight slow tests.
import type {
Reporter,
TestCase,
TestResult,
FullConfig,
Suite,
FullResult
} from ‘@playwright/test/reporter’;
class ConsoleReporter implements Reporter {
private slowThreshold = 5000;
onBegin(config: FullConfig, suite: Suite) {
console.log(
`Starting ${suite.allTests().length} tests`
);
}
onTestEnd(test: TestCase, result: TestResult) {
const marker =
result.status === ‘passed’ ? ‘PASS’ :
result.status === ‘failed’ ? ‘FAIL’ :
‘OTHER’;
console.log(
`[${marker}] test.title({result.duration}ms)`
);
if (result.duration > this.slowThreshold) {
console.log(
` SLOW TEST: ${test.title}`
);
}
}
onEnd(result: FullResult) {
console.log(`Run: ${result.status}`);
}
printsToStdio() {
return true;
}
}
export default ConsoleReporter;
This is useful for local development because slow tests become visible without opening an HTML report.
Building a Custom JSON Reporter
JSON is one of the most useful formats for enterprise reporting.
Reporter
import fs from ‘node:fs’;
import path from ‘node:path’;
import type {
Reporter,
FullConfig,
Suite,
TestCase,
TestResult,
FullResult
} from ‘@playwright/test/reporter’;
type TestRecord = {
title: string;
file: string;
project: string;
status: string;
duration: number;
error?: string;
};
class JsonReporter implements Reporter {
private results: TestRecord[] = [];
onBegin(config: FullConfig, suite: Suite) {
this.results = [];
}
onTestEnd(test: TestCase, result: TestResult) {
this.results.push({
title: test.title,
file: test.location.file,
project: test.parent?.project()?.name ?? ‘unknown’,
status: result.status,
duration: result.duration,
error: result.error?.message
});
}
async onEnd(result: FullResult) {
const outputDir = path.resolve(‘custom-report’);
fs.mkdirSync(outputDir, {
recursive: true
});
const report = {
status: result.status,
duration: result.duration,
generatedAt: new Date().toISOString(),
total: this.results.length,
results: this.results
};
fs.writeFileSync(
path.join(outputDir, ‘results.json’),
JSON.stringify(report, null, 2)
);
}
printsToStdio() {
return false;
}
}
export default JsonReporter;
Example output:
{
“status”: “passed”,
“duration”: 12450,
“total”: 3,
“results”: [
{
“title”: “login works”,
“file”: “tests/login.spec.ts”,
“project”: “chromium”,
“status”: “passed”,
“duration”: 1250
}
]
}
This JSON can later feed:
- Internal dashboards
- Data warehouses
- Slack bots
- Test management systems
- Release dashboards
- Analytics pipelines
If a custom reporter does not print to stdout/stderr, returning false from printsToStdio() lets Playwright supplement it with an appropriate terminal reporter.
Building a Custom HTML Reporting Solution
A custom HTML reporter can convert collected results into an HTML page.
A simple implementation:
async onEnd(result: FullResult) {
const rows = this.results.map(item => `
<tr>
<td>escapeHtml(item.title)</td><td>{escapeHtml(item.project)}</td>
<td>escapeHtml(item.status)</td><td>{item.duration} ms</td>
<td>${escapeHtml(item.error ?? ”)}</td>
</tr>
`).join(”);
const html = `
<!DOCTYPE html>
<html>
<head>
<meta charset=”UTF-8″>
<title>Playwright Custom Report</title>
</head>
<body>
<h1>Playwright Test Results</h1>
<p>Run status: ${result.status}</p>
<table border=”1″ cellpadding=”8″>
<tr>
<th>Test</th>
<th>Project</th>
<th>Status</th>
<th>Duration</th>
<th>Error</th>
</tr>
${rows}
</table>
</body>
</html>
`;
fs.writeFileSync(
‘custom-report/index.html’,
html
);
}
In production, do not concatenate untrusted error strings directly into HTML. Escape HTML characters or use a templating system.
Recommended enterprise architecture
Reporter
↓
Normalized Test Model
↓
+———+———-+
| |
JSON HTML
| |
API/Dashboard Human UI
This is more maintainable than putting HTML-generation logic directly into onTestEnd().
Capturing Screenshots, Traces, Videos, and Test Artifacts
A reporter can inspect result.attachments.
onTestEnd(test: TestCase, result: TestResult) {
for (const attachment of result.attachments) {
console.log({
name: attachment.name,
contentType: attachment.contentType,
path: attachment.path
});
}
}
Typical attachments can include:
- Screenshots
- Traces
- Videos
- Logs
- Custom JSON
- Text files
Your reporter can store their metadata:
type Artifact = {
name: string;
contentType: string;
path?: string;
};
Then include links in a custom HTML report.
For example:
Failed: checkout should reject invalid card
Artifacts:
– Screenshot
– Trace
– Video
This is one of the strongest reasons to build a custom reporter for enterprise teams.
Adding Browser, Project, Worker, and Environment Information
A report is more useful when it contains execution metadata.
Playwright exposes project information through the test hierarchy.
You can access:
const projectName =
test.parent?.project()?.name;
For worker-level error information, the Reporter API’s onError() can receive WorkerInfo in supported Playwright versions.
Environment information can be supplied through environment variables:
const environment =
process.env.TEST_ENV ?? ‘local’;
Then include:
{
environment,
project: projectName,
browser: projectName,
buildId: process.env.BUILD_ID
}
A useful enterprise report might contain:
Build: 4812
Environment: staging
Commit: 91af2c1
Project: chromium
Worker: 3
Duration: 8m 14s
This dramatically improves failure investigation.
Integrating Custom Reporters With CI/CD and GitHub Actions
A CI pipeline should preserve reports even when tests fail.
Example:
name: Playwright Tests
on:
pull_request:
push:
branches:
– main
jobs:
test:
runs-on: ubuntu-latest
steps:
– name: Checkout
uses: actions/checkout@v6
– name: Setup Node
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: npm
– name: Install dependencies
run: npm ci
– name: Install Playwright
run: npx playwright install –with-deps chromium
– name: Run tests
run: npx playwright test
– name: Upload custom report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: custom-playwright-report
path: custom-report/
– name: Upload Playwright results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-results
path: test-results/
The important CI principle is:
↓
Do NOT lose report
↓
Upload artifacts
↓
Developer investigates
Playwright’s CI documentation also demonstrates publishing reports and artifacts even when test execution fails.
Combining Custom Reporters With HTML, JSON, and JUnit Reports
You do not have to replace built-in reporters.
Playwright supports multiple reporters:
reporter: [
[‘list’],
[‘html’, {
outputFolder: ‘playwright-report’
}],
[‘json’, {
outputFile: ‘test-results/results.json’
}],
[‘./reporters/custom-reporter.ts’]
]
This creates a layered reporting strategy:
Test Run
|
+————+————-+
| | |
List HTML JSON
| | |
Developer Browser UI Automation
|
Custom Reporter
|
Enterprise Metrics
Playwright’s reporter configuration accepts built-in reporters, modules, or reporter file paths, with optional reporter-specific options.
For JUnit:
reporter: [
[‘junit’, {
outputFile: ‘test-results/e2e.xml’
}],
[‘./reporters/custom-reporter.ts’]
]
JUnit is particularly useful when the CI platform or test-management system consumes XML results.
Handling Parallel Execution and Multiple Workers
Custom reporters must be designed with concurrency in mind.
Playwright runs tests using worker processes, so a reporter should not assume tests execute sequentially.
Avoid relying on:
let currentTest: string;
as if only one test exists at a time.
Instead, store completed results independently:
private results: TestRecord[] = [];
onTestEnd(test: TestCase, result: TestResult) {
this.results.push({
title: test.title,
file: test.location.file,
status: result.status,
duration: result.duration,
project: test.parent?.project()?.name ?? ‘unknown’
});
}
The Reporter API is designed to receive events from test execution, and the final reporter can aggregate them at onEnd().
Important
Do not use a custom reporter as a shared mutable database.
If you need cross-worker aggregation during execution, use a durable mechanism such as:
- Files
- Database
- Queue
- CI artifact
- External reporting API
Custom Reporter Architecture for Enterprise Frameworks
A scalable architecture should separate data collection from presentation.
Playwright
|
Reporter API
|
Event Collector
|
Normalized Results
|
+————-+————-+
| | |
JSON HTML JUnit
| | |
Dashboard Developers CI
|
Artifacts
|
Screenshots/Traces
Recommended project structure:
playwright-framework/
│
├── reporters/
│ ├── enterprise-reporter.ts
│ ├── result-model.ts
│ ├── html-builder.ts
│ └── artifact-manager.ts
│
├── tests/
├── pages/
├── fixtures/
├── test-results/
├── playwright-report/
├── playwright.config.ts
└── package.json
Result model
export interface TestRecord {
id: string;
title: string;
file: string;
project: string;
status: string;
duration: number;
error?: string;
artifacts: Artifact[];
}
This allows multiple presentation formats to use the same data.
Real-World Playwright Custom Reporting Project
Consider a company with:
2,000 tests
3 browsers
8 CI shards
Multiple environments
The QA leadership team wants:
Release dashboard
|
+– Pass rate
+– Failure rate
+– Flaky tests
+– Slow tests
+– Browser failures
+– Environment
+– Build
+– Artifacts
The architecture becomes:
2,000 Tests
|
8 Shards
|
Workers
|
Reporter API
|
Blob / Result Data
|
Aggregation
|
Enterprise Reporter
|
+—-+——-+——-+
| | |
HTML JSON JUnit
| | |
QA Data CI
For sharded runs, Playwright’s blob reporter is specifically designed to retain test results and attachments and later merge results from multiple shards.
A custom reporter can also be used during merged-report processing, but architects must account for the fact that projects from different shards can appear as separate TestProject objects with the same project name.
This matters when aggregating data.
Do not blindly use project name as a unique identifier.
Common Custom Reporter Errors and Solutions
| Problem | Cause | Solution |
| Reporter does not load | Incorrect path/export | Use default export and verify path |
| No terminal output | Reporter captures stdio | Implement printsToStdio() correctly |
| Missing error | Reading result too early | Use onTestEnd() |
| Missing final status | Processing too early | Use onEnd() |
| Report incomplete | Async work not awaited | Return/await Promise |
| Artifacts missing | Wrong attachment handling | Inspect result.attachments |
| Parallel data corrupted | Shared mutable state | Store independent records |
| CI report disappears | Artifact upload only on success | Use if: always/!cancelled() appropriately |
| HTML is broken | Unescaped values | Escape HTML or use templates |
| Sharded report duplicates data | Incorrect aggregation | Include shard/project/build identity |
One especially important point is reporter errors.
Playwright can swallow errors thrown by custom reporter methods. If reporter failures must affect your pipeline, implement explicit error handling rather than assuming a thrown reporter exception will automatically fail the test run.
Playwright Custom Reporter Best Practices
1. Keep reporting separate from testing
A reporter should observe test execution, not control test behavior.
2. Normalize data first
Use a stable TestRecord model before generating HTML or JSON.
3. Avoid excessive terminal output
Large suites can produce enormous logs.
4. Handle retries correctly
Distinguish:
Attempt 1 → failed
Attempt 2 → passed
Final → flaky/passed
from a simple pass.
5. Preserve artifacts
Screenshots, traces, videos, and logs are often more useful than an error string.
6. Design for parallelism
Never assume sequential execution.
7. Support CI metadata
Capture:
- Build ID
- Commit
- Branch
- Environment
- Browser
- Shard
- Worker
8. Make reports deterministic
A report should be reproducible from the same test results.
9. Use built-in reporters when possible
Do not build an HTML reporter if the standard HTML reporter already satisfies the requirement.
10. Keep HTML secure
Escape test titles, error messages, and other dynamic values before inserting them into HTML.
11. Do not block test execution unnecessarily
Avoid slow network calls from onTestEnd() for every test.
Queue or batch external reporting where appropriate.
12. Design for failure
Reports must still be generated when tests fail.
Advanced Playwright Reporter Interview Questions With Answers
1. What is a Playwright custom reporter?
A custom reporter is a class implementing Playwright’s Reporter API to receive test lifecycle events and generate custom reporting output.
2. Which method is called before tests execute?
onBegin().
3. Which method should be used to access the final test result?
onTestEnd().
4. Which method is used to generate the final report?
Usually onEnd().
5. What is onExit() used for?
It is the final reporter lifecycle stage before the runner exits and can be used for last-stage operations such as report uploads.
6. How do you get test duration?
Use:
result.duration
7. How do you get the error?
Use:
result.error
8. How do you access screenshots and traces?
Inspect:
result.attachments
9. Can multiple Playwright reporters run together?
Yes.
reporter: [
[‘html’],
[‘json’, { outputFile: ‘results.json’ }],
[‘./reporters/custom.ts’]
]
10. How should a custom reporter handle parallel workers?
Treat events as concurrent and aggregate independent result records rather than assuming a single sequential execution stream.
11. How would you design an enterprise reporter?
Separate:
Event collection
↓
Data model
↓
Aggregation
↓
Output adapters
↓
HTML / JSON / JUnit / Dashboard
12. How would you support Playwright sharding?
Use shard-aware metadata and aggregate results after shard execution. For Playwright’s built-in blob workflow, merge shard reports before producing the final report.
Playwright Custom Reporter Learning Roadmap
If you are a Selenium engineer or SDET moving toward advanced Playwright framework development, follow this progression.
Level 1 — Playwright Fundamentals
Learn:
- TypeScript
- Locators
- Assertions
- Browser contexts
- Fixtures
- Configuration
Level 2 — Built-In Reporting
Learn:
- List
- Dot
- Line
- HTML
- JSON
- JUnit
- Blob
Level 3 — Reporter API
Learn:
- onBegin
- onTestBegin
- onStepBegin
- onStepEnd
- onTestEnd
- onError
- onEnd
- onExit
Level 4 — Custom Reporting
Build:
- Console reporter
- JSON reporter
- HTML reporter
- Artifact reporter
- CI reporter
Level 5 — Enterprise Reporting
Learn:
- Parallel execution
- Test sharding
- Blob report merging
- CI/CD
- External dashboards
- Metrics
- Test ownership
- Historical reporting
- Failure analytics
For broader framework expertise, continue with Advanced Playwright Automation Techniques, Playwright Custom Fixtures Advanced, Playwright Test Sharding Advanced, Playwright Visual Regression Advanced Setup, Playwright Network Mocking Advanced, Playwright Reporting Tutorial, Playwright Parallel Execution Tutorial, Playwright CI/CD Tutorial, Playwright GitHub Actions Tutorial, Playwright Docker Tutorial, Playwright Framework Design, Playwright TypeScript Tutorial, Playwright Best Practices, and Playwright Interview Questions.
FAQs: Playwright Custom Reporter Development
What is Playwright custom reporter development?
It is the process of building a custom implementation of the Playwright Reporter API to collect test execution events and generate organization-specific reports.
How do I create a custom Playwright reporter?
Create a class implementing Reporter, export it as the default export, and configure its file path under reporter in playwright.config.ts.
What is the most important Playwright Reporter API method?
For individual completed tests, onTestEnd() is especially important because the TestResult is complete at that point.
Can I create a custom JSON reporter?
Yes. Collect test results and write them to a JSON file during onEnd().
Can I build a custom HTML reporter?
Yes. A reporter can collect results and generate an HTML dashboard during onEnd().
Can a custom reporter access screenshots and traces?
Yes. Test attachments are available through TestResult.attachments.
Can I use a custom reporter with the HTML reporter?
Yes. Playwright supports multiple reporters in the same configuration.
How do custom reporters work with CI/CD?
The reporter generates files or artifacts during the test run, and the CI pipeline uploads them for later inspection.
Can a custom reporter work with parallel execution?
Yes, but it must be designed around concurrent worker events and should avoid assumptions about test execution order.
Can custom reporters work with Playwright sharding?
Yes. A robust design should account for shard-specific execution and final aggregation. Playwright’s blob reports are designed specifically for merging results from sharded executions.
Should every Playwright project have a custom reporter?
No. Start with built-in reporters. Build a custom reporter only when there is a concrete reporting requirement that built-in options do not satisfy.
