Reporters in Playwright: real-world lessons and examples. Results your team can read.
After weeks of writing solid Playwright tests, your team realizes the test suite is only half the battle—reporting those results in a format your stakeholders actually read is the other half. Reporters in Playwright transform raw test output into actionable intelligence, whether that's a polished HTML report for post-mortems, JUnit XML for your CI dashboard, or a Slack message that lands in the right channel before the standup. We've learned the hard way that picking the right reporter strategy early saves countless hours of manual result wrangling down the line.
Why reporters matter in production test suites
Test execution is only useful if someone can understand what happened. In early-stage projects, a simple console dump might suffice. But once you're running hundreds of tests across multiple environments, sharded across containers, and feeding results into dashboards, you need structured output that integrates seamlessly with your team's workflow. A reporter in Playwright sits between your test execution and your visibility layer, formatting results into formats that your CI system, your team lead, and your incident response process all understand.
Over the years, we've seen teams build elaborate custom dashboards only to realize they could have solved 80% of their problems with Playwright's built-in reporters and a simple webhook. The key is understanding which reporter solves which problem.
HTML reporter — Rich local and CI reports
The HTML reporter is the most human-friendly output format Playwright offers. It generates a self-contained HTML file with videos, screenshots, traces, and a detailed breakdown of every test case. For local development, it's invaluable. For CI, it's often stored as an artifact and linked from your build logs.
Enable it in your Playwright config:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
reporter: [
['html', { open: 'never' }],
],
webServer: {
command: 'npm run dev',
port: 3000,
},
});
After a test run, open playwright-report/index.html in your browser. You'll see a timeline of all tests, filtered by status, with clickable entries that show console logs, network activity, screenshots at key moments, and full action traces. If a test fails, the report isolates exactly which step broke and provides visual context.
In CI, we typically generate the HTML report and upload it to an artifact store. Here's a GitHub Actions example:
- name: Run Playwright tests
run: npx playwright test
- name: Upload HTML report
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
retention-days: 30
The team can then download the report zip and open it locally, or integrate with services like Argos or Percy that archive and version your reports over time. We've found the HTML report essential for debugging flaky tests—the video replay alone has saved us from misdiagnosing timing issues as race conditions.
JUnit — For CI dashboards
If your CI system (Jenkins, GitLab CI, GitHub Actions) has built-in test result parsing, JUnit XML is the standard input format. The JUnit reporter outputs test results in an XML structure that most CI platforms recognize natively, allowing you to see pass rates, failure trends, and flaky test identification without leaving your pipeline UI.
Configure it alongside HTML:
export default defineConfig({
reporter: [
['html', { open: 'never' }],
['junit', { outputFile: 'junit-results.xml' }],
],
});
The resulting XML file looks roughly like this:
<?xml version="1.0" encoding="UTF-8"?>
<testsuites name="Playwright Suite" tests="42" failures="2" skipped="1" time="145.23">
<testsuite name="auth.spec.ts" tests="5" failures="1">
<testcase classname="auth.spec.ts" name="should login with email" time="2.34"/>
<testcase classname="auth.spec.ts" name="should reject invalid password" time="1.56">
<failure message="Expected 'error' to contain 'Invalid password'">
AssertionError: expected 'success' to contain 'Invalid password'
</failure>
</testcase>
</testsuite>
</testsuites>
GitHub Actions parses this automatically and displays results in the Actions tab. GitLab CI plots a test report widget on merge requests. Jenkins integrates with the JUnit plugin to track trend graphs over weeks. For teams running large suites, JUnit reporting is non-negotiable—it's how you spot when your test suite starts degrading.
Blob reports — Merge results from shards
When your test suite grows large enough that you shard it across multiple machines or containers, gathering all results into a single coherent report becomes complex. Each shard generates its own results, but you need one unified view. Playwright's blob reporter was designed for exactly this scenario.
Blob reports are binary files that Playwright generates during each shard run. You then merge them into a final HTML or JUnit report. This is particularly useful in GitHub Actions where you run tests on parallel runners:
export default defineConfig({
reporter: [
['blob', { outputFile: 'blob-report.zip' }],
],
});
In GitHub Actions, your workflow might look like this:
jobs:
test:
strategy:
matrix:
shard: [1, 2, 3, 4]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: Run Playwright tests
run: npx playwright test --shard=${{ matrix.shard }}/4
- name: Upload blob report
if: always()
uses: actions/upload-artifact@v3
with:
name: blob-report-${{ matrix.shard }}
path: blob-report.zip
merge-reports:
if: always()
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- name: Download blob reports
uses: actions/download-artifact@v3
with:
path: all-blob-reports
pattern: blob-report-*
- name: Merge reports
run: npx playwright merge-reports --reporter html ./all-blob-reports
- name: Upload merged HTML report
uses: actions/upload-artifact@v3
with:
name: html-report
path: html-report/
This approach scales well because each shard doesn't wait for others to finish—they all upload their blob files, and a final merge step combines them into one report. We've used this pattern to run 500+ tests across 10 shards in under three minutes.
Custom reporters — Send results to Slack or dashboards
Standard reporters cover most needs, but sometimes you want test results flowing into custom dashboards, Slack channels, or internal tools. Playwright's reporter API lets you hook into the test lifecycle and send data wherever you need it.
A basic custom reporter looks like this:
import type { Reporter, FullResult } from '@playwright/test/reporter';
export default class SlackReporter implements Reporter {
async onEnd(result: FullResult) {
const { passed, failed, skipped } = result.stats;
const color = failed > 0 ? 'danger' : 'good';
const message = {
attachments: [
{
color,
title: `Test Run: ${passed} passed, ${failed} failed, ${skipped} skipped`,
text: `Duration: ${Math.round(result.duration / 1000)}s`,
},
],
};
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
body: JSON.stringify(message),
});
}
}
Register it in your config:
export default defineConfig({
reporter: [
['./slack-reporter.ts'],
],
});
Now after every test run, your team gets a Slack notification without any manual setup. You can expand this to include failed test names, links to HTML reports, environment info, or whatever context your team needs.
For more sophisticated dashboards, you might send data to a custom endpoint that stores test history:
export default class DashboardReporter implements Reporter {
async onTestEnd(test, result) {
await fetch('https://dashboard.internal/api/test-result', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
testName: test.title,
status: result.status,
duration: result.duration,
timestamp: new Date(),
branch: process.env.CI_BRANCH,
commit: process.env.CI_COMMIT,
}),
});
}
}
This unlocks powerful analytics: you can identify which tests are slowest, which environments have the most failures, or which commits introduced flakiness. Many teams we work with combine this with a simple Next.js dashboard that queries the stored data and shows trends over weeks.
Common mistakes
Forgetting the if: always() condition in CI. If your test upload step only runs on success, you'll never see results when tests fail. Always use conditional execution to ensure reports upload regardless of test status.
Not rotating blob report artifacts. If you run tests multiple times per day, blob reports accumulate rapidly. Set a retention policy (e.g., 7 days) to avoid storage bloat.
Sending too much data to custom reporters. If you're uploading full traces and videos to an external API on every test, you'll hit rate limits and slow down your pipeline. Send structured metadata; store heavy artifacts separately.
Ignoring reporter performance. Custom reporters run in-process and can block the test run from finishing. Keep them fast, or run them asynchronously after the test suite completes.
Combining too many reporters. Having HTML, JUnit, blob, and three custom reporters generates redundant work. Pick one or two that integrate with your actual workflow, not every reporter available.
Key takeaways
- HTML reports are essential for local debugging and post-mortem analysis; store them as CI artifacts with a reasonable retention window.
- JUnit XML integrates natively with CI dashboards, giving you pass rates and failure trends without extra tooling.
- Blob reports scale sharded test runs cleanly, letting you merge results from parallel executions into a single report.
- Custom reporters unlock integration with your team's existing tools—Slack, dashboards, data warehouses—but keep them lightweight and non-blocking.
- Plan your reporter strategy early; adding it retroactively to a mature test suite requires broader coordination.
- Test result visibility is not optional at scale; it's the primary mechanism for maintaining suite health and catching regressions.






Comments
Loading comments…