Debugging Failed Playwright Tests Like a Pro
Failed tests are not problems — they're clues. Learn how to use Playwright's UI mode, trace viewer, headed debugging, and artifact analysis to isolate flaky steps, understand root causes, and fix.
This post is part of the Playwright Essentials series. If you missed the previous post, read Part 8 — Parallel Runs, Sharding, and CI-Ready Playwright first.
Introduction — Debugging Is a Skill, Not a Guess
The worst way to debug a failing test is to stare at the code, change something that “might help,” re-run the test, and repeat until it passes. This is slow, demoralising, and teaches you nothing about the root cause.
The best way to debug a failing test is to observe what actually happened. Playwright gives you tools to see exactly what the browser did, what the test expected, and where the mismatch occurred. With these tools, debugging becomes systematic: gather evidence, form a hypothesis, verify the hypothesis, fix the root cause.
This post will show you how to use Playwright’s debugging tools:
- UI mode — interactive test explorer with live browser preview
- Trace viewer — time-travel debugger with full network, DOM, and action history
- Headed mode — watch tests run live with browser DevTools
--debugmode — step through tests line-by-line like a traditional debugger- Artifact analysis — screenshots, videos, and logs from CI failures
By the end, you’ll know how to diagnose test failures faster than anyone on your team.
UI Mode: The Interactive Test Explorer
UI mode is Playwright’s best-kept secret. It’s an interactive test runner that shows you:
- A live browser preview as the test runs
- A timeline of every action and assertion
- Network requests, console logs, and DOM snapshots
- Real-time pass/fail status for every step
Start UI mode:
npx playwright test --ui
This opens a browser-based interface. Select a test from the sidebar, click Run, and watch it execute step-by-step. If a step fails, the timeline highlights the failure, and you can inspect the DOM state at that exact moment.
When to Use UI Mode
Use UI mode when:
- Writing a new test — Run it interactively to verify each step works as expected
- Debugging a local failure — See exactly what the browser is doing at each step
- Isolating a flaky test — Run the test 10 times in UI mode and observe when/where it fails
Key Features
1. Action timeline: Every page.goto(), page.click(), expect() is logged. Click any action to see the DOM state at that moment.
2. Network inspector: See every HTTP request, status code, and response payload. If your test fails because an API returns 500, you’ll see it here.
3. Console logs: If the application logs errors to the console, they appear in the UI mode sidebar. No need to check the terminal.
4. Slow motion: Slow down test execution to see animations, loading states, and transitions that happen too fast to observe normally.
Trace Viewer: The Time-Travel Debugger
The trace viewer is the most powerful debugging tool in Playwright. A trace is a complete recording of the test execution: every action, every network request, every DOM snapshot, every console log. You can step forward and backward through the test as if you were there when it ran.
Generating Traces
Configure trace collection in playwright.config.ts:
export default defineConfig({
use: {
trace: 'on-first-retry',
},
});
This generates traces only for tests that fail and are retried. Traces are large (~2–10 MB each), so don’t generate them for every test.
If a test fails locally and you want to inspect the trace:
npx playwright test --trace on
Opening the Trace Viewer
After a test fails, Playwright saves the trace to test-results/. Open it:
npx playwright show-trace test-results/<test-name>/trace.zip
Or if you’ve uploaded traces from CI, download the artifact and run:
npx playwright show-trace trace.zip
What You Can See in the Trace
1. Action timeline: A full list of every action the test performed. Click any action to see:
- The DOM state before the action
- The DOM state after the action
- The locator used
- The action’s duration
2. Screenshots at every step: The trace includes a screenshot before and after each action. If a button moved or disappeared, you’ll see it.
3. Network tab: Every network request with:
- Request URL, method, headers, payload
- Response status, headers, body
- Timing information (how long the request took)
4. Console logs: Every console.log(), console.error(), and console.warn() from the application.
5. Snapshots: Click “Before” or “After” on any action to see a full DOM snapshot. You can inspect elements, check styles, and verify the application state.
Example: Debugging a Flaky Login Test
Imagine this test fails intermittently:
test('user can log in', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button:has-text("Log in")');
await expect(page).toHaveURL('/dashboard');
});
It passes locally but fails in CI. Open the trace from CI:
- Action timeline shows: The test clicked “Log in” and waited for the URL to be
/dashboard - Network tab shows: The login POST request returned 200, but the response took 8 seconds
- Snapshot at failure shows: The page is still on
/login, showing a loading spinner
Root cause: The default timeout (30 seconds) expired because the server took too long to respond. The test is correct, but the application is slow.
Fix: Either optimize the server or increase the timeout for this specific action:
await page.click('button:has-text("Log in")');
await expect(page).toHaveURL('/dashboard', { timeout: 60000 }); // 60 seconds
Without the trace, you’d have guessed. With the trace, you knew.
Headed Mode: Watch Tests Run Live
By default, Playwright runs in headless mode (no visible browser). When debugging, it’s useful to see the browser:
npx playwright test --headed
This opens a real browser window and runs the test inside it. You can watch every action in real time.
When to Use Headed Mode
- Debugging visual issues — Is the button really hidden, or just off-screen?
- Understanding timing issues — Are animations interfering with clicks?
- Verifying user flows — Does the test match how a real user would interact with the page?
Slow Motion
Combine headed mode with slow motion to slow down test execution:
npx playwright test --headed --slow-mo=1000
This adds a 1-second delay between each action. Useful for watching complex interactions unfold.
Debug Mode: Step Through Tests Line-by-Line
Debug mode pauses test execution at the start and opens Playwright Inspector, a built-in step-through debugger:
npx playwright test --debug
The inspector lets you:
- Step over — Execute the next line and pause
- Step into — If the next line is a function call, step into it
- Resume — Run until the next breakpoint or test end
You can also set breakpoints in your test code:
test('user can log in', async ({ page }) => {
await page.goto('/login');
await page.pause(); // Execution pauses here
await page.fill('input[name="email"]', 'user@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button:has-text("Log in")');
});
When the test reaches page.pause(), it stops. You can inspect the page in the browser, run commands in the Playwright Inspector console, and resume when ready.
When to Use Debug Mode
- Writing a new test — Pause after each step to verify it worked
- Fixing a failing test — Pause at the failure point and inspect the page state
- Isolating flaky behaviour — Run the test multiple times and pause when it fails
Analyzing CI Artifacts
When a test fails in CI, you don’t have UI mode or headed debugging available. Instead, you rely on artifacts: traces, videos, screenshots, and logs.
Downloading Artifacts from GitHub Actions
If your CI workflow uploads artifacts (as shown in Part 8), download them:
- Go to the failed GitHub Actions run
- Scroll to Artifacts
- Download
playwright-results-<shard>orplaywright-report-<shard>
Inside, you’ll find:
trace.zip— Open withnpx playwright show-trace trace.zipvideo.webm— Watch the test run in a video playerscreenshot.png— The DOM state at failurestdout.txt/stderr.txt— Console output
What to Look For
1. Trace: Always check the trace first. It has the most information.
2. Network requests: Did an API return an error? Was a request unexpectedly slow?
3. Console errors: Did the application log JavaScript errors?
4. Timing: Did the test timeout waiting for an element? Was an action slower than expected?
5. DOM state: Was the element present but not visible? Was it overlapped by another element?
Example: A Failing Test in CI
Test fails with: Error: Timeout 30000ms exceeded waiting for locator('button:has-text("Submit")').click()
Step 1: Download the trace from CI and open it.
Step 2: Inspect the timeline. The test called page.click('button:has-text("Submit")') and waited 30 seconds.
Step 3: Check the snapshot. The button is present, but it has disabled="true".
Step 4: Check the network tab. The form validation API returned 200, but 3 seconds before the test timed out.
Root cause: The button is disabled until the validation API responds. The API is slow in CI, so the button stays disabled longer than the test waits.
Fix: Wait for the button to be enabled before clicking:
await page.click('button:has-text("Submit")'); // Old: fails if button is disabled
await page.waitForSelector('button:has-text("Submit"):not([disabled])');
await page.click('button:has-text("Submit")'); // New: waits for enabled state
Or use Playwright’s auto-waiting with a higher timeout:
await page.click('button:has-text("Submit")', { timeout: 60000 });
Isolating Flaky Tests
Flaky tests are tests that sometimes pass and sometimes fail without code changes. They’re the most frustrating failures to debug because they’re non-deterministic.
Common Causes of Flakiness
1. Timing issues — The test clicks a button before it’s enabled, or waits for an element that hasn’t appeared yet.
2. Race conditions — Two tests modify the same database record simultaneously in parallel execution.
3. Transient network failures — An API returns 500 randomly.
4. Animation interference — An element moves while the test is trying to click it.
5. Test pollution — Test A changes application state that Test B depends on.
Debugging Flaky Tests
Step 1: Reproduce locally
Run the test 20–50 times to see if it fails locally:
npx playwright test <test-name> --repeat-each=50
If it fails, debug it in UI mode or with traces.
Step 2: Check the trace from CI
If it only fails in CI, download the trace and compare:
- Network requests (is CI slower?)
- Console errors (does CI have environment-specific issues?)
- Timing (did CI exceed a timeout that passes locally?)
Step 3: Add explicit waits
Replace implicit waits with explicit waits:
// Bad: Implicit wait (Playwright waits, but maybe not long enough)
await page.click('button:has-text("Submit")');
// Good: Explicit wait (ensure the button is visible and enabled)
await page.waitForSelector('button:has-text("Submit"):not([disabled])');
await page.click('button:has-text("Submit")');
Step 4: Isolate shared state
If the test modifies data that other tests depend on, either:
- Use
.serial()to run tests sequentially - Give each test its own test data (unique user accounts, unique records)
Best Practices for Debugging
1. Always Check the Trace First
The trace has more information than any other artifact. Start there.
2. Reproduce Locally Before Guessing
Don’t change code based on a CI failure log. Download the trace, reproduce the issue, and verify your fix.
3. Use page.pause() Liberally When Writing Tests
When writing a new test, pause after each step to verify it worked:
await page.goto('/login');
await page.pause(); // Verify the page loaded
await page.fill('input[name="email"]', 'user@example.com');
await page.pause(); // Verify the email was filled
Remove the pauses once the test is stable.
4. Don’t Ignore Flaky Tests
Track flaky test metrics. If a test is flaky > 5% of the time, investigate immediately. Flaky tests erode trust in the test suite.
5. Add Contextual Logging
If a test is failing in CI but passing locally, add logging to understand the environment:
test('user can log in', async ({ page }) => {
console.log('Starting login test');
await page.goto('/login');
console.log('Navigated to /login');
await page.fill('input[name="email"]', 'user@example.com');
console.log('Filled email');
await page.fill('input[name="password"]', 'password123');
console.log('Filled password');
await page.click('button:has-text("Log in")');
console.log('Clicked login button');
});
This helps narrow down where the test is failing.
Common Debugging Scenarios
Scenario 1: “Element not visible”
Error: Error: locator('button').click() — element is not visible
Debug steps:
- Open the trace
- Check the snapshot at failure — is the element actually present?
- Check CSS — is it
display: noneorvisibility: hidden? - Check z-index — is another element overlapping it?
Common fix: Wait for the element to be visible:
await page.waitForSelector('button', { state: 'visible' });
await page.click('button');
Scenario 2: “Timeout waiting for element”
Error: Error: Timeout 30000ms exceeded waiting for locator('div.results')
Debug steps:
- Check the trace — did the element ever appear?
- Check network requests — did an API fail or return late?
- Check console logs — did JavaScript throw an error?
Common fix: Either fix the application (API latency, JS error) or increase the timeout:
await page.waitForSelector('div.results', { timeout: 60000 });
Scenario 3: Test passes locally, fails in CI
Debug steps:
- Download the trace from CI
- Compare network timing — is CI slower?
- Compare environment variables — is CI missing a config?
- Check for race conditions — does CI run more tests in parallel?
Common fix: CI is often slower. Increase timeouts or optimize the application.
Conclusion
Debugging is not guessing. Playwright gives you observability: traces, screenshots, network logs, console output. Use these tools to see what happened, form a hypothesis, and verify your fix.
Key takeaways:
- Use UI mode when writing or debugging tests locally
- Use the trace viewer to investigate CI failures
- Use headed mode to watch tests run live
- Use debug mode to step through tests line-by-line
- Always download artifacts from CI and inspect the trace
:::tip[Build a Debugging Checklist] When a test fails, follow this checklist:
- Download the trace
- Check the action timeline — which step failed?
- Check the snapshot — what was the DOM state?
- Check the network tab — any failed requests?
- Check console logs — any JavaScript errors?
- Form a hypothesis and verify it locally :::
Action for this week: The next time a test fails, don’t guess. Open the trace viewer and systematically investigate. Time yourself. After a few failures, you’ll debug faster than anyone who’s just guessing and re-running tests.
Next in this series: Part 10 — Playwright Practices That Survive Growth