Advanced Chapter 17 · 14 min read

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.

Tip: Use 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.

reporters/slack-reporter.ts
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;
playwright.config.reporters.ts
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
    }],
  ],
});
CustomReporter.java
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_reporter.py
# 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}"
    )

Playwright Advanced Custom Reporters

Written by PV

© 2026 All Rights Reserved