Built-in HTML Reporter and Trace Viewer

8 min read

The work isn't done when the test fails — it's done when you understand why. Playwright ships two tools that turn a failing test from a mystery into a five-minute investigation: the HTML reporter (a rich, browseable summary of every run) and the trace viewer (a complete time-travel recording of the test, including DOM snapshots, network calls, and console logs). Together they replace the "add console.log, push to CI, wait, repeat" loop that defines so much of testing life. This lesson is the report layout, the trace recording config, the workflow for opening a CI failure on your laptop, and why the trace viewer alone is enough reason to choose Playwright over almost anything else.

The HTML report

Run any test and Playwright writes a playwright-report/ folder next to your project. Open it with one command:

npx playwright test
npx playwright show-report

show-report boots a tiny local server and opens the report in your browser. The landing page lists every test with status (pass/fail/flaky/skipped), duration, browser, and the file/line. You can filter by status, browser, or file — useful when a CI run produces 800 results and three failed.

Click any test to drill in: error message, stack trace, the assertion's expected vs actual values, attached screenshots and videos, console output, and — when configured — a link to the trace.

To control where and when the report opens, configure the reporter:

// playwright.config.ts
import { defineConfig } from "@playwright/test";
 
export default defineConfig({
  reporter: [
    ["html", { open: "never", outputFolder: "playwright-report" }]
  ]
});

open: 'never' is the right setting for CI — without it, Playwright tries to open a browser window on the runner, which silently fails and slows the job.

Enabling the trace

The trace is the killer feature. By default Playwright runs without one because tracing has overhead — but a single line in use flips it on:

// playwright.config.ts
import { defineConfig } from "@playwright/test";
 
export default defineConfig({
  use: {
    trace: "on-first-retry"
  }
});

The valid trace modes:

  • "off" — no trace recorded (default).
  • "on" — record every test. High overhead. Useful in development, painful in CI.
  • "retain-on-failure" — record everything, keep the trace only if the test fails.
  • "on-first-retry" — record only when a test is being retried. The right CI default: zero overhead on green, full trace the moment something breaks.

The trace is a .zip file written into test-results/<test-name>/trace.zip. Playwright also embeds it into the HTML report — you don't have to track files manually.

Opening a trace

Two paths. From the HTML report: click the failed test, scroll to "Trace", click. The trace viewer opens in a new tab. From the command line:

npx playwright show-trace test-results/login-test/trace.zip

Both routes open the same UI:

  • Timeline (top) — every action as a labelled bar. Click any action to jump to that moment.
  • Action panel (left) — what action ran, its parameters, how long it took, whether it succeeded.
  • DOM snapshot (centre) — the page exactly as it looked at that moment. Scroll, hover, click — it's a fully interactive snapshot, not a screenshot. You can right-click → Inspect Element on the failing locator.
  • Network tab — every request the page made during the test, with status, headers, response body, and timing.
  • Console tab — every console.log, warning, and error from the page during the test.
  • Source tab — the test code with the current line highlighted.
  • Before/after — for each action, two DOM snapshots: the page before the action and the page after.

A failing test goes from "what happened?" to "I see exactly what happened" in about a minute.

What the trace viewer looks like

A real workflow

CI is red. The "transfer funds" test failed on Firefox. Here's the loop:

  1. In GitHub Actions, click the failed job → Artifacts → download playwright-report.zip.
  2. Unzip locally → cd playwright-report && npx http-server (or simply open index.html).
  3. Find the failing test in the list, click it.
  4. Click the Trace link.
  5. The timeline shows the test up to "Click Confirm" succeeded. The next assertion failed.
  6. Click the assertion in the timeline. Centre panel shows the page; expected text "Transfer complete" isn't there — the page is on a "Confirm OTP" step.
  7. Network tab shows the /transfer request returned 202 Accepted, not 200 — the API now requires an OTP step Firefox handles slightly differently from Chromium.
  8. Fix: add the OTP handler in the test or update the page object.

That whole loop is five minutes. Without the trace it would be: re-run locally, fail to reproduce because your environment differs, add logs, push, wait for CI, repeat for half a day.

Recording without a test failure

You can capture a trace any time, not just on retry:

import { test } from "@playwright/test";
 
test("manual trace", async ({ page, context }) => {
  await context.tracing.start({ screenshots: true, snapshots: true, sources: true });
 
  await page.goto("https://www.saucedemo.com");
  await page.getByPlaceholder("Username").fill("standard_user");
  await page.getByPlaceholder("Password").fill("secret_sauce");
  await page.getByRole("button", { name: "Login" }).click();
 
  await context.tracing.stop({ path: "trace.zip" });
});

Useful for debugging non-test scripts or recording reproductions to share with the team.

Coming from Cypress?

Cypress's time-travel debugger pioneered the "see the page at every step" idea, and for years it was the differentiator. Playwright's trace viewer is the same idea taken further:

  • Cypress time-travel works only in the open Cypress runner (interactive mode), not for CI failures.
  • Cypress's CI dashboard requires Cypress Cloud (paid) to view runs after-the-fact.
  • Playwright traces are zip files — download, open, share via Slack. No service required.
  • Playwright's network tab shows every HTTP call; Cypress shows only what cy.intercept saw.
  • Playwright's DOM snapshots are fully interactive; Cypress's are static images at runner-time.

Trace viewer is the single biggest reason teams migrate from Cypress to Playwright once their CI pipelines mature.

⚠️ Common mistakes

  • Setting trace: 'on' in CI. Every test gets full tracing — your CI runtime doubles or triples and the artefact size balloons. Use 'on-first-retry' for zero overhead on green and full information on red.
  • Forgetting open: 'never'. Without it, Playwright tries to launch a browser on the CI runner; the job either hangs briefly or wastes a chunk of runtime. Always pass { open: 'never' } in CI.
  • Not uploading the report as a CI artefact. A trace that exists only in /tmp on a destroyed runner helps no one. Wire up actions/upload-artifact for playwright-report/ so failures are debuggable from anywhere.

🎯 Practice task

Catch a failure with the trace viewer end-to-end. 25 minutes.

  1. Configure tracing and the HTML reporter:

    import { defineConfig } from "@playwright/test";
     
    export default defineConfig({
      testDir: "./tests",
      retries: process.env.CI ? 1 : 0,
      reporter: [["html", { open: "never" }]],
      use: {
        trace: "on-first-retry"
      }
    });
  2. Write a deliberately-broken test:

    import { test, expect } from "@playwright/test";
     
    test("login button shows the wrong text (deliberate fail)", async ({ page }) => {
      await page.goto("https://www.saucedemo.com");
      await page.getByPlaceholder("Username").fill("standard_user");
      await page.getByPlaceholder("Password").fill("secret_sauce");
      await page.getByRole("button", { name: "LOGIN_TYPO" }).click();
      await expect(page.getByText("Products")).toBeVisible();
    });
  3. Run with retries to force a trace: npx playwright test --retries=1.

  4. npx playwright show-report → click the failed test → click Trace.

  5. In the trace viewer, find the action that failed. Inspect the DOM snapshot to confirm the actual button text. Fix the test (name: "Login") and rerun.

  6. Stretch: wire up a GitHub Actions workflow that uploads playwright-report/ as an artefact. Push a deliberately-broken test, download the artefact, and replay the failure on your laptop.

The HTML report and trace viewer cover 80% of debugging needs. The next lesson plugs additional reporters into the same pipeline — JUnit for Jenkins, Allure for stakeholder dashboards, and a custom Slack reporter so your team hears about failures the moment they happen.

// tip to track lessons you complete and pick up where you left off across devices.