Fun with Learning Technology
LearnCoursesQuestionsTracksToolsNewsExplorePractice
Fun with Learning Technology

A new problem, explained clearly, every day.

Subscribe
Learn
  • Lessons
  • Topics
  • News
  • Tools
  • Courses
  • Career tracks
  • Everything
Site
  • About
  • Contact
  • Support
  • Privacy
  • Terms
Get the daily one

One email per new problem. No spam.

Request a tutorial

Requests shape what gets made next.

© 2026 Fun with Learning TechnologyRSS
Home›Courses›Playwright›Debugging Playwright tests effectively

Advanced Playwright Techniques

Debugging Playwright tests effectively

Effective debugging in Playwright involves leveraging built-in diagnostic tools to observe execution states, intercept network traffic, and inspect browser contexts in real-time. Mastering these techniques transforms frustrating intermittent failures into transparent, traceable events that can be addressed systematically. You should reach for these advanced debugging strategies whenever automated tests yield inconsistent results or fail without providing clear error messages in standard logs.

Using the Playwright Inspector

The Playwright Inspector is the foundational tool for interactive debugging, allowing developers to step through test execution line by line. When you execute your test with the inspector enabled, the browser launches in a persistent state, pausing execution at specific commands. This allows you to inspect the Document Object Model (DOM) and verify selectors precisely as the test engine sees them at that exact millisecond. The reason this works effectively is that it synchronizes the test runner with the browser's internal event loop, preventing the race conditions that occur during high-speed automated runs. By using 'pause()' within your test script, you gain the ability to manually trigger actions or examine the state of complex web components. This creates a bridge between your test logic and the actual browser execution, ensuring that you can validate locators and state transitions visually before finalizing your test assertions.

import { test } from '@playwright/test';

test('inspecting state', async ({ page }) => {
  await page.goto('https://example.com');
  // Pause execution to open the Inspector
  await page.pause(); 
  await page.getByRole('button').click();
});

Visual Tracing and Artifacts

Tracing is an essential post-mortem debugging feature that captures every action, network request, and console log during a test run. Unlike a simple screenshot, a trace provides a complete timeline of the test execution, including the DOM snapshot at every step. This works by serializing the state of the browser at each event and storing it in a structured file format that can be viewed later. When a test fails in a CI environment, retrieving the trace file allows you to 'time travel' through the test execution. You can examine why an action failed, view the specific network responses that occurred leading up to the failure, and see how the UI changed. By analyzing the trace, you identify the root cause by comparing the recorded state against the expected application behavior, effectively eliminating guesswork regarding environmental timing or configuration issues.

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

export default defineConfig({
  use: {
    // Capture traces on first retry for debugging
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
});

Network Request Interception

Debugging complex application logic often requires inspecting network traffic to verify that frontend interactions correctly communicate with backend services. Playwright provides a mechanism to intercept, log, and even mock network requests in real-time. By monitoring 'request' and 'response' events, you can confirm that your application sends the expected payload with the correct authorization headers. The reason this is powerful for debugging is that many test failures are caused by silent failures in network calls that do not trigger visual errors in the UI. Intercepting these requests allows you to verify the contract between your code and the server. If a test fails because a button click doesn't trigger a navigation, intercepting the request tells you immediately if the network request was actually fired, rejected by a server error, or never initiated due to a logic gate error in your client-side code.

test('monitoring network activity', async ({ page }) => {
  // Listen for all network requests
  page.on('request', request => console.log('>>', request.method(), request.url()));
  await page.goto('https://api.example.com');
});

Headful Execution and Console Logging

While headless execution is standard for CI, running tests in 'headful' mode is often the first step in troubleshooting visual regressions or complex interactions. Headful mode forces the browser to open a visible window, allowing you to observe the execution rhythm and witness rendering issues that are hidden in headless environments. Furthermore, integrating console logging directly from the browser context into your terminal provides immediate feedback on hidden exceptions. The reason this is highly effective is that it surfaces errors within the browser's JavaScript environment that the test runner might otherwise treat as generic timeouts. By routing browser console logs into your terminal, you can correlate specific test failures with uncaught exceptions or warnings thrown by the application logic. This transparency makes debugging significantly faster because you can pinpoint the exact line of client-side code failing simultaneously with your test command.

test('logging console messages', async ({ page }) => {
  // Forward browser logs to the console
  page.on('console', msg => console.log(`Browser log: ${msg.text()}`));
  await page.goto('https://example.com');
});

Debugging with Timeouts and Retries

Flaky tests often stem from subtle timing issues or resource contention that only appear sporadically. To debug these effectively, you must understand how Playwright handles timeouts and how retries can be used to gather more diagnostic data. By intentionally slowing down specific actions or extending test timeouts, you can observe whether a test eventually passes, which indicates an underlying synchronization issue. However, relying solely on arbitrary timeouts is a 'code smell'; instead, you should use retries combined with trace artifacts. When a test is configured to retry, Playwright automatically attempts the action again, potentially providing a cleaner execution trace if the flake was due to an external service hiccup. This strategy helps differentiate between genuine logic bugs—which fail consistently—and environmental instability, which can be mitigated through smarter polling and better handling of asynchronous data states.

test('debug with custom timeout', async ({ page }) => {
  // Increase timeout for a slow operation
  await page.goto('https://slow-load.com', { timeout: 30000 });
  await page.waitForSelector('.ready-state', { timeout: 10000 });
});

Key points

  • The Playwright Inspector allows for pausing execution to perform real-time verification of locators and element states.
  • Tracing provides a comprehensive timeline of test events, including DOM snapshots and network logs for post-mortem analysis.
  • Intercepting network requests helps identify silent failures that occur between the client and backend services.
  • Running tests in headful mode exposes rendering issues and visual glitches that are invisible in headless environments.
  • Console logs from the browser should be forwarded to your test terminal to surface uncaught client-side JavaScript errors.
  • Strategic use of retries helps isolate whether a failure is a permanent logic bug or an intermittent environmental issue.
  • Timeouts should be adjusted to observe race conditions rather than as a permanent fix for slow-loading application components.
  • Effective debugging requires correlating browser-side events with the high-level test actions executed by the test runner.

Common mistakes

  • Mistake: Relying solely on console.log for debugging. Why it's wrong: It bloats the console output and doesn't provide visual context. Fix: Use the Playwright Inspector or the UI mode to step through actions visually.
  • Mistake: Overusing page.waitForTimeout(). Why it's wrong: It introduces non-deterministic test flakiness and slows down execution. Fix: Rely on Playwright's auto-waiting locators and web-first assertions like expect().toBeVisible().
  • Mistake: Neglecting to set the --headed flag during development. Why it's wrong: Headless mode hides the browser's state, making it impossible to see where the interaction fails. Fix: Use 'npx playwright test --ui' or '--headed' to watch the browser in real-time.
  • Mistake: Ignoring Trace Viewer in CI failures. Why it's wrong: Without traces, you have no recorded proof of the UI state at the moment of failure. Fix: Ensure 'trace: on-first-retry' is enabled in your config to inspect snapshots and network activity.
  • Mistake: Hardcoding dynamic selectors like IDs that change per build. Why it's wrong: The test breaks whenever the UI implementation shifts. Fix: Use user-facing locators like getByRole, getByText, or getByLabel which mimic how users interact with the page.

Interview questions

What is the most effective way to debug a failing Playwright test locally?

The most effective way to debug locally is using the Playwright Inspector with the 'headed' mode and the 'debug' flag. By running your command with 'npx playwright test --debug', you launch the browser in a controlled environment where you can step through every action one line at a time. This allows you to inspect the DOM state, check element selectors, and see exactly what Playwright 'sees' before it clicks or types. It is superior to just reading logs because it provides a live, interactive visualization of the failing assertion, which makes identifying locator issues or timing problems instantaneous.

How can you use Playwright Trace Viewer to analyze a failed test run?

The Trace Viewer is a powerful post-mortem debugging tool. When a test fails, Playwright generates a trace file that captures a complete recording of the test execution. You can open this using 'npx playwright show-trace trace.zip'. It shows a visual timeline where you can hover over every action to see the DOM snapshot, the network requests made, console logs, and the exact error stack trace. It is essential because it recreates the environment of the CI failure locally, allowing you to see exactly which network request failed or which locator became stale without needing to reproduce the test manually.

Compare using 'page.pause()' versus 'console.log()' for debugging Playwright tests. Why is one generally better?

While 'console.log()' can provide basic visibility into variables, 'page.pause()' is significantly more powerful for Playwright debugging. Using 'page.pause()' stops test execution entirely and opens the Playwright Inspector, giving you access to the Live Locator picker. This tool allows you to hover over elements to generate the exact CSS or XPath selectors that Playwright recommends. 'console.log()' only gives you static text, whereas 'page.pause()' lets you actively manipulate the page, trigger manual actions, and verify selector reliability in real-time, making it the superior choice for fixing flaky locators.

How do you debug flaky tests that only fail on CI due to timing issues?

To debug intermittent CI failures, I recommend enabling video recording and trace files for every run. Once you have the trace, you should look for race conditions where the test expects an element that has not yet attached to the DOM. I often add specific diagnostic logging using 'page.on('console')' to capture browser-side errors. Additionally, replacing brittle 'waitForTimeout' calls with web-first assertions like 'expect(locator).toBeVisible()' is crucial, as Playwright's auto-retrying mechanism is specifically designed to eliminate these timing-related flakes by polling the state until the assertion passes.

When a locator is failing to find an element, what steps should you take to debug it?

When a locator fails, I first use the 'npx playwright codegen' tool or the Inspector's pick-locator feature to verify what Playwright perceives. I check if the element is hidden behind a loading spinner or if there are multiple elements matching the same selector. I look at the error message, which usually indicates if the element is 'hidden' or 'attached'. If it is a shadow DOM issue, I verify the selection strategy to ensure it pierces the shadow root correctly. Writing more resilient locators using 'getByRole' or 'getByLabel' rather than complex CSS paths is usually the fix, as these prioritize accessibility trees which are inherently more stable.

How do you approach debugging network-related failures or slow API responses in Playwright?

For network issues, I leverage Playwright’s 'page.waitForResponse' or 'page.waitForRequest' methods to pinpoint exactly where the handshake or data transfer fails. I also use the Network tab within the Trace Viewer to inspect the request payload, response status codes, and timing waterfall. If an API is slow, I increase the timeout specifically for that network call using the 'timeout' option in the network wait method. This allows the test to wait for the backend process without globally increasing the timeout for the entire suite, ensuring that the test remains fast while becoming more robust against backend latency.

All Playwright interview questions →

Check yourself

1. When debugging a flaky test that passes locally but fails in CI, which tool provides the most comprehensive data including snapshots, console logs, and network requests?

  • A.console.log statements
  • B.The Playwright Trace Viewer
  • C.Browser screenshot diffs
  • D.The --debug flag without a trace
Show answer

B. The Playwright Trace Viewer
Trace Viewer captures the full execution context including state snapshots and network requests. Console logs are limited to text, screenshots are static, and --debug is for interactive sessions rather than post-mortem analysis.

2. Which approach is most effective for debugging an interaction that triggers a hidden overlay or modal?

  • A.Using page.waitForTimeout(5000)
  • B.Running the test with the --headed flag to observe the state
  • C.Adding more console logs to the test file
  • D.Reducing the browser viewport size
Show answer

B. Running the test with the --headed flag to observe the state
Running in headed mode allows you to visually see if the overlay appears. Timeouts cause flakiness, console logs do not show DOM changes, and viewport changes may alter the layout, potentially masking the issue.

3. What is the primary advantage of using locators like getByRole over CSS selectors during a debugging session?

  • A.They execute faster in the browser engine
  • B.They are immune to changes in the page CSS styling
  • C.They encourage testing from a user perspective and remain resilient to implementation shifts
  • D.They are the only way to select elements that are dynamically generated
Show answer

C. They encourage testing from a user perspective and remain resilient to implementation shifts
User-facing locators mimic the user journey. CSS selectors are fragile because they change when the HTML structure is refactored. While they are resilient, performance is not the primary advantage over other selector types.

4. A test fails because an element is 'hidden'. What is the first thing you should verify using the Playwright Inspector?

  • A.That the CSS z-index is set to a negative value
  • B.That the element is not covered by another element or conditionally rendered
  • C.That the test has waited long enough for the server response
  • D.That the browser is running in the correct language
Show answer

B. That the element is not covered by another element or conditionally rendered
Playwright checks for actionability (like visibility) automatically. If it reports hidden, the element is likely obstructed or not in the DOM. Z-index is a specific CSS possibility, but checking the DOM structure is the general debugging step.

5. Why is 'expect().toBeVisible()' preferred over checking an element's existence when debugging race conditions?

  • A.toBeVisible triggers an automatic retry until the condition is met
  • B.toBeVisible is faster because it skips the DOM node lookup
  • C.toBeVisible only works in headed browser mode
  • D.toBeVisible validates the element has a background color
Show answer

A. toBeVisible triggers an automatic retry until the condition is met
Web-first assertions like toBeVisible use Playwright's auto-retrying mechanism, which eliminates the need for manual waits. It has nothing to do with background colors, speed of lookup, or headed mode requirements.

Take the full Playwright quiz →

← PreviousIntegrating Playwright with CI/CD pipelinesNext →Performance testing with Playwright

Playwright

37 lessons, free to read.

All lessons →

Track your progress

Sign in to mark lessons done, score quizzes and keep notes.

Open in the app