Chapter 19 · Debugging Techniques and Flaky Test Resolution
Chapter goals: Master Playwright's official debug toolchain (Inspector, headed mode, Trace Viewer), build a root-cause analysis process for flaky tests, and make the test suite trustworthy again.
19.1 Why E2E Tests Become "Randomly Failing"
Flaky tests — the same code, the same command, sometimes pass sometimes fail — are E2E testing's biggest enemy. Once a team gets used to "red means re-run", the entire suite loses credibility. Common root causes fall into four categories:
| Root Cause Category | Typical Symptom | Direction of Fix |
|---|---|---|
| Animations / transitions | Element "invisible" or position shifted | Wait for animation to end, or disable via CSS |
| Network race conditions | Assertion fires before data renders | Use Web-First assertions with auto-retry (Chapter 5) |
| Time dependency | Cases only fail at midnight / month-end | Inject a fixed clock; never depend on real time |
| Shared data | Fails in parallel, passes serially | Test-level data isolation (Chapters 6/13) |
First principle
When encountering flakiness, the first response should not be "add more retries" — it should be root-cause analysis first. Retries are painkillers: they reduce the failure probability but the root cause remains, and it will eventually explode in a more隐蔽 way.
19.2 PWDEBUG and Inspector: The Official Debug Entry
Playwright's officially recommended debugging method is setting the environment variable PWDEBUG=1. It simultaneously enables three tools:
- Inspector window: visual step-by-step test execution;
- Headed browser (headed mode): see the page change in real time;
- Timeout zeroed: all auto-waits no longer time out, letting you observe at your own pace.
# macOS / Linux: run in debug mode (with -s to see print output)
PWDEBUG=1 pytest -s test_login.py::test_login_success
# Windows PowerShell
$env:PWDEBUG=1; pytest -s test_login.py::test_login_successThe Inspector toolbar supports play / pause / step: each action (click, fill) highlights the corresponding code line and the target element on the page simultaneously — you can watch frame by frame what the test actually does.
19.3 page.pause(): Start Debugging from a Breakpoint
If the test is long and step-by-step is too slow, insert a hard breakpoint where you care:
def test_checkout_flow(page):
page.goto("https://shop.example.com/cart")
page.get_by_role("button", name="Checkout").click()
# Pause from here; earlier steps run through directly
page.pause()
page.get_by_label("Shipping address").fill("1 XX Road, Chaoyang, Beijing")
page.get_by_role("button", name="Submit order").click()
expect(page.get_by_text("Order created")).to_be_visible()page.pause() only takes effect when PWDEBUG=1 (ignored in normal runs), so it's safe to leave in code for temporary debugging. Two high-frequency features when Inspector pauses at the breakpoint:
- Pick Locator: hover any element on the page; Playwright auto-generates a resilient locator using role/text/testid;
- Live editing: modify the expression in the Inspector's locator input box directly; matched elements highlight in real time — tweak until satisfied, then copy back to code.
19.4 Trace Viewer: Best Evidence for Post-Mortem
Inspector is for "debug on the spot"; Trace is for "post-failure review". We configured --tracing retain-on-failure in Chapter 11; now let's use it to review an intermittent failure:
# Failed cases leave a trace zip in test-results/
playwright show-trace test-results/test-cart-test-checkout-chromium.zipEach tab in Trace Viewer is a different information dimension:
- Actions: timing and result of every action; red = failure point; hover to see before/after DOM snapshots;
- Log: action-level detailed logs (waiting for elements, scrolling, injecting events…) — see exactly what Playwright retried before timeout;
- Network: all requests and responses at that moment; spot "API didn't return" race conditions at a glance;
- Console / Errors: frontend JS errors are often the real cause of test failures.
Debugging routine
Look at Actions to find the failing action → switch to Log to see what it waited for → go to Network/Console to find "what didn't arrive". 90% of flaky cases can be traced along this path.
19.5 Waiting Strategy Correction: Stop Using sleep as a Crutch
Recurring time.sleep() or page.wait_for_timeout() is the strongest signal that a test is masking a race condition rather than solving it. The correct hierarchy:
- Auto-wait (built into every action) — sufficient for 95% of cases;
- Web-First assertions (
expect().to_be_visible()) — for conditional state changes; - Explicit waits (
wait_for_url,expect_response) — for event-driven changes; wait_for_timeout— last resort, only when you know exactly why and for how long.
If you find yourself needing wait_for_timeout, ask: "what condition am I actually waiting for?" — the answer is almost always "the element to appear", which expect already handles.
19.6 Chapter Summary
- Flaky roots: animation, network race, time dependency, shared data — categorize before treating;
PWDEBUG=1opens Inspector + headed + zero-timeout simultaneously;page.pause()inserts a breakpoint that only fires in debug mode;- Trace Viewer is the ultimate post-mortem tool: Actions → Log → Network → Console;
- Never use
sleeporwait_for_timeoutas a crutch — fix the waiting strategy instead.
🧪 随堂测验
点击你认为正确的选项。答错时会展示正确答案与原因解析。
1. What is the first principle when a test turns flaky?
2. What does PWDEBUG=1 enable all at once?
3. When should wait_for_timeout be used?
4. In Trace Viewer, what is the recommended investigation order?
🛠️ Hands-On Exercises
- Write a test that intentionally fails due to an animation race; run with
PWDEBUG=1and use Inspector to step through and observe the auto-wait behavior. - Add
page.pause()mid-test; run withPWDEBUG=1 pytest -s; use Pick Locator and Live Editing in Inspector to refine a flaky selector. - Trigger a failure with
--tracing retain-on-failure; open the trace withplaywright show-traceand follow the Actions → Log → Network path to identify the root cause.
Proceed to Chapter 20: Accessibility Testing and Aria Snapshot.