You wrote the test. It passed three times. You merged it. Now it fails in CI and nobody knows why. Or worse: it fails randomly, passes on retry, and slowly erodes the team's trust in the entire suite.
Debugging Playwright tests is a skill that separates productive test engineers from those stuck in a cycle of console.log, guess, and re-run. Playwright ships with world-class debugging tools — most teams just never learn to use them properly.
This guide walks through every tool in the Playwright debugging arsenal, from the basics to advanced CI diagnosis and AI-assisted auto-fixing. By the end, you'll have a systematic approach for any failure type.
The 5 Most Common Playwright Failures
Before reaching for debugging tools, it helps to recognize the failure pattern. Roughly 90% of Playwright test failures fall into five categories:
1. Timeout Error
The most frequent failure. Playwright waited for an element or navigation, exceeded the configured timeout, and threw TimeoutError. This usually means the element never appeared, the page didn't navigate, or a network request hung.
TimeoutError: locator.click: Timeout 30000ms exceeded. Call log: - waiting for getByRole('button', { name: 'Submit' }) - locator resolved to 0 elements - waiting for locator
Root cause: The selector doesn't match anything on the page. Either the element hasn't rendered yet, or the locator is wrong.
2. Selector Not Found
Similar to timeouts, but specifically when a locator resolves to zero elements. This often happens after a UI redesign where button text, labels, or component structure changed.
3. Assertion Failed
The element exists, but its state doesn't match your expectation. expect(page).toHaveURL() got a different URL. expect(locator).toHaveText() got different text. These are logic errors — the test expected one thing, the app did another.
Error: expect(received).toHaveText(expected) Expected: "Welcome back, Alice" Received: "Welcome back, Bob" Call log: - expect.toHaveText with timeout 5000ms - waiting for getByRole('heading', { level: 1 })
4. Navigation Error
Pages that fail to load, redirect loops, or net::ERR_CONNECTION_REFUSED. Common in CI when the dev server hasn't fully started, or when the base URL is misconfigured.
5. Flaky Test
The test passes sometimes and fails other times with no code change. This is the hardest to debug because it's non-deterministic. Usually caused by race conditions, shared state between tests, animation timing, or slow network responses.
Before you debug: Read the full error message and call log. Playwright's error output is exceptionally detailed — it tells you exactly which locator it was waiting for, how many elements matched, and what it tried. Most engineers skip straight to adding console.log without reading what Playwright already told them.
Tool 1: Playwright Inspector
The Playwright Inspector is an interactive GUI debugger that lets you step through test actions one at a time, inspect selectors against the live page, and see exactly what Playwright sees at each step. It's the single most useful debugging tool Playwright ships with.
Launch with --debug
# Debug all tests npx playwright test --debug # Debug a specific test file npx playwright test tests/login.spec.ts --debug # Debug a specific test by title npx playwright test -g "should login with valid credentials" --debug
This opens two windows: the browser and the Inspector panel. The test pauses before the first action and waits for you to click Step Over to advance.
Step Through Actions
Each step in the Inspector shows:
- The action — click, fill, navigate, expect
- The locator — the exact selector being used
- The target element — highlighted on the page with a blue overlay
- The result — success, pending, or error
You can click Step Over to execute one action, Resume to run until the next breakpoint (or the end), and Record to generate new locators by clicking elements on the page.
Inspect Selectors Live
The most powerful Inspector feature: type any locator into the Explore field and see it highlighted on the page in real time. This lets you test alternative selectors without modifying code.
// Type these into the Explore box to test getByRole('button', { name: 'Submit' }) // Matches 1 element getByRole('button', { name: 'Send' }) // Matches 0 elements getByLabel('Email address') // Matches 1 element getByTestId('login-form') // Matches 1 element
Pro tip: When a test fails because a selector matches 0 elements, open the Inspector, navigate to the failing page state, and use the Explore field to find the correct selector. Then copy it directly into your test. This is faster than guessing and re-running.
Tool 2: Trace Viewer
The Trace Viewer is Playwright's post-mortem debugger. While the Inspector is for live debugging, Trace Viewer lets you replay a test execution after it's finished — including tests that failed in CI where you can't open a browser window.
Enable Traces in Config
import { defineConfig } from '@playwright/test'; export default defineConfig({ use: { // 'on-first-retry' — only captures trace on first retry (recommended) // 'on' — captures trace for every test (large files) // 'retain-on-failure' — keeps trace only for failed tests trace: 'on-first-retry', }, retries: 2, });
The recommended setting is 'on-first-retry' with at least 1 retry configured. This captures a trace only when a test fails and is retried, keeping file sizes manageable while ensuring you have diagnostic data for every failure.
Open a Trace File
# Open a specific trace file npx playwright show-trace test-results/login-test/trace.zip # Or open the HTML report, then click "Trace" on any failed test npx playwright show-report
Read the Timeline & DOM Snapshots
Trace Viewer gives you four panels:
- Timeline — a horizontal bar showing every action with duration. Click any action to jump to that point.
- DOM Snapshot — the page state before and after each action. You can inspect the full DOM, including elements that were hidden or removed.
- Network — every HTTP request with status, timing, headers, and response body. Essential for diagnosing API-dependent test failures.
- Console — browser console output at each action. Shows JavaScript errors, warnings, and your
console.logstatements.
Key workflow: When a test fails in CI, download the trace artifact, open it with npx playwright show-trace, click the failing action in the timeline, then toggle between "Before" and "After" snapshots to see exactly what state the page was in when the action was attempted.
Trace Viewer for Assertion Failures
For assertion failures, the trace shows you the element's actual text/state at the moment the assertion was evaluated. Combined with the "Before" snapshot, you can see whether the page hadn't finished loading, showed stale data, or rendered something unexpected.
export default defineConfig({ use: { trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure', }, retries: process.env.CI ? 2 : 0, });
Tool 3: page.pause()
Sometimes you need to pause the test at a specific point in execution — not at the beginning, but after login, after navigating to a page, or after triggering a specific action. That's what page.pause() is for.
import { test, expect } from '@playwright/test'; test('debug checkout flow', async ({ page }) => { await page.goto('https://shop.example.com'); await page.getByRole('button', { name: 'Add to Cart' }).click(); await page.getByRole('link', { name: 'Checkout' }).click(); // Pause here — inspect the checkout page state await page.pause(); // These lines won't execute until you resume in the Inspector await page.getByLabel('Card number').fill('4242424242424242'); await page.getByRole('button', { name: 'Pay' }).click(); });
When execution hits page.pause(), the Playwright Inspector opens (if it isn't already) and pauses. You can:
- Inspect the DOM — use browser DevTools or the Inspector's Explore field
- Test selectors — type locators into Explore to verify they match
- Check network state — verify pending requests in DevTools
- Step through remaining actions — click Step Over to execute one action at a time
Don't commit page.pause(): Remove all page.pause() calls before committing. If one slips into CI, the test will hang indefinitely waiting for a human to click Resume. Consider adding a lint rule to catch it: no-restricted-syntax for page.pause().
Conditional Pausing
For debugging intermittent issues, you can pause only when a specific condition is met:
const cartCount = await page.getByTestId('cart-count').textContent(); // Only pause if the cart is unexpectedly empty if (cartCount === '0') { console.log('Cart is empty — pausing for inspection'); await page.pause(); }
Tool 4: Console Logs & Screenshots
Sometimes the simplest tools are the most effective. Capturing browser console output and taking screenshots at strategic points can reveal issues that other tools miss.
Capture Browser Console Output
test('track console errors during checkout', async ({ page }) => { // Collect all console messages const logs: string[] = []; page.on('console', (msg) => { if (msg.type() === 'error') { logs.push(`[ERROR] ${msg.text()}`); } }); // Also catch uncaught exceptions page.on('pageerror', (err) => { logs.push(`[UNCAUGHT] ${err.message}`); }); await page.goto('/checkout'); await page.getByRole('button', { name: 'Pay' }).click(); // Assert no console errors occurred expect(logs).toEqual([]); });
This pattern is especially useful for catching JavaScript errors that silently break functionality without causing visible failures — like a payment SDK failing to load or a tracking pixel throwing CORS errors.
Screenshot on Failure
Playwright can automatically capture screenshots when tests fail — this is also the foundation of visual regression testing. Configure it globally:
export default defineConfig({ use: { screenshot: 'only-on-failure', // auto-capture on failure }, });
For strategic debugging, take manual screenshots at specific points:
// Full page screenshot await page.screenshot({ path: 'debug-checkout.png', fullPage: true }); // Screenshot of a specific element await page.getByTestId('order-summary').screenshot({ path: 'debug-order-summary.png' });
Video Recording
For complex multi-step failures, video can be more revealing than screenshots:
export default defineConfig({ use: { video: 'retain-on-failure', // keeps video only for failed tests }, });
Trace vs. video vs. screenshot: Use trace for deep debugging (DOM snapshots + network + console). Use video for quick visual confirmation of what went wrong. Use screenshot when you just need the final page state. In CI, trace: 'on-first-retry' + screenshot: 'only-on-failure' is the best default. For more on output options, see our test reporting tools guide.
Tool 5: VS Code Extension
The official Playwright Test for VS Code extension turns your editor into a full debugging environment. It's the most ergonomic way to debug Playwright tests for day-to-day development.
Install & Setup
Install "Playwright Test for VS Code" from the extensions marketplace (publisher: Microsoft). It auto-detects your playwright.config.ts and populates the Test Explorer sidebar.
Set Breakpoints
Click the gutter next to any line in your test file to set a breakpoint. Then right-click the test in the Test Explorer and choose "Debug Test". The test runs until it hits your breakpoint, then pauses with the full VS Code debugger — variables panel, call stack, watch expressions, and the live browser.
test('login flow', async ({ page }) => { await page.goto('/login'); await page.getByLabel('Email').fill('alice@example.com'); await page.getByLabel('Password').fill('secret123'); await page.getByRole('button', { name: 'Sign In' }).click(); // Set breakpoint here ← to inspect page state after login await expect(page).toHaveURL('/dashboard'); // ← breakpoint await expect(page.getByRole('heading')).toHaveText('Welcome'); });
Run Individual Tests
The extension adds green play buttons next to each test() block. Click to run that single test. Hold Shift+click to run in debug mode. This eliminates the need to type long CLI commands with -g grep filters.
Watch Mode
Enable Watch Mode in the Test Explorer to auto-rerun tests when you save file changes. Combined with the "Show Browser" checkbox, you get a live preview of your test running in a real browser every time you save.
1. Show Browser = checked2. Set breakpoint on failing line3. Click "Debug Test" play button4. Inspect variables + page state
1. Add console.log() everywhere2. Run full suite from terminal3. Read terminal output4. Repeat 20 times
Debugging Flaky Tests
Flaky tests are the most demoralizing debugging challenge. They pass in isolation, fail in parallel. They pass locally, fail in CI. They pass 9 times, fail on the 10th. Our Playwright best practices guide covers prevention strategies, but when flaky tests slip through, here's a systematic approach to nail them down.
Step 1: Reproduce Reliably
# Run the test 50 times — if it fails even once, you've confirmed the flake npx playwright test tests/checkout.spec.ts --repeat-each=50 # Run with retries to see how often it fails npx playwright test tests/checkout.spec.ts --retries=5 --repeat-each=20
If the test fails 2 out of 50 runs, you've confirmed a 4% flake rate. Now you need to capture a trace on the failing run.
Step 2: Capture the Failure
# Enable trace on first retry + repeat 50 times npx playwright test tests/checkout.spec.ts \ --repeat-each=50 \ --retries=1 \ --trace=on-first-retry
When the test fails and retries, Playwright captures a trace of the retry. Open it with npx playwright show-report and click the failed test to examine the trace.
Step 3: Identify the Race Condition
The most common causes of flaky Playwright tests:
- Animation timing — An element is animating (sliding, fading) when Playwright tries to click it. Fix:
page.emulateMedia({ reducedMotion: 'reduce' })or wait for animation completion. - Network race — The test acts before an API response arrives. Fix:
await page.waitForResponse('**/api/cart')before asserting. - Shared test data — Two parallel tests use the same user/record. Fix: Create unique data per test using custom fixtures.
- Stale DOM reference — The DOM re-renders between locator resolution and action execution. Fix: Use Playwright's built-in auto-waiting locators instead of
elementHandle. - Timing-dependent assertions — Asserting on time-sensitive values (timestamps, counters). Fix: Use
toHaveTextwith regex patterns ortoContainText.
Never fix flaky tests with retries alone. Setting retries: 3 masks the problem. Use retries as a safety net, but always investigate and fix the root cause. A test that needs 3 retries to pass is telling you something is wrong.
Debugging in CI
"It works on my machine" is the most expensive sentence in test automation. When tests pass locally but fail in CI, follow this systematic diagnosis.
The CI Debugging Checklist
- Check the timeout: CI machines are typically 2–5x slower than your laptop. Increase
actionTimeoutandnavigationTimeoutfor CI. - Check the browser: Are you running the same browser version locally and in CI? Run
npx playwright installin your CI pipeline. - Check the viewport: Set explicit
viewport: { width: 1280, height: 720 }in config. CI headless browsers may default to different sizes. - Check the base URL: Is the dev server fully started before tests run? Use
webServerconfig withreuseExistingServer. - Check environment variables: Timezone, locale, API keys — anything that differs between local and CI.
export default defineConfig({ timeout: process.env.CI ? 60_000 : 30_000, expect: { timeout: process.env.CI ? 10_000 : 5_000, }, retries: process.env.CI ? 2 : 0, use: { trace: 'on-first-retry', screenshot: 'only-on-failure', viewport: { width: 1280, height: 720 }, }, webServer: { command: 'npm run start', port: 3000, reuseExistingServer: !process.env.CI, timeout: 120_000, // give server 2 min to start in CI }, });
Upload Artifacts in CI
- name: Run Playwright tests run: npx playwright test - name: Upload test results if: always() # Upload even when tests fail uses: actions/upload-artifact@v4 with: name: playwright-report path: playwright-report/ retention-days: 14 - name: Upload traces if: always() uses: actions/upload-artifact@v4 with: name: test-traces path: test-results/ retention-days: 14
Use the Playwright Docker image: mcr.microsoft.com/playwright:v1.50.0-noble includes all system dependencies pre-installed. This eliminates the #1 cause of "works locally, fails in CI" — missing system libraries for Chromium, Firefox, or WebKit rendering.
Debugging Parallel Failures in CI
If tests only fail when running in parallel (but pass with --workers=1), the problem is shared state:
- Shared database records — Two tests modify the same user/order. Fix: Each test creates its own data with unique identifiers.
- Shared files — Two tests write to the same file. Fix: Use temp directories per worker.
- Port conflicts — Two tests start servers on the same port. Fix: Use dynamic port allocation.
- Global state — Tests modify a singleton or environment variable. Fix: Reset state in
beforeEach.
# Run with 1 worker to confirm it's a parallelism issue npx playwright test --workers=1 # Then gradually increase to find the breaking point npx playwright test --workers=2 npx playwright test --workers=4
AI-Assisted Debugging
In 2026, AI isn't just for writing tests — it's a powerful debugging tool. Claude AI with the Playwright MCP Server can diagnose failures and auto-fix broken tests by inspecting the live page state.
Paste the Failure Into Claude
When a test fails, copy the full error output (including the call log) and paste it into Claude. A well-structured prompt looks like this:
This Playwright test is failing. Here's the error: TimeoutError: locator.click: Timeout 30000ms exceeded. waiting for getByRole('button', { name: 'Submit Order' }) locator resolved to 0 elements The button text was recently changed to "Place Order". Fix the test selector.
Claude will identify that the button text changed and suggest updating the locator from getByRole('button', { name: 'Submit Order' }) to getByRole('button', { name: 'Place Order' }).
MCP Server for Live DOM Inspection
The Playwright MCP Server gives Claude direct access to your browser. Instead of guessing what the page looks like, Claude can:
- Navigate to the page and inspect the actual DOM
- Find the correct selector by examining live elements
- Verify the fix by running the updated test in the connected browser
- Handle complex scenarios like dynamic content, shadow DOM, and iframes
// 1. Claude navigates to the page via MCP // 2. Claude inspects the DOM and finds the button // 3. Claude generates the correct locator: // Before (broken): await page.getByRole('button', { name: 'Submit Order' }).click(); // After (fixed by Claude + MCP): await page.getByRole('button', { name: 'Place Order' }).click();
Speed comparison: Manual debugging of a broken selector typically takes 10–30 minutes (find the page, inspect the element, update the test, re-run). With Claude + MCP, the same fix takes under 2 minutes. For teams with 500+ tests, this translates to hours saved per sprint.
Auto-Fix Patterns
AI-assisted debugging works best for these failure types:
- Broken selectors after UI redesigns — Claude finds the new selector from the live DOM
- Assertion mismatches — Claude compares expected vs. actual and identifies the root cause
- Missing waits — Claude recognizes race condition patterns and adds appropriate waits
- Configuration issues — Claude analyzes config and error together to spot mismatches
Debugging Checklist
Use this quick-reference when a test fails. Work through it top to bottom — most issues resolve within the first few items.
First Response (under 2 minutes)
- Read the full error message and call log
- Check which locator failed and how many elements matched
- Verify the test ran against the correct URL
- Check if other tests in the same file also fail
- Look for recent UI changes in git log
- Run the test in headed mode to see visually
Deep Diagnosis (5-15 minutes)
- Run with --debug to step through actions
- Open Trace Viewer for DOM snapshots
- Check network tab for failed/slow requests
- Check console for JavaScript errors
- Test selectors in Inspector Explore field
- Use page.pause() at the failing step
- Run with --repeat-each=20 for flake check
- Run with --workers=1 for parallel issues
CI-Specific Debugging
- Download trace artifacts from CI
- Compare CI timeout vs. local timeout
- Check viewport size in CI config
- Verify browser version matches local
- Check webServer startup timeout
- Review environment variable differences
- Use Docker image for consistency
- Run same test locally with --workers=N
Frequently Asked Questions
How do I debug a failing Playwright test?
Start by running the test with --debug flag to open the Playwright Inspector. Step through each action, inspect selectors live, and see the browser state at every point. For tests that already failed (especially in CI), use Trace Viewer to replay the execution with DOM snapshots and network logs.
What is Playwright Trace Viewer and how do I use it?
Trace Viewer is a GUI tool that replays test execution step-by-step. Enable it with use: { trace: 'on-first-retry' } in your config. After a failure, open the trace with npx playwright show-trace trace.zip. You get DOM snapshots, network requests, console logs, and action timing for every step.
Why do my Playwright tests pass locally but fail in CI?
Common causes: (1) CI machines are slower — increase timeouts, (2) Missing system dependencies — use the official Playwright Docker image, (3) Different viewport sizes — set explicit dimensions in config, (4) Dev server not ready — use webServer config with adequate timeout, (5) Environment variable differences — set timezone and locale explicitly.
How do I fix flaky Playwright tests?
First, reproduce the flake with --repeat-each=50. Enable trace recording to capture the failure. Common fixes: replace waitForTimeout with web-first assertions, ensure test isolation with no shared state, handle animations with reducedMotion, and wait for specific network responses before asserting.
Can I use AI to debug Playwright tests?
Yes. With Claude AI and the Playwright MCP Server, you can paste a test failure and get an auto-fix within minutes. The MCP Server gives Claude live access to your browser DOM, so it can inspect the actual page state and generate correct selectors — especially powerful after UI redesigns break multiple tests.
Asim Noaman
Senior QA Automation Engineer & AI Testing Specialist
With years of hands-on experience building test automation frameworks for production applications, Asim specializes in combining traditional QA methodologies with cutting-edge AI tools. He has helped teams adopt Playwright and AI-driven testing workflows to ship faster with fewer bugs.