Guide

Debugging Playwright Tests: Advanced Techniques & Tools

By Pavel Borji··Founder @ Assrt

When an end-to-end test fails, the error message alone is rarely enough. This guide covers every tool and technique available for diagnosing Playwright test failures, from interactive inspectors to full trace recordings.

Faster

Teams with structured debugging workflows resolve test failures much faster than those relying on console logs alone.

1. The Debugging Mindset for E2E Tests

Debugging end-to-end tests requires a fundamentally different approach compared to unit tests. Unit tests are deterministic, fast, and operate on isolated code. When they fail, the error usually points directly to the problem. E2E tests, on the other hand, exercise the entire application stack: browser rendering, JavaScript execution, network requests, server responses, database state, and timing between all of these layers.

The first principle of E2E debugging is that timing matters everywhere. A test that passes locally might fail in CI because the server responded 200ms slower. A test that passes in headless mode might fail in headed mode because the browser window size changed the layout. Understanding that E2E tests operate in a world of probabilities rather than certainties is essential.

The second principle is that visual context is invaluable. Unlike unit tests where you can reason about inputs and outputs abstractly, E2E failures often require seeing what the browser actually displayed at the moment of failure. Was the button hidden behind a modal? Did the page show a loading spinner instead of the expected content? Was the element scrolled out of view? These questions cannot be answered from a stack trace alone.

The third principle is to reproduce first, then diagnose. Flaky test failures are notoriously difficult to debug because the conditions that caused them may not recur on the next run. Before diving into the code, establish a reliable reproduction. Run the test in isolation, increase the timeout, add tracing, and try to capture the exact state that triggered the failure.

With these principles in mind, let us explore the specific tools Playwright provides for each stage of the debugging process.

2. Playwright Inspector

The Playwright Inspector is an interactive debugging tool that lets you step through test actions one at a time, evaluate selectors in real time, and record new interactions. It is the closest equivalent to a traditional debugger for browser-based tests, and it should be the first tool you reach for when a test fails in a way that is not immediately obvious from the error message.

Launching the Inspector

You can launch the Inspector by setting the PWDEBUG environment variable before running your tests. This opens a browser window alongside the Inspector panel, which displays each action in your test and lets you execute them one at a time.

# Launch a specific test with the Inspector
PWDEBUG=1 npx playwright test tests/login.spec.ts

# You can also use the --debug flag directly
npx playwright test tests/login.spec.ts --debug

Stepping Through Actions

Once the Inspector is open, each action in your test appears in the action log. Click the “Step Over” button to execute actions one at a time. After each step, you can inspect the page state, check the DOM, and verify that the action had the intended effect before moving on. This is particularly useful for tests where the failure occurs mid-sequence and you need to pinpoint exactly which step goes wrong.

Evaluating Selectors

The Inspector includes a selector evaluation panel where you can type any Playwright selector and see which elements it matches on the current page. Matching elements are highlighted directly in the browser window. This is invaluable for diagnosing “element not found” errors, as you can see whether the selector matches zero elements, multiple elements, or an unexpected element.

Recording Interactions

The Inspector also supports recording mode, where it generates Playwright code as you interact with the page manually. Click “Record” in the Inspector toolbar, then perform actions in the browser. The Inspector captures each interaction and generates the corresponding Playwright code, which you can copy directly into your test. This is especially useful for building tests for complex interaction sequences where you are not sure which selectors to use.

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started

3. Trace Viewer

While the Inspector is excellent for interactive debugging, many failures occur in CI where interactive tools are not available. The Trace Viewer fills this gap by recording a complete trace of every action, including screenshots, DOM snapshots, network requests, and console messages. You can replay these traces locally to investigate failures that happened on a remote machine.

Enabling Traces

Configure trace recording in your Playwright configuration file. The most common pattern is to record traces only on test failure or on the first retry, which keeps trace storage manageable while ensuring you have data for every failure.

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    // Record trace on first retry of a failed test
    trace: 'on-first-retry',
  },
  // Enable retries so traces get captured on failure
  retries: 1,
});

Viewing the Timeline

Open the Trace Viewer by running npx playwright show-trace trace.zip. The viewer displays a timeline of every action, with screenshots at each step. You can click on any action to see the page state at that exact moment, including the DOM snapshot, console output, and network activity. The before and after screenshots for each action make it easy to see exactly what changed.

Network Request Analysis

The Trace Viewer includes a network tab that shows every HTTP request made during the test, along with request and response headers, status codes, timing, and body content. This is essential for debugging tests that fail due to unexpected API responses, slow endpoints, or missing data.

Console Log Integration

All browser console messages (logs, warnings, errors) are captured in the trace and displayed alongside the action timeline. This allows you to correlate JavaScript errors with specific test actions. If the application throws an unhandled promise rejection at the same time a click action fails, the trace makes that connection visible immediately.

4. Headed Mode Debugging

Running tests in headed mode (with a visible browser window) lets you watch the test execute in real time. While this is slower than headless execution, it provides immediate visual feedback that can reveal issues invisible in logs.

Running Tests Headed

Pass the --headed flag to run tests with a visible browser, or configure it in your project settings:

# Run a specific test in headed mode
npx playwright test tests/checkout.spec.ts --headed

# Combine with slowMo for visual debugging
# In playwright.config.ts:
export default defineConfig({
  use: {
    headless: false,
    launchOptions: {
      slowMo: 500, // 500ms delay between each action
    },
  },
});

Using slowMo for Visual Debugging

The slowMo option adds a specified delay (in milliseconds) between each Playwright action. This slows the test down enough that you can visually follow what is happening. A value of 200 to 500ms is typically sufficient to observe each action without making the test unbearably slow. This is particularly useful for debugging tests that involve complex sequences of clicks, drags, or form interactions where the order and timing of actions matter.

Watching Tests Execute

When watching a headed test, pay attention to several things. Does the page load completely before the test starts interacting? Are there unexpected modals, popups, or banners obscuring target elements? Does the page layout shift during interactions? Is the scrolling behavior correct? Many failures that produce cryptic error messages become immediately obvious when you can see the browser.

You can also combine headed mode with the page.pause() method to freeze the test at a specific point and interact with the page manually:

test('checkout flow', async ({ page }) => {
  await page.goto('/cart');
  await page.click('[data-testid="checkout-button"]');

  // Pause here to inspect the page manually
  await page.pause();

  // Test continues after you resume in the Inspector
  await expect(page.locator('.order-summary')).toBeVisible();
});

5. Video and Screenshot Capture

For failures that occur in CI where headed mode is not available, Playwright can record videos of every test run and capture screenshots at specific points or automatically on failure. These artifacts provide visual evidence that complements the text-based error output.

Automatic Video on Failure

Configure video recording in your Playwright config. The retain-on-failure mode records video for every test but only saves the file when a test fails. This gives you visual playback of failures without consuming storage for passing tests.

// playwright.config.ts
export default defineConfig({
  use: {
    // Record video, keep only on failure
    video: 'retain-on-failure',

    // Capture screenshot on failure
    screenshot: 'only-on-failure',
  },
  // Videos are saved to test-results directory
  outputDir: './test-results',
});

Screenshot Assertions

Playwright supports visual comparison testing through screenshot assertions. Capture a reference screenshot and compare it against subsequent runs to detect visual regressions:

// Visual comparison assertion
await expect(page).toHaveScreenshot('dashboard.png', {
  maxDiffPixels: 100, // allow minor rendering differences
});

// Screenshot a specific element
await expect(
  page.locator('.chart-container')
).toHaveScreenshot('revenue-chart.png');

// Take a manual screenshot for debugging
await page.screenshot({
  path: 'debug-screenshot.png',
  fullPage: true,
});

Artifact Management

In CI environments, configure your pipeline to upload test artifacts (videos, screenshots, traces) to a persistent storage location. GitHub Actions, GitLab CI, and Jenkins all support artifact uploads. Without this step, the valuable debugging data is lost when the CI runner is recycled. Organize artifacts by test name and run date so you can easily correlate them with specific pipeline runs.

6. Console and Network Analysis

Browser console messages and network requests often contain the critical context needed to understand why a test failed. Playwright provides APIs to capture and analyze both during test execution.

Capturing Browser Console Messages

Listen for console events to capture every message the application logs during the test. This is especially useful for catching unhandled errors, deprecation warnings, and application-level debug output.

test('captures console output', async ({ page }) => {
  const consoleMessages: string[] = [];

  page.on('console', (msg) => {
    consoleMessages.push(`[${msg.type()}] ${msg.text()}`);
  });

  // Also capture uncaught exceptions
  page.on('pageerror', (error) => {
    console.error('Page error:', error.message);
  });

  await page.goto('/dashboard');
  await page.click('#load-data');

  // Check if any errors were logged
  const errors = consoleMessages.filter((m) => m.startsWith('[error]'));
  expect(errors).toHaveLength(0);
});

Monitoring Network Requests

Track network requests to understand what data the application fetched and whether any requests failed or returned unexpected responses:

test('monitors API calls', async ({ page }) => {
  const apiCalls: { url: string; status: number }[] = [];

  page.on('response', (response) => {
    if (response.url().includes('/api/')) {
      apiCalls.push({
        url: response.url(),
        status: response.status(),
      });
    }
  });

  await page.goto('/dashboard');

  // Wait for the critical API call to complete
  await page.waitForResponse(
    (resp) => resp.url().includes('/api/user/profile')
  );

  // Verify no API calls returned errors
  const failedCalls = apiCalls.filter((c) => c.status >= 400);
  if (failedCalls.length > 0) {
    console.log('Failed API calls:', JSON.stringify(failedCalls, null, 2));
  }
});

Intercepting Responses

Sometimes you need to modify network responses to test error handling or simulate specific conditions. Playwright's route API lets you intercept any request and provide a custom response:

// Simulate a server error to test error handling
await page.route('**/api/data', (route) => {
  route.fulfill({
    status: 500,
    contentType: 'application/json',
    body: JSON.stringify({ error: 'Internal Server Error' }),
  });
});

// Simulate a slow response to test loading states
await page.route('**/api/data', async (route) => {
  await new Promise((resolve) => setTimeout(resolve, 3000));
  route.continue();
});

7. Common Debugging Scenarios

Certain failure modes appear so frequently in Playwright tests that they deserve dedicated debugging strategies. Here are the most common scenarios and how to approach each one systematically.

Element Not Found

The “element not found” error means Playwright could not locate an element matching your selector within the timeout period. Start by checking whether the selector is correct using the Inspector's selector evaluation. Then verify the element is actually present in the DOM at the time the test tries to find it. Common causes include: the element exists but is hidden, the page has not finished loading, the element is inside an iframe, or the selector uses a class name that has changed.

// Debug element visibility
const locator = page.locator('#submit-button');

// Check if element exists in DOM at all
const count = await locator.count();
console.log('Elements found:', count);

// Check visibility
if (count > 0) {
  const isVisible = await locator.isVisible();
  const box = await locator.boundingBox();
  console.log('Visible:', isVisible, 'BoundingBox:', box);
}

Timeout Errors

Timeout errors indicate that an action or assertion took longer than the allowed time. Before simply increasing the timeout, investigate why the operation is slow. Is the server responding slowly? Is there a loading spinner that never disappears? Is there a redirect loop? Use traces to see exactly what was happening on the page when the timeout occurred. If the operation is genuinely slow in certain environments, increase the timeout selectively rather than globally.

Assertion Failures

When an assertion fails, the error message tells you what was expected versus what was received. For text assertions, check for whitespace differences, Unicode characters, or dynamic content (timestamps, random IDs) that change between runs. For visibility assertions, check if the element is present but hidden by CSS, or if it is obscured by another element (a modal overlay, for example).

Navigation Issues

Navigation failures occur when Playwright expects a page load that does not happen, or when the navigation completes but lands on an unexpected URL. Common causes include: single-page app navigation that does not trigger a full page load, authentication redirects that send the test to a login page, and server-side redirects that change the URL. Use page.url() to verify the current URL after navigation and page.waitForURL() to wait for a specific URL pattern.

Popup and Dialog Handling

Browser dialogs (alerts, confirms, prompts) and popup windows require explicit handling in Playwright. If a dialog appears and the test does not handle it, subsequent actions will fail. Register dialog handlers before the action that triggers them:

// Handle a confirmation dialog
page.on('dialog', async (dialog) => {
  console.log('Dialog message:', dialog.message());
  await dialog.accept();
});

// Handle a popup window
const [popup] = await Promise.all([
  page.waitForEvent('popup'),
  page.click('#open-popup-link'),
]);
await popup.waitForLoadState();
console.log('Popup URL:', popup.url());

8. Integrating with IDE Debuggers

For the most powerful debugging experience, integrate Playwright directly with your IDE. This lets you set breakpoints, inspect variables, evaluate expressions, and step through test code line by line, all while the browser executes your test actions.

VS Code Playwright Extension

The official Playwright extension for VS Code provides a rich debugging experience. Install it from the VS Code marketplace, and it automatically detects your Playwright configuration. You can run individual tests from the editor gutter, see test results inline, and launch the Playwright Inspector directly from the extension.

// .vscode/launch.json for Playwright debugging
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Playwright Tests",
      "type": "node",
      "request": "launch",
      "program": "${workspaceFolder}/node_modules/.bin/playwright",
      "args": ["test", "--headed", "${file}"],
      "console": "integratedTerminal",
      "env": {
        "PWDEBUG": "1"
      }
    }
  ]
}

Setting Breakpoints

With the launch configuration above, you can set breakpoints in your test files just like any Node.js application. When execution hits a breakpoint, you can inspect the current state of all variables, including page objects, locators, and response data. The browser window stays open during breakpoint pauses, so you can also inspect the page state visually.

Watch Expressions

VS Code's watch panel is useful for monitoring values that change throughout the test. Add expressions like page.url() or await locator.textContent() to the watch panel to see how the application state evolves as you step through the test. Note that async expressions require evaluation in the Debug Console rather than the Watch panel.

Other IDE Options

JetBrains WebStorm and IntelliJ IDEA also offer Playwright support through plugins. The debugging workflow is similar: configure a Node.js run configuration pointing to the Playwright binary, set the PWDEBUGenvironment variable, and use the IDE's built-in debugger to set breakpoints and step through code.

Regardless of which IDE you use, the combination of code-level debugging (breakpoints, variable inspection) and visual debugging (seeing the browser state at each step) provides the most comprehensive view of what your test is doing and why it might be failing. For teams adopting Assrt, the AI layer adds another dimension: when a test fails due to a UI change, Assrt automatically attempts to repair the broken selector or timing before reporting a failure. This eliminates many of the debugging scenarios described above entirely, letting you focus on genuine application bugs rather than test maintenance.

Related Guides

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk