Custom Reporters
Build custom test reporters that send results to Slack, databases, or custom dashboards beyond the built-in HTML report.
Built-In Reporters
Playwright ships with several reporters out of the box: html (interactive web report), json (machine-readable results), junit (XML for Jenkins/Azure DevOps), dot (minimal CI output), list (detailed per-test output), and github (annotations in GitHub Actions PR checks). You can activate multiple reporters simultaneously by passing an array in the config.
reporter: [['html', { open: 'never' }], ['json', { outputFile: 'results.json' }]] to generate both an HTML report and a JSON file in the same run. CI systems can parse the JSON to post custom pass/fail summaries.Writing a Custom Reporter
A custom reporter is a class that implements the Reporter interface with lifecycle hooks: onBegin, onTestBegin, onTestEnd, onEnd, and optionally onStdOut and onStdErr. The onTestEnd hook receives the full test result including title, status, duration, errors, and attachments. Use it to post results to a Slack webhook, write to a database, or feed a custom dashboard.
Python Reporter Hooks
In Python, pytest plugin hooks (pytest_runtest_logreport, pytest_terminal_summary) provide equivalent functionality. Package your hooks in a conftest.py or a standalone pip-installable pytest plugin to share them across multiple projects.
import https from 'https';
import type { Reporter, TestCase, TestResult, FullResult } from '@playwright/test/reporter';
class SlackReporter implements Reporter {
private passed = 0;
private failed = 0;
private skipped = 0;
private readonly webhookUrl: string;
constructor(options: { webhookUrl: string }) {
this.webhookUrl = options.webhookUrl;
}
onTestEnd(test: TestCase, result: TestResult) {
if (result.status === 'passed') this.passed++;
if (result.status === 'failed') this.failed++;
if (result.status === 'skipped') this.skipped++;
}
async onEnd(result: FullResult) {
const icon = result.status === 'passed' ? ':white_check_mark:' : ':x:';
const payload = JSON.stringify({
text: `${icon} Playwright: ${this.passed} passed / ${this.failed} failed / ${this.skipped} skipped`,
});
// Post to Slack webhook
await new Promise((resolve) => {
const req = https.request(this.webhookUrl, { method: 'POST' }, resolve);
req.end(payload);
});
}
}
export default SlackReporter;
import { defineConfig } from '@playwright/test';
export default defineConfig({
// Multiple reporters in parallel
reporter: [
['html', { outputFolder: 'playwright-report', open: 'never' }],
['json', { outputFile: 'test-results/results.json' }],
['junit', { outputFile: 'test-results/junit.xml' }],
['./reporters/slack-reporter.ts', {
webhookUrl: process.env.SLACK_WEBHOOK_URL
}],
],
});
import com.microsoft.playwright.*;
/**
* Playwright Java uses standard JUnit/TestNG reporters.
* Add custom logic via JUnit 5 TestExecutionListener.
*/
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.engine.TestExecutionResult;
public class CustomReporter implements TestExecutionListener {
private int passed = 0;
private int failed = 0;
@Override
public void executionFinished(
TestIdentifier id, TestExecutionResult result) {
if (!id.isTest()) return;
switch (result.getStatus()) {
case SUCCESSFUL -> passed++;
case FAILED -> failed++;
default -> {}
}
}
@Override
public void testPlanExecutionFinished(TestPlan plan) {
System.out.printf("Results: %d passed, %d failed%n", passed, failed);
}
}
# conftest.py — custom pytest reporter hooks
import json
from pathlib import Path
import pytest
from _pytest.terminal import TerminalReporter
_results = []
def pytest_runtest_logreport(report):
if report.when == "call":
_results.append({
"nodeid": report.nodeid,
"outcome": report.outcome,
"duration": report.duration,
})
def pytest_terminal_summary(terminalreporter: TerminalReporter, exitstatus):
# Write JSON results file for downstream tooling
output = Path("test-results/custom-results.json")
output.parent.mkdir(exist_ok=True)
output.write_text(json.dumps(_results, indent=2))
passed = sum(1 for r in _results if r["outcome"] == "passed")
failed = sum(1 for r in _results if r["outcome"] == "failed")
terminalreporter.write_line(
f"Custom report: {passed} passed, {failed} failed → {output}"
)
Written by PV
© 2026 All Rights Reserved