A Playwright test may pass several times on your local machine and then fail unexpectedly in a CI pipeline. Sometimes a button is not clicked, a page takes longer to load, or an element appears just after the test has already timed out.
These failures are frustrating because they are not always caused by a bug in the application. A test can also fail because it depends on timing, network conditions, animations, shared test data, or the state left behind by another test.
This is where Playwright Trace Viewer becomes useful. It lets you inspect a recorded test execution, including actions, page snapshots, network activity, console messages, and other debugging information.
Instead of guessing why a test failed, you can examine what happened during the run and identify the actual cause.
What Is Playwright Trace Viewer?
Playwright Trace Viewer is a debugging tool that opens a recorded test trace in an interactive interface. You can use it to review the sequence of actions performed by a test and inspect the browser state at different points.
A trace can include:
- Test steps and their execution times.
- Browser snapshots before and after actions.
- DOM information and element details.
- Network requests and responses.
- Console messages and page errors.
- Screenshots and other recorded information, depending on the trace configuration.
For example, suppose a test clicks the Login button and then waits for the dashboard heading to appear. The test fails because the heading never appears.
Without a trace, you might increase the timeout or add a delay. With Trace Viewer, you can investigate whether the click happened, whether the login request failed, whether an error appeared, or whether the application displayed a validation message instead.
That evidence helps you fix the cause rather than simply making the test wait longer.
How to Enable Playwright Traces
Playwright Test supports trace collection through its configuration file. The trace option controls when a trace is recorded.
Record traces when a test fails on retry
For most projects, retain-on-failure or on-first-retry is a practical starting point. The right choice depends on whether you want traces for every failed test or primarily for tests that fail and are retried.
Here is an example using playwright.config.ts:
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
use: {
trace: "retain-on-failure",
},
retries: 1,
});With this configuration, Playwright retains traces for failed tests while avoiding the storage cost of retaining a trace for every successful run.
The retries setting allows a failed test to run again. This is useful for diagnosing intermittent failures, but a test that passes on retry is still potentially flaky and should not automatically be considered healthy.
If you specifically want to capture a trace on the first retry, you can use:
import { defineConfig } from "@playwright/test";
export default defineConfig({
use: {
trace: "on-first-retry",
},
retries: 1,
});This records the retry attempt, which is often helpful because the first execution may fail without enough diagnostic information.
Choose one trace mode based on your debugging needs rather than enabling every recording option permanently.
Run a Test and Generate a Trace
Suppose your test checks whether a user can log in successfully.
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("correct-password");
await page.getByRole("button", { name: "Login" }).click();
await expect(
page.getByRole("heading", { name: "Dashboard" })
).toBeVisible();
});The test uses accessible locators to find the email field, password field, button, and dashboard heading.
If the test fails, the trace can help you determine which step caused the problem.
For a local debugging session, you can also enable tracing explicitly:
npx playwright test --trace onThis is useful when investigating a particular test. Be aware that --trace on can generate more artifacts than failure-focused modes, so use it selectively when debugging larger suites.
How to Open a Playwright Trace
After a test run, locate the trace artifact generated by Playwright. The exact file location depends on your reporter, configuration, and whether the test ran locally or in CI.
If you have a trace file, commonly named trace.zip, open it with:
npx playwright show-trace path/to/trace.zipPlaywright opens Trace Viewer in a browser window.
If your CI pipeline uploads traces as downloadable test artifacts, download the relevant artifact and open the trace locally.
You do not need to rerun the original test just to inspect an existing trace. This is especially useful when a failure happens only on a remote CI runner.
How to Read Trace Viewer
Trace Viewer provides several useful views. The most effective approach is to start with the failed test step and work backward through the evidence.
1. Find the failed action
Start with the test's action list and identify the first step that failed or timed out.
For example, you might see that the click on the Login button completed, but the assertion for the Dashboard heading timed out.
That tells you the failure occurred after the click. It does not yet prove that the click was the problem.
Inspect the preceding steps and the browser state immediately before the assertion failed.
2. Inspect DOM snapshots
Snapshots let you examine what the page looked like at particular points during the test.
If the expected heading is missing, check what the page actually contains. The application might be showing a validation error, a loading indicator, an unexpected redirect, or an empty state.
This is often more useful than looking only at a screenshot because DOM information helps you understand which elements existed and how Playwright located them.
3. Review network requests
A UI failure can be caused by a failed backend request.
For example, a login request might return an HTTP 401 response because the test account has expired credentials. The dashboard assertion then fails because the user never reaches the dashboard.
Inspect the relevant request and response in the trace. Check its status, timing, and available request or response details.
Do not assume every HTTP error is a test failure. Some applications intentionally return errors for certain requests, so compare the result with the expected application behavior.
4. Check console messages and page errors
JavaScript errors can prevent the application from updating correctly.
Look for exceptions, rejected promises, and other console messages around the time of the failure. A component might throw an exception after a button click, leaving the page in a state that the test does not expect.
Trace Viewer can help you correlate these messages with the failed test step.
5. Compare before and after snapshots
When an action completes but the expected result does not appear, compare the page snapshots before and after that action.
Ask these questions:
- Did the click trigger the expected change?
- Did the URL change?
- Did a loading indicator appear and disappear?
- Did a dialog open?
- Did an error message replace the expected content?
These observations help distinguish a timing problem from an application or test-data problem.
Debug a Flaky Test Without Adding Arbitrary Delays
One common reaction to a flaky test is to add waitForTimeout().
For example:
await page.getByRole("button", { name: "Save" }).click();
await page.waitForTimeout(3000);
await expect(
page.getByText("Changes saved")
).toBeVisible();This approach is unreliable. If the application needs more than three seconds, the test can still fail. If the application finishes in 100 milliseconds, the test wastes time.
Prefer waiting for the condition that matters:
await page.getByRole("button", { name: "Save" }).click();
await expect(
page.getByText("Changes saved")
).toBeVisible();Playwright's web-first assertions automatically retry until the expected condition is met or the assertion times out.
If the operation depends on a specific backend request, you can wait for that response explicitly:
const saveResponse = page.waitForResponse(
response =>
response.url().includes("/api/profile") &&
response.request().method() === "PUT"
);
await page.getByRole("button", { name: "Save" }).click();
const response = await saveResponse;
expect(response.ok()).toBeTruthy();
await expect(
page.getByText("Changes saved")
).toBeVisible();Registering the response wait before clicking avoids missing a fast response. Adjust the URL and HTTP method to match your application.
The test now checks both the backend result and the visible UI outcome.
Debugging Tests That Fail Only in CI
A test may pass locally but fail in CI because the environments are different.
Common causes include slower machines, different browser versions, parallel execution, missing environment variables, unavailable services, and shared test data.
Trace Viewer helps narrow down the problem, but you should also check the environment in which the trace was captured.
Check test isolation
Tests should not depend on another test running first.
For example, one test might create a user while another test assumes that the same user already exists. Parallel execution can change their order and produce intermittent failures.
Give each test independent data where possible, clean up records when appropriate, and avoid shared mutable state.
Check the actual failure location
If a test fails while navigating to a page, inspect the navigation and network activity. If it fails during an assertion, inspect the relevant DOM snapshot.
Do not change timeouts until you understand which operation is taking longer than expected.
Check retries carefully
Retries are useful for collecting evidence, but they can hide instability if the team looks only at the final result.
Track tests that fail on the first attempt and pass on a retry. A test that behaves differently across identical runs deserves investigation.
Upload Traces from GitHub Actions
When a test fails in CI, saving the Playwright report and trace artifacts allows developers to investigate the failure without reproducing it immediately on their own machines.
For example, a GitHub Actions workflow can upload the test-results directory:
- name: Run Playwright tests
run: npx playwright test
- name: Upload Playwright results
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-results
path: |
playwright-report/
test-results/
retention-days: 7This example assumes your Playwright configuration writes its report and trace artifacts to those directories. Adjust the paths to match your project.
Keep artifact retention appropriate for your team's debugging needs. Traces can contain screenshots, page content, URLs, and other information that should not be exposed publicly. Restrict access to CI artifacts and avoid using real credentials or sensitive production data in tests.
Common Mistakes When Using Trace Viewer
| Mistake | Why it causes problems | Better approach |
|---|---|---|
| Increasing timeouts without investigating | Hides the actual failure and slows tests | Inspect the failed step and its surrounding evidence |
| Adding fixed sleeps | Makes tests depend on arbitrary timing | Use web-first assertions and condition-based waits |
| Keeping traces for every run indefinitely | Consumes storage and may expose sensitive data | Choose an appropriate trace mode and retention period |
| Ignoring network failures | Blames the UI for a backend or environment problem | Inspect relevant requests and responses |
| Relying on retries alone | Allows intermittent failures to remain unnoticed | Track and fix tests that fail on retry |
| Sharing trace artifacts publicly | Can reveal page content or sensitive test information | Restrict artifact access and use safe test data |
Best Practices for Reliable Playwright Debugging
Follow these practices to make debugging faster and keep the test suite stable:
- Enable failure-focused tracing. Use a trace mode that captures enough evidence without producing unnecessary artifacts.
- Start with the first meaningful failure. The earliest failing action often provides more useful information than later cascading failures.
- Inspect the DOM and network together. A missing element may be the result of a failed request rather than a locator issue.
- Prefer accessible locators. Use roles, labels, and meaningful accessible names where possible.
- Wait for real conditions. Use Playwright assertions and explicit response waits instead of arbitrary delays.
- Keep tests independent. Avoid shared mutable data and hidden dependencies between tests.
- Review retry patterns. A passing retry is evidence of possible instability, not proof that the test is reliable.
- Protect debugging artifacts. Traces can reveal information that should remain within your development team.
Conclusion
Playwright Trace Viewer makes flaky browser tests easier to investigate by showing the sequence of actions, page snapshots, network activity, and other details from a test run.
Instead of repeatedly rerunning a failed test or adding longer delays, start with the failed step and inspect the evidence around it. Determine whether the cause is a locator problem, a timing issue, an application error, a failed request, or shared test data.
Combine tracing with reliable assertions, independent test data, and CI artifact retention. This approach helps you find the real cause of intermittent failures and build a test suite that developers can trust.

Join the conversation! Your thoughts help the community grow.