Debugging uses errors, traces, logs, and responses to find the cause. A failure does not automatically mean the app is broken.
read error → inspect clues → reproduce → fix cause
Series: Playwright Zero to Expert | Demo app used throughout this series: saucedemo.com
This part doesn’t introduce many genuinely new tools — Playwright Inspector is new, but DevTools, trace, screenshots, and video are all things you’ve already met. What this part actually builds is a methodology: a repeatable, systematic process for going from “this test failed and I don’t know why” to “I understand exactly what happened and what to do about it,” instead of randomly guessing and re-running.
Playwright Inspector
npx playwright test --debug
This launches your test in a special mode: a real, visible browser opens alongside a separate Inspector window, and execution pauses before the very first action, waiting for you to manually step forward, one action at a time, at your own pace. This is genuinely different from just watching a --headed run — you control the pacing entirely, which is invaluable for a test that’s failing at some specific point you haven’t yet pinpointed.
await page.pause(); // insert this line anywhere in your test code
Adding page.pause() directly into your test code pauses execution at that exact line, every time, opening the Inspector at that specific point — genuinely useful for jumping straight to the area you actually suspect is the problem, rather than stepping through an entire test from the very beginning every single time.
The Inspector also includes a locator picker — click any element on the paused page, and it shows you the locator Playwright would generate for it, alongside confirming how many elements on the page currently match. This is a fast, direct way to verify a locator you’re unsure about, without writing and running a separate test just to check it.
A Systematic Debugging Process
Here’s a methodology worth genuinely internalizing, rather than a checklist to follow mechanically — the actual value is in the reasoning behind each step, not the order itself.
Analogy: The Medical Triage Checklist When a patient walks into the emergency room feeling sick:
- Wrong approach: The doctor immediately hands them random medications and runs surgery, hoping something cures them (blindly editing test code and repeatedly clicking run).
- Right approach (Triage):
- Check vital signs: Read the chart and monitor heart rate (Read the exact Playwright error message / stack trace).
- Check historical scans: Look at the X-rays and MRI scans (Open the Playwright Trace Viewer zip file to inspect DOM and network history).
- Check physical symptoms: Ask the patient to walk or move (Manually reproduce the steps in a standard headed browser).
- Isolate conditions: Test one limb at a time in isolation (Run the single test file alone with
--debugand breakpoints).
📊 Visual Flowchart: The Systematic Debugging Workflow
Follow these sequential diagnostic checks when investigating a failing test:
graph TD
Fail["1. Test Fails"] --> ReadMsg["2. Read error message completely<br>(What failed, on what line, which timeout?)"]
ReadMsg --> OpenTrace["3. Open Trace Viewer zip<br>(Verify DOM state and API network status)"]
OpenTrace --> Manual["4. Repro manually in headed browser<br>(Is it an application bug or a test bug?)"]
Manual -->|Application Bug| Report["5. File bug report with devs"]
Manual -->|Test Bug| AnalyzeTest["6. Categorize the test issue"]
AnalyzeTest -->|Locator Issue| Inspector["Verify selector with locator picker"]
AnalyzeTest -->|Timing Issue| AutoWait["Ensure await/polling is correctly used"]
Inspector --> RunIso["7. Run single spec in isolation with --debug"]
AutoWait --> RunIso
RunIso --> Pass["8. Verify fix works cleanly"]
1. Read the actual error message first, completely, before doing anything else.
This sounds obvious, and yet it’s genuinely one of the most commonly skipped steps under time pressure. Playwright’s error messages are usually specific and informative — a timeout error will typically tell you exactly which locator it was waiting on, and often even shows you how many elements it did find, if any. Skimming past this and jumping straight to re-running the test, hoping it passes the second time, throws away real, already-available information.
2. Check the trace before touching the code.
Recall Part 23 — the trace shows you exactly what the page’s DOM and network activity looked like at the moment of failure, and at every step leading up to it. Very often, the actual cause becomes obvious just from looking here — an unexpected error message that was actually on screen, a network request that returned an unexpected status, an element that was present but simply not yet visible.
3. Ask: is this the application, or is this the test?
Recall Part 0.4’s honest point — a red test doesn’t automatically mean an application bug. Reproduce the exact same steps manually, in a real browser, outside of Playwright entirely. If the application genuinely behaves the same broken way by hand, it’s likely a real application bug. If it works fine by hand, the problem is very likely in the test itself — a locator issue, a timing issue, or incorrect data.
4. If it’s a timing issue, identify exactly what the test needed to wait for, and whether it actually did.
Recall Part 12 in full here — was there a missing await? A waitForTimeout masking a real, underlying race condition? An assertion that should have used auto-retry but didn’t?
5. If it’s a locator issue, use the Inspector’s locator picker (or DevTools directly) to verify the locator against the actual current page.
Has the element’s role, text, or attributes genuinely changed? Is the locator now matching multiple elements ambiguously, where it previously matched exactly one (recall Part 7’s discussion of this exact ambiguity)?
6. Reproduce it in isolation.
If a specific test is failing within a large suite, run just that one test alone (npx playwright test login.spec.ts), ideally with --debug. This removes any possibility that some other test running before it is leaving behind unexpected, contaminating state — a genuinely real category of bug that only becomes visible once Part 27’s parallel and sequential execution nuances are fully understood.
Reading a Real Failure Message
Error: expect(locator).toBeVisible() failed
Locator: getByText('Products')
Expected: visible
Received: <element(s) not found>
Timeout: 5000ms
Call log:
- waiting for getByText('Products')
Walk through this properly, the way you now can with everything built up across this series: the assertion (Part 9) was toBeVisible(), on a locator (Part 7) searching for text “Products.” It waited — auto-retrying (Part 9, Part 12) — for the full 5-second timeout (Part 16’s expect.timeout), and never found any matching element at all, not even a hidden one.
This immediately narrows the investigation meaningfully: this isn’t a visibility timing issue (where the element exists but isn’t visible yet) — it’s an existence issue. Either the page never actually navigated to where “Products” should appear (worth checking the trace’s earlier steps), or the text itself has changed, or something upstream in the test genuinely failed before this point ever reasonably had a chance to succeed.
How It Works in a Real Test Run
Debugging is evidence reduction. Start with the failed assertion and call log, use the trace to reconstruct state, reproduce the smallest failing scope, classify application versus test failure, and change one suspected cause at a time.
Useful comparison: passes alone but fails in suite suggests shared state; passes headed but fails headless suggests timing or environment; fails only in one browser suggests compatibility; fails after a retry suggests flakiness rather than proof of health.
Interview Questions
Q: What’s the practical difference between running a test with --headed and running it with --debug?
Ans: --headed simply makes the browser visible while the test runs at its normal pace, without pausing. --debug opens the Playwright Inspector and pauses execution before the first action, letting you manually step through the test one action at a time, at your own pace — genuinely useful for closely investigating a specific point in a test rather than just passively watching it run through.
Q: Before touching any code, what should be the very first two things you check when a test fails?
Ans: The actual error message itself, read completely rather than skimmed — Playwright’s errors are typically specific about which locator or assertion failed and why. Then the trace, if available, which shows the exact DOM and network state at the moment of failure and at every step leading up to it — very often revealing the actual cause directly, without needing to guess or add debugging code.
Q: How would you determine whether a failing test represents a genuine application bug versus a problem in the test itself?
Ans: I’d reproduce the exact same steps manually, in a real browser, completely outside of Playwright. If the application genuinely behaves the same broken way by hand, it’s likely a real application bug worth reporting. If the application works correctly by hand but the test still fails, the problem is very likely in the test itself — commonly a locator issue, a timing issue, or incorrect test data.
Q: In the example error Received: <element(s) not found> for a toBeVisible() assertion, why does this narrow the investigation differently than if the error had instead shown an element that existed but wasn’t visible?
Ans: “Element(s) not found” means the locator never matched anything at all during the entire retry window — an existence problem, not a visibility timing problem. This points toward checking whether the page actually navigated to the right place, whether the target text or locator itself has changed, or whether an earlier step in the test genuinely failed before this point could reasonably succeed — rather than looking at visibility-specific causes like something being covered or still animating.
Q: Why is reproducing a failing test in isolation (running just that one test file, rather than the whole suite) a useful debugging step?
Ans: Running the full suite leaves open the possibility that some other test, running before the failing one, left behind unexpected state that’s contaminating it — a real category of bug tied to test isolation. Running the failing test completely alone removes that variable, helping determine whether the problem is genuinely intrinsic to that one test, or actually caused by interference from something else in the suite.
Q: A teammate says “the test just needs a longer timeout” every time it fails, without further investigation. What would you push back on, and why?
Ans: I’d push back on treating an increased timeout as an automatic default fix rather than an investigated conclusion — a genuinely slow but legitimate operation might warrant it, but a timeout that keeps needing to be raised is very often a sign of something else entirely, like a locator matching the wrong thing intermittently, or a genuine race condition the fixed timeout is just barely papering over. I’d want the actual error message and trace reviewed first, to understand why the timeout is being hit, before simply increasing a number and hoping the underlying problem quietly goes away.
Exercises — Part 24
Understand: Without looking back, describe, step by step, in your own words, the debugging process you’d follow starting from “a test just failed and I have no idea why” through to actually understanding the root cause.
Simple Practice:
Deliberately break a working test from earlier in this series (change a locator to something incorrect, or an assertion to an obviously wrong expected value), run it with --debug, and step through it action by action using the Inspector until you reach the point of failure.
Real-World Scenario: Take a genuinely intermittent test you’ve written earlier in this series (or deliberately introduce a timing issue using Part 19’s mocked delays), and walk through this part’s full systematic process — error message, trace, application-vs-test question, and so on — writing down your reasoning at each step as you actually diagnose it.
Challenge: Find a real, publicly documented Playwright GitHub issue or Stack Overflow question describing a confusing test failure. Without necessarily running the code yourself, apply this part’s debugging methodology to the description given, and write out what you’d investigate first, second, and third, and why, based purely on the information available in the report.
Next: Part 25 — Visual Testing
— screenshot comparison, baselines, and the specific challenges dynamic content creates for visual regression testing.
- Author
- TechByteByByte Editorial Team
- Reviewed by
- TechByteByByte Admin
- Published
- Last reviewed