Playwright & Web Automation Hub

Playwright architecture, selector reliability, and advanced interaction patterns.

Analyzing Test Failures with the Playwright Trace Viewer

A CI job goes red with expect(locator).toBeVisible() failed. The stack trace tells you which assertion broke but not why — and the failure does not reproduce on your machine. This is the exact situation the Playwright Trace Viewer was built for. A trace recorded on the failing run is a complete recording of what the browser did, so you diagnose the failure that already happened instead of trying to recreate it. This walkthrough takes a red run from artifact to root cause using nothing but the trace. It sits under Trace Viewer & Debugging, part of the wider Debugging & Test Observability guide.

From red CI run to root cause A failed run produces a trace.zip that is opened, scrubbed to the failing action, and read across DOM and network panels to find the cause. Red run trace.zip Open viewer show-trace Scrub to red action DOM snapshot was it there? Network panel 200 or 500? Root cause fix + verify
The diagnosis path is linear: open the trace, jump to the failing action, then read DOM and network to name the cause.

Root cause: the failure is invisible from the log alone

A bare assertion failure conflates many causes into one message. toBeVisible() failed can mean the element never rendered, rendered then was removed, rendered off-screen, was covered by an overlay, or rendered after the locator's timeout because the API that feeds it was slow. The log cannot tell these apart, so any fix you guess at is a coin flip. The trace removes the guessing: it holds the DOM at the instant the assertion ran and the network activity that preceded it, which is exactly the evidence needed to distinguish "the element was never there" from "the data arrived too late." The procedure below converts that evidence into a root cause every time.

Minimal reproducible example

Here is a test that fails intermittently in CI. It asserts a confirmation banner after submitting an order, but the banner depends on a /api/orders POST that is sometimes slow.

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

test('order submission shows confirmation', async ({ page }) => {
  await page.goto('/checkout');
  await page.getByLabel('Card number').fill('4242424242424242');
  await page.getByRole('button', { name: 'Place order' }).click();
  // This is the assertion that fails in CI when the POST is slow.
  await expect(page.getByText('Order confirmed')).toBeVisible();
});

To make sure a trace exists for the failure, configure capture on retry so CI records evidence automatically. This is the same setting described in Playwright Config & Fixtures.

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

export default defineConfig({
  retries: process.env.CI ? 2 : 0,
  use: {
    // Record a full trace the first time a failing test is retried.
    trace: 'on-first-retry',
  },
});

Step-by-step fix

  1. Locate and download the trace artifact. From the failed CI job, download trace.zip (under test-results/<test>/). If your pipeline publishes the HTML report, the failed test row links to its trace directly. If no trace exists, the run was not configured with trace: 'on-first-retry' — fix that first, then rerun.
  2. Open the trace in the viewer. Run npx playwright show-trace path/to/trace.zip, or drag the file onto trace.playwright.dev, which parses it entirely client-side. The viewer opens to the action timeline with the failing run loaded.
  3. Jump to the failing action. In the Actions list on the left, find the entry highlighted in red — the expect(getByText('Order confirmed')).toBeVisible() call. Click it to pin every panel to the moment that assertion timed out.
  4. Read the DOM snapshot at the failure. Open the Action/After snapshot tab and search the rendered DOM for the banner text. If "Order confirmed" is absent, the element never rendered — a data or logic problem, not a selector problem. If it is present but the locator did not match, the selector is wrong; revisit Reliable Selector Strategies for Playwright.
  5. Check the network panel for the triggering request. Open the Network tab and find the POST /api/orders issued by the click. Inspect its status and timing. A pending or 200-but-late response that resolves after the assertion's timeout is the smoking gun: the UI rendered the banner a few hundred milliseconds after the locator gave up.
  6. Apply the fix that matches the evidence. Because the snapshot showed the banner missing and the network showed a slow POST, the cause is a race, not a bug. Replace the implicit wait with an explicit wait on the response so the assertion only runs once the data has arrived — see below.
  7. Verify with a repeated run and a fresh trace. Rerun with npx playwright test --repeat-each=10 --trace on and confirm the test is now stable and the new trace shows the assertion firing after the POST resolves.

The fix that step 6 points to synchronizes the assertion with the network instead of with the clock:

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

test('order submission shows confirmation', async ({ page }) => {
  await page.goto('/checkout');
  await page.getByLabel('Card number').fill('4242424242424242');
  // Wait for the POST to resolve before asserting on the banner it produces.
  const orderResponse = page.waitForResponse('**/api/orders');
  await page.getByRole('button', { name: 'Place order' }).click();
  await orderResponse; // gate the assertion on the real network event
  await expect(page.getByText('Order confirmed')).toBeVisible();
});

Troubleshooting variants

The failing action snapshot looks correct

If the DOM snapshot shows the element present and visible, the assertion likely failed on a transient state that the After snapshot already moved past. Use the Before snapshot and the timeline filmstrip to inspect the instant the locator's timeout expired, and check whether an overlay or animation covered the element. Auto-waiting assertions retry, so the relevant snapshot is the last attempt before the timeout, not the final page state.

There is no network entry for the request you expected

The request may have been served by a route handler. The Network tab flags fulfilled requests; if you mocked the endpoint per Network Interception Basics, confirm the mock returned the shape the UI expects. A mock that drifts from the real contract produces a green-looking request and a missing banner.

The trace opens but panels are empty

The trace was captured without snapshots or resources. If you started tracing manually, pass snapshots: true and screenshots: true to context.tracing.start(); otherwise switch to the config-driven trace: 'on-first-retry', which records everything needed.

Verification

Confirm the root cause is fixed three ways. First, npx playwright test --repeat-each=20 passes with zero flakes. Second, open the new trace and confirm the POST /api/orders resolves before the toBeVisible() action on the timeline. Third, artificially slow the endpoint (throttle in DevTools or add latency to a mock) and confirm the test still passes because it now waits on the response rather than a fixed timeout. A test that survives an injected delay has had its race genuinely removed, not merely papered over.

Frequently Asked Questions

Where does Playwright save the trace for a failed test?

By default under the output directory at test-results/<test-name>/trace.zip. When the HTML reporter is enabled, each failed test in the report links directly to its trace, and in CI you typically publish that directory as a job artifact so the trace is one click from the failed pipeline.

How do I know whether a failure is a race or a real bug from the trace?

Compare the DOM snapshot at the failing action with the network panel. If the element is absent and a feeding request resolved after the assertion's timeout, it is a race — fix it by waiting on the response. If the element is present but the locator did not match, or the request returned an error, it is a selector or backend bug.

Can I analyze a trace without installing Playwright?

Yes. Drag the trace.zip onto trace.playwright.dev, which runs the full viewer in your browser without uploading the file anywhere. This is handy for sharing a failure with a teammate who does not have the project checked out.

Back to overview