A browser test may pass several times and then fail without any changes to the application code. One run might find a button immediately, while another reports that the element is not visible. A test may also fail because a network request takes longer than expected or a page updates after the test has already checked its contents.
These tests are commonly called flaky tests. They make CI/CD pipelines unreliable because developers cannot always tell whether a failure indicates a real application bug or a problem with the test itself.
Playwright provides the Trace Viewer to help investigate these failures. A trace records information about a test run, including actions, screenshots, page snapshots, network activity, and timing details. Developers can inspect the recorded sequence instead of trying to reproduce the failure from a short error message.
This article explains how to capture Playwright traces, inspect failed tests, identify common causes of flaky behavior, and make browser tests more reliable.
What Is the Playwright Trace Viewer?
The Playwright Trace Viewer is a debugging tool that lets you examine a recorded browser test.
Depending on the trace configuration, it can show:
The sequence of test actions.
Screenshots and DOM snapshots associated with actions.
Locator details and action results.
Console messages and errors.
Network requests and responses.
Timing information for each operation.
This information helps answer practical questions. Did the button exist when the test tried to click it? Was an overlay covering the element? Did the application return an error from an API request? Did the test assert the page state before the application finished updating?
A trace is particularly useful when a failure occurs in CI but cannot be reproduced consistently on a developer's machine.
Step 1: Configure Playwright to Record Traces
Playwright's test runner supports trace recording through its configuration file.
For example, a playwright.config.ts file can contain:
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
use: {
trace: 'on-first-retry'
},
retries: 1
});
The trace: 'on-first-retry' setting records a trace when a test is retried for the first time after a failure.
This is a useful default for CI because traces are not recorded for every successful test run. That can reduce storage usage and the overhead associated with collecting diagnostic data.
Playwright supports several trace modes:
off: Do not record traces.
on: Record a trace for each test run.
on-first-retry: Record a trace on the first retry after a failure.
retain-on-failure: Keep traces for failed tests and discard them for successful tests.
on: Can also be configured with additional trace options when more detailed recording is needed.
Choose a mode that fits your debugging requirements and the volume of your test suite. Recording every test can be useful during a short investigation, but it may generate a large amount of diagnostic data in a large CI pipeline.
Step 2: Write a Test That Can Be Diagnosed
Consider a login page where a test enters credentials and expects to see a dashboard.
import { test, expect } from '@playwright/test';
test('user can log in', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('test-password');
await page.getByRole('button', {
name: 'Sign in'
}).click();
await expect(
page.getByRole('heading', {
name: 'Dashboard'
})
).toBeVisible();
});
The test uses accessible locators based on labels and roles. These are generally more resilient than selectors tied to implementation details such as deeply nested CSS classes.
The final assertion uses Playwright's web-first assertion. It automatically retries the visibility check until the expected condition is met or the assertion timeout expires.
This is more reliable than checking visibility immediately after clicking the button.
However, a retrying assertion does not fix every synchronization problem. If the login request fails, the page redirects incorrectly, or the dashboard never appears, the test should fail. The trace helps reveal which condition did not occur.
Step 3: Run the Test and Capture a Trace
Run the test using Playwright's test runner:
npx playwright test
With trace: 'on-first-retry', the trace is collected when Playwright retries a failed test. If the test passes on the first attempt, no trace is recorded for that run under this configuration.
To investigate a specific test, you can run it with a trace enabled:
npx playwright test tests/login.spec.ts --trace on
This enables tracing for the test run and is useful during local debugging.
After the test finishes, Playwright stores the trace in its test output directory. The exact path depends on your test configuration and output settings.
Avoid assuming that every run produces the same trace path. If you are debugging in CI, configure your workflow to preserve the generated test results and trace files as artifacts.
Step 4: Open a Trace in the Trace Viewer
You can open a trace file using the Playwright CLI:
npx playwright show-trace path/to/trace.zip
Replace the example path with the actual trace file.
The Trace Viewer opens an interface that lets you move through the test's recorded actions. Select an action to inspect the associated timing, page state, and other available details.
For example, if the test fails while clicking the Sign in button, inspect the action and the snapshot captured around that point.
Check whether:
The button was present in the page.
The locator matched the expected element.
The button was enabled and visible.
Another element covered the button.
The application was still loading.
A JavaScript error or failed request occurred before the action.
These details can narrow the investigation from a general timeout to a specific application or test problem.
Step 5: Inspect the Timeline and DOM Snapshots
The action timeline shows the order of operations and how much time they consumed.
Suppose a test spends several seconds waiting for a dashboard heading. The trace may show that the click succeeded but the expected heading never appeared.
That result suggests that the problem occurred after the click rather than during element selection.
The DOM snapshots provide a view of the page at particular points in the test. Compare the snapshot before the click with the state after the action.
For example, the application may have displayed a validation error because the test credentials were invalid. The test then waited for a dashboard that could never appear.
Alternatively, the server may have returned an error, leaving the page in a loading state.
The trace helps distinguish these situations without adding arbitrary delays to the test.
Step 6: Investigate Network Requests
Browser tests often depend on API responses. A page may remain in a loading state when an endpoint is slow or returns an error.
The Trace Viewer includes network information that can help identify failed requests, unexpected response statuses, and timing problems.
For example, a dashboard test may depend on /api/dashboard. If the endpoint returns an HTTP 500 response, the UI might display an error message instead of the expected dashboard.
The test should account for the expected application behavior. If the endpoint is part of the real integration being tested, the failure may indicate an application or environment issue.
If the purpose of the test is to verify the UI independently of a third-party service, consider mocking that dependency with Playwright's routing features.
For example:
import { test, expect } from '@playwright/test';
test('dashboard displays account data', async ({ page }) => {
await page.route('**/api/account', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
name: 'Sample User',
status: 'Active'
})
});
});
await page.goto('/dashboard');
await expect(
page.getByText('Sample User')
).toBeVisible();
await expect(
page.getByText('Active')
).toBeVisible();
});
This test supplies a predictable response for the account endpoint. It reduces dependence on the real API, making the UI test easier to reproduce.
Mocking should match the test's purpose. A UI test that mocks every backend operation does not verify that the application integrates correctly with those services. Keep separate integration tests for important real dependencies.
Step 7: Fix Common Causes of Flaky Tests
Replace fixed delays with conditions
A common source of flakiness is using waitForTimeout() to guess when an operation will finish.
For example:
await page.waitForTimeout(3000);
await expect(page.getByText('Saved')).toBeVisible();
The fixed delay may be unnecessarily long on a fast machine and insufficient on a slow one.
Prefer waiting for the actual condition:
await expect(
page.getByText('Saved')
).toBeVisible();
Playwright's assertion retries until the condition is met or the timeout expires.
For navigation, requests, or other events, use the appropriate event or state-based waiting mechanism. Avoid waiting for networkidle by default when the application uses polling, analytics, or long-lived connections, because the page may never reach a fully idle network state.
Use reliable locators
Selectors based on unstable CSS classes or the exact position of an element can break when the UI changes.
Prefer roles, labels, and other meaningful attributes:
await page.getByRole('button', {
name: 'Save changes'
}).click();
If the page contains multiple matching elements, narrow the locator to the appropriate section or container rather than relying on an arbitrary index.
Make test data predictable
Tests can fail when they share records or modify the same account concurrently.
Use isolated test data, unique identifiers where appropriate, and reliable cleanup. Avoid depending on a particular database state unless the test explicitly prepares that state.
Check for overlays and animations
A modal, loading indicator, or animation may prevent an element from being clicked. The trace can show whether the element was blocked or whether the click timed out.
Wait for the relevant UI state to complete. Do not use forced clicks as a general solution because they can hide genuine usability problems.
Separate product failures from test failures
A test that consistently fails because an API returns an error may be exposing a real defect. Repeating the test until it passes does not fix that defect.
Use the trace to identify whether the failure comes from application behavior, environment instability, test data, or synchronization.
Step 8: Preserve Traces in GitHub Actions
When browser tests run in CI, retain the Playwright test results so developers can investigate failures after the job finishes.
For example:
name: Playwright Tests
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run tests
run: npx playwright test
- name: Upload Playwright results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-results
path: test-results/
if-no-files-found: ignore
retention-days: 7
This workflow uploads the contents of test-results/ when the job has not been cancelled. Ensure your Playwright configuration writes traces and other diagnostic files into that directory.
The example uses action version tags for readability. Production workflows should review third-party actions and consider pinning them to verified full commit SHAs.
The retention period is configurable. Keep artifacts only as long as needed for debugging and compliance, especially if traces might contain sensitive page data.
Security Considerations When Sharing Traces
Trace files can contain more than timing information. Depending on the application and recording configuration, they may include page snapshots, URLs, request details, response data, and other information from the test environment.
Do not upload traces containing real passwords, session tokens, personal information, or production customer data to publicly accessible locations.
Use synthetic test accounts, restrict access to CI artifacts, and define an appropriate retention period. Before sharing a trace with another team, inspect it for sensitive information.
Conclusion
Playwright's Trace Viewer helps developers understand why browser tests fail intermittently. By inspecting action timing, DOM snapshots, network requests, and error messages, you can identify the actual cause instead of adding arbitrary delays or repeatedly rerunning the test.
Enable trace collection for failed retries, preserve test artifacts in CI, and use the recorded evidence to improve synchronization, locators, test data, and application error handling.
Reliable browser tests depend on waiting for meaningful conditions and controlling their dependencies. The Trace Viewer makes those problems easier to investigate and helps keep CI/CD pipelines dependable.
Join the conversation! Your thoughts help the community grow.