Reporting and Artifacts

May 26, 2026 · View on GitHub

When to use: Configuring test output for local debugging, CI dashboards, and team visibility. Every project needs a reporting strategy from day one.

Quick Reference

# View the last HTML report
npx playwright show-report

# Run with specific reporter
npx playwright test --reporter=html
npx playwright test --reporter=dot           # minimal CI output
npx playwright test --reporter=line          # one line per test
npx playwright test --reporter=json          # machine-readable
npx playwright test --reporter=junit         # CI integration

# Multiple reporters via CLI
npx playwright test --reporter=dot,html

# Merge shard reports
npx playwright merge-reports --reporter=html ./blob-report

Patterns

Pattern 1: Multi-Reporter Configuration

Use when: Every project. You always want at least two reporters: one for humans, one for CI. Avoid when: Never -- always configure reporters.

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  reporter: process.env.CI
    ? [
        // CI: machine-readable + human-readable + CI annotations
        ['dot'],                                    // minimal console output
        ['html', { open: 'never' }],               // browsable report (uploaded as artifact)
        ['junit', { outputFile: 'test-results/junit.xml' }],  // CI test tab integration
        ['github'],                                 // PR annotations (GitHub Actions only)
      ]
    : [
        // Local: detailed console + auto-opening report
        ['list'],                                   // verbose console output
        ['html', { open: 'on-failure' }],          // auto-open on failure
      ],
});

Pattern 2: Built-in Reporters in Detail

Use when: Choosing the right reporter for your context.

ReporterOutputBest For
listOne line per test with pass/failLocal development
lineUpdates a single line as tests completeLocal, less verbose
dotSingle dot per test: . pass, F failCI logs (minimal)
htmlInteractive HTML page with tracesPost-run analysis
jsonMachine-readable JSON to stdout or fileCustom tooling, dashboards
junitJUnit XMLCI platforms (Azure DevOps, Jenkins, CircleCI)
githubGitHub Actions annotationsGitHub PRs
blobBinary archive for shard mergingSharded CI runs

JSON reporter -- write to file:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  reporter: [
    ['json', { outputFile: 'test-results/results.json' }],
  ],
});

JUnit reporter -- customize output:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  reporter: [
    ['junit', {
      outputFile: 'test-results/junit.xml',
      stripANSIControlSequences: true,
      includeProjectInTestName: true,
    }],
  ],
});

Pattern 2b: Trace Retention For Flaky Tests (Playwright 1.59+)

Use when: A test sometimes fails and sometimes passes on retry, and you need artifacts from both attempts to compare behavior. Avoid when: Trace size matters more than diagnosis depth. In that case, keep using 'on-first-retry'.

Playwright 1.59 adds a new trace mode, 'retain-on-failure-and-retries', which records each run and keeps all traces when any attempt fails.

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  use: {
    trace: process.env.CI
      ? 'retain-on-failure-and-retries'
      : 'on-first-retry',
  },
});

This is excellent for flaky-test analysis because you can compare the failing run against a passing retry instead of guessing what changed between attempts.

Pattern 2c: Scoped HAR Artifacts For a Flow (Playwright 1.60+)

Use when: You want a network archive (HAR) attached to the report for one specific flow — e.g. a flaky third-party integration — without recording HAR for the entire context. Avoid when: A trace already gives you the network waterfall you need (traces include network detail). Reach for a dedicated HAR only when you need a portable .har to replay or hand to another tool.

Playwright 1.60 adds tracing.startHar() / tracing.stopHar(), which record HAR on demand inside the test. Capture only the suspect flow and attach it to the report:

import { test } from '@playwright/test';

test('payment integration', async ({ context, page }, testInfo) => {
  const harPath = testInfo.outputPath('payment.har');
  await context.tracing.startHar({ path: harPath, urlFilter: '**/payments/**' });

  await page.goto('/checkout');
  await page.getByRole('button', { name: 'Pay' }).click();

  await context.tracing.stopHar();
  await testInfo.attach('payment-network', { path: harPath, contentType: 'application/json' });
});

The attached HAR shows up alongside traces and screenshots in the HTML report and CI artifacts. See core/network-mocking.md for the full API and await using cleanup.

Pattern 3: Custom Reporter

Use when: Built-in reporters don't meet your needs -- you want Slack notifications, database logging, or custom dashboards. Avoid when: A built-in reporter or existing third-party reporter covers your case.

// reporters/slack-reporter.ts
import type {
  FullConfig,
  FullResult,
  Reporter,
  Suite,
  TestCase,
  TestResult,
} from '@playwright/test/reporter';

class SlackReporter implements Reporter {
  private passed = 0;
  private failed = 0;
  private skipped = 0;
  private failures: string[] = [];

  onTestEnd(test: TestCase, result: TestResult) {
    switch (result.status) {
      case 'passed':
        this.passed++;
        break;
      case 'failed':
      case 'timedOut':
        this.failed++;
        this.failures.push(`${test.title}: ${result.error?.message?.split('\n')[0]}`);
        break;
      case 'skipped':
        this.skipped++;
        break;
    }
  }

  async onEnd(result: FullResult) {
    const total = this.passed + this.failed + this.skipped;
    const emoji = this.failed > 0 ? ':red_circle:' : ':large_green_circle:';
    const text = [
      `${emoji} *Playwright Tests*: ${result.status}`,
      `Passed: ${this.passed} | Failed: ${this.failed} | Skipped: ${this.skipped} | Total: ${total}`,
      `Duration: ${(result.duration / 1000).toFixed(1)}s`,
    ];

    if (this.failures.length > 0) {
      text.push('', '*Failures:*');
      this.failures.slice(0, 5).forEach((f) => text.push(`  - ${f}`));
      if (this.failures.length > 5) {
        text.push(`  ...and ${this.failures.length - 5} more`);
      }
    }

    const webhookUrl = process.env.SLACK_WEBHOOK_URL;
    if (webhookUrl) {
      await fetch(webhookUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text: text.join('\n') }),
      });
    }
  }
}

export default SlackReporter;

Register the custom reporter:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  reporter: [
    ['dot'],
    ['html', { open: 'never' }],
    ['./reporters/slack-reporter.ts'],
  ],
});

Pattern 4: Trace File Management

Use when: Debugging test failures. Traces capture a complete timeline of actions, network requests, DOM snapshots, and console logs. Avoid when: Never disable traces entirely in CI -- the on-first-retry setting has minimal overhead.

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  use: {
    // 'on-first-retry': records trace only when a test fails and retries.
    // Minimal overhead on passing tests, full debugging on failures.
    trace: 'on-first-retry',
  },
});

Trace options:

ValueRecords traceWhenOverhead
'off'Never--None
'on'Every testAlwaysHigh (large files)
'on-first-retry'On first retry after failureRetries onlyMinimal
'retain-on-failure'Every test, keeps only failuresFailuresMedium
'retain-on-first-failure'Every test, keeps only first failureFirst failureMedium

Viewing traces:

# Open trace viewer locally
npx playwright show-trace test-results/my-test/trace.zip

# Open trace from HTML report (click "Traces" tab in the report)
npx playwright show-report

# Online trace viewer (upload trace.zip)
# https://trace.playwright.dev

Pattern 5: Screenshot and Video Configuration

Use when: Visual evidence of test failures is valuable for debugging or bug reports. Avoid when: Never disable screenshots in CI -- the on-failure setting is cheap.

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    // Screenshots
    screenshot: 'only-on-failure',  // capture final state on failure

    // Video
    video: 'retain-on-failure',     // record all, keep only failures

    // Video size (optional -- smaller = less disk)
    video: {
      mode: 'retain-on-failure',
      size: { width: 1280, height: 720 },
    },
  },
});

Screenshot options:

ValueCapturesDisk cost
'off'NeverNone
'on'Every test (at end)High
'only-on-failure'Failed tests onlyLow

Video options:

ValueRecordsKeepsDisk cost
'off'Never--None
'on'Every testAllVery high
'on-first-retry'On retryRetried testsLow
'retain-on-failure'Every testFailed onlyMedium

Pattern 6: Artifact Organization for CI

Use when: Keeping test artifacts organized and accessible in CI.

Recommended directory structure:

test-results/             # Playwright's default output directory
├── my-test-chromium/
│   ├── trace.zip         # Trace file
│   ├── test-failed-1.png # Screenshot
│   └── video.webm        # Video recording
├── another-test-firefox/
│   ├── trace.zip
│   └── test-failed-1.png
└── junit.xml             # JUnit report (if configured)

playwright-report/        # HTML report directory
├── index.html
└── data/
    └── ...

blob-report/              # Blob report for shard merging
└── report-1.zip

GitHub Actions artifact upload:

# Upload HTML report (always -- useful even when tests pass)
- uses: actions/upload-artifact@v4
  if: ${{ !cancelled() }}
  with:
    name: playwright-report
    path: playwright-report/
    retention-days: 14

# Upload traces and screenshots (only on failure -- saves storage)
- uses: actions/upload-artifact@v4
  if: failure()
  with:
    name: test-traces
    path: |
      test-results/**/trace.zip
      test-results/**/*.png
      test-results/**/*.webm
    retention-days: 7

Pattern 7: Allure Integration

Use when: Your team uses Allure for test reporting across multiple test frameworks. Avoid when: The built-in HTML reporter meets your needs (it usually does).

# Install Allure reporter
npm install -D allure-playwright
// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  reporter: [
    ['line'],
    ['allure-playwright', {
      detail: true,
      outputFolder: 'allure-results',
      suiteTitle: true,
    }],
  ],
});
# Generate and view Allure report
npx allure generate allure-results -o allure-report --clean
npx allure open allure-report

# Or use Allure CLI
allure serve allure-results

Add Allure metadata to tests:

import { test, expect } from '@playwright/test';
import { allure } from 'allure-playwright';

test('checkout flow', async ({ page }) => {
  await allure.epic('E-Commerce');
  await allure.feature('Checkout');
  await allure.story('Credit Card Payment');
  await allure.severity('critical');

  await page.goto('/checkout');
  // ... test implementation
});

Decision Guide

ScenarioReporter ConfigurationWhy
Local development[['list'], ['html', { open: 'on-failure' }]]Verbose console + auto-opening report on failure
GitHub Actions[['dot'], ['html'], ['github']]Minimal logs + report artifact + PR annotations
GitLab CI[['dot'], ['html'], ['junit']]Minimal logs + report artifact + test tab
Azure DevOps / Jenkins[['dot'], ['html'], ['junit']]JUnit for native test results integration
Sharded CI[['blob'], ['github']]Blob for merging; github for PR annotations
Team uses Allure[['line'], ['allure-playwright']]Cross-framework reporting consistency
Custom dashboard[['json', { outputFile: '...' }]] + custom reporterJSON for data, custom for notifications
ArtifactWhen to CollectRetentionUpload Condition
HTML reportAlways14 daysif: ${{ !cancelled() }}
Traces (.zip)On failure7 daysif: failure()
Screenshots (.png)On failure7 daysif: failure()
Videos (.webm)On failure7 daysif: failure()
JUnit XMLAlways14 daysif: ${{ !cancelled() }}
Blob reportAlways (sharded)1 dayif: ${{ !cancelled() }}

Anti-Patterns

Anti-PatternProblemDo This Instead
No reporter configuredDefault list only; no persistent reportAlways configure html + one CI reporter
trace: 'on' in CIMassive artifacts (50-100 MB per test), slow uploadsUse trace: 'on-first-retry'
video: 'on' in CIEnormous storage cost; slows test executionUse video: 'retain-on-failure'
Only uploading artifacts on failureNo report when tests pass; can't verify resultsUpload with if: ${{ !cancelled() }} (always)
No retention limits on artifactsCI storage fills up within weeksSet retention-days: 7-14
Using only dot reporter with no HTMLCan't drill into failures after the runAlways pair dot with html in CI
JUnit output to stdoutInterferes with console output; hard to parseWrite to file: ['junit', { outputFile: 'results/junit.xml' }]
Custom reporter that blocks onEndSlow Slack/HTTP calls delay pipeline completionUse Promise.race with a timeout in custom reporters

Troubleshooting

HTML report is empty or missing tests

Cause: Another reporter is conflicting, or outputFolder was overridden to a non-default path.

Fix: Check your reporter config. The HTML report defaults to playwright-report/:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  reporter: [['html', { outputFolder: 'playwright-report', open: 'never' }]],
});

Traces are too large for CI artifact upload

Cause: trace: 'on' records every test, even passing ones.

Fix: Switch to 'on-first-retry' and ensure retries > 0 in CI:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  use: {
    trace: 'on-first-retry',
  },
});

JUnit XML not recognized by CI platform

Cause: Output path doesn't match what the CI task expects, or the file is empty.

Fix: Ensure the path matches your CI configuration:

// playwright.config.ts -- the outputFile path
reporter: [['junit', { outputFile: 'test-results/junit.xml' }]],
# GitHub Actions
- uses: dorny/test-reporter@v1
  with:
    path: test-results/junit.xml
    reporter: java-junit

# Azure DevOps
- task: PublishTestResults@2
  inputs:
    testResultsFiles: 'test-results/junit.xml'

# Jenkins
junit 'test-results/junit.xml'

merge-reports produces empty report

Cause: Shards are using html reporter instead of blob. Only blob output can be merged.

Fix: Use blob reporter for sharded runs:

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  reporter: process.env.CI
    ? [['blob'], ['dot']]  // blob for merge, dot for console
    : [['html', { open: 'on-failure' }]],
});

Screenshots not appearing in HTML report

Cause: screenshot: 'off' or screenshots are in test-results/ but not linked to the report.

Fix: Enable screenshots and ensure both directories are available:

use: {
  screenshot: 'only-on-failure',
},

The HTML report automatically embeds screenshots from test-results/. If you move or delete test-results/, screenshots will be missing from the report.