Skip to main content

Overview

Playwright provides a flexible reporter API that allows you to create custom test reporters. Custom reporters enable you to integrate test results with your CI/CD pipeline, send notifications, generate custom reports, or integrate with third-party services.

Reporter API

Custom reporters implement the Reporter interface with lifecycle hooks that are called during test execution.

Core Reporter Methods

import type {
  FullConfig,
  FullResult,
  Reporter,
  Suite,
  TestCase,
  TestResult,
} from '@playwright/test/reporter';

class MyReporter implements Reporter {
  onBegin(config: FullConfig, suite: Suite) {
    console.log(`Starting test run with ${suite.allTests().length} tests`);
  }

  onTestBegin(test: TestCase, result: TestResult) {
    console.log(`Starting test ${test.title}`);
  }

  onTestEnd(test: TestCase, result: TestResult) {
    console.log(`Finished test ${test.title}: ${result.status}`);
  }

  onEnd(result: FullResult) {
    console.log(`Finished test run: ${result.status}`);
  }
}

export default MyReporter;

Lifecycle Hooks

The reporter lifecycle provides hooks at different stages of test execution.

Available Hooks

Called once before running tests. Receives the full configuration and root suite.
onBegin(config: FullConfig, suite: Suite): void
Called for each test when it starts running.
onTestBegin(test: TestCase, result: TestResult): void
Called when a test writes to stdout.
onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult): void
Called when a test writes to stderr.
onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult): void
Called after each test finishes.
onTestEnd(test: TestCase, result: TestResult): void
Called after all tests complete.
async onEnd(result: FullResult): Promise<void>
Called on global errors not associated with a specific test.
onError(error: TestError): void

Practical Examples

JSON Reporter

Create a custom JSON reporter that outputs structured test results.
JSON Reporter Example
import fs from 'fs';
import path from 'path';
import type {
  FullConfig,
  FullResult,
  Reporter,
  Suite,
  TestCase,
  TestResult,
} from '@playwright/test/reporter';

interface JSONReportTest {
  title: string;
  file: string;
  line: number;
  column: number;
  duration: number;
  status: string;
  error?: string;
}

class JSONReporter implements Reporter {
  private config!: FullConfig;
  private suite!: Suite;
  private results: JSONReportTest[] = [];
  private outputFile: string;

  constructor(options: { outputFile?: string } = {}) {
    this.outputFile = options.outputFile || 'test-results.json';
  }

  onBegin(config: FullConfig, suite: Suite) {
    this.config = config;
    this.suite = suite;
  }

  onTestEnd(test: TestCase, result: TestResult) {
    this.results.push({
      title: test.title,
      file: path.relative(this.config.rootDir, test.location.file),
      line: test.location.line,
      column: test.location.column,
      duration: result.duration,
      status: result.status,
      error: result.error?.message,
    });
  }

  async onEnd(result: FullResult) {
    const report = {
      status: result.status,
      duration: result.duration,
      startTime: result.startTime,
      tests: this.results,
    };

    await fs.promises.writeFile(
      this.outputFile,
      JSON.stringify(report, null, 2)
    );
    console.log(`JSON report written to ${this.outputFile}`);
  }
}

export default JSONReporter;

Slack Notification Reporter

Send test results to Slack when tests fail.
Slack Reporter
import type {
  FullConfig,
  FullResult,
  Reporter,
  Suite,
  TestCase,
  TestResult,
} from '@playwright/test/reporter';

class SlackReporter implements Reporter {
  private webhookUrl: string;
  private failures: Array<{ test: TestCase; result: TestResult }> = [];

  constructor(options: { webhookUrl: string }) {
    this.webhookUrl = options.webhookUrl;
  }

  onBegin(config: FullConfig, suite: Suite) {
    console.log(`Running ${suite.allTests().length} tests`);
  }

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

  async onEnd(result: FullResult) {
    if (this.failures.length === 0) {
      await this.sendSlackMessage({
        text: '✅ All tests passed!',
        color: 'good',
      });
      return;
    }

    const failureMessages = this.failures.map(({ test, result }) => {
      return `• ${test.title} - ${result.status}\n  ${result.error?.message || 'No error message'}`;
    });

    await this.sendSlackMessage({
      text: `❌ ${this.failures.length} test(s) failed`,
      color: 'danger',
      fields: [
        {
          title: 'Failed Tests',
          value: failureMessages.join('\n\n'),
        },
      ],
    });
  }

  private async sendSlackMessage(message: any) {
    try {
      const response = await fetch(this.webhookUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          attachments: [message],
        }),
      });
      
      if (!response.ok) {
        console.error('Failed to send Slack notification');
      }
    } catch (error) {
      console.error('Error sending Slack notification:', error);
    }
  }
}

export default SlackReporter;

Custom HTML Reporter

Generate a custom HTML report with test results.
HTML Reporter
import fs from 'fs';
import type {
  FullConfig,
  FullResult,
  Reporter,
  Suite,
  TestCase,
  TestResult,
} from '@playwright/test/reporter';

class HTMLReporter implements Reporter {
  private suite!: Suite;
  private startTime!: Date;
  private outputFile: string;

  constructor(options: { outputFile?: string } = {}) {
    this.outputFile = options.outputFile || 'report.html';
  }

  onBegin(config: FullConfig, suite: Suite) {
    this.suite = suite;
    this.startTime = new Date();
  }

  async onEnd(result: FullResult) {
    const allTests = this.suite.allTests();
    const passed = allTests.filter(t => t.ok()).length;
    const failed = allTests.filter(t => !t.ok()).length;

    const html = `
<!DOCTYPE html>
<html>
<head>
  <title>Test Report</title>
  <style>
    body { font-family: Arial, sans-serif; margin: 20px; }
    .summary { background: #f5f5f5; padding: 20px; border-radius: 5px; }
    .passed { color: green; }
    .failed { color: red; }
    .test { margin: 10px 0; padding: 10px; border: 1px solid #ddd; }
  </style>
</head>
<body>
  <h1>Test Report</h1>
  <div class="summary">
    <p>Total: ${allTests.length}</p>
    <p class="passed">Passed: ${passed}</p>
    <p class="failed">Failed: ${failed}</p>
    <p>Duration: ${result.duration}ms</p>
  </div>
  <h2>Test Results</h2>
  ${allTests.map(test => `
    <div class="test ${test.ok() ? 'passed' : 'failed'}">
      <strong>${test.title}</strong>
      <p>Status: ${test.outcome()}</p>
    </div>
  `).join('')}
</body>
</html>
    `;

    await fs.promises.writeFile(this.outputFile, html);
    console.log(`HTML report generated: ${this.outputFile}`);
  }
}

export default HTMLReporter;

Configuration

Register your custom reporter in the Playwright configuration file.
playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  reporter: [
    ['./my-custom-reporter.ts', { outputFile: 'custom-report.json' }],
    ['html'], // Keep default HTML reporter
    ['list'], // Keep list reporter for terminal output
  ],
});

Multiple Reporters

You can use multiple reporters simultaneously. Playwright will call the hooks on all reporters in the order they are defined.

Best Practices

Performance

Keep reporter logic lightweight to avoid slowing down test execution. Use async operations for I/O.

Error Handling

Always handle errors in reporters gracefully. A failing reporter shouldn’t break the test run.

Configuration

Make reporters configurable through constructor options for maximum flexibility.

Standards

Follow common reporting standards (JUnit XML, TAP) when integrating with CI/CD systems.

Built-in Reporters

Learn about Playwright’s built-in reporters

CI/CD Integration

Configure reporters for continuous integration

Build docs developers (and LLMs) love