
playwright-test-debugger
Debug failing Playwright E2E tests by analyzing error messages, stack traces, screenshots, and Playw
When to Use This Skill
Use this skill when you need to:
- Fix failing or flaky Playwright tests
- Understand why a test is timing out
- Debug element selector issues
- Analyze test traces and screenshots
- Resolve race conditions
- Investigate unexpected test behavior
- Use Playwright MCP to inspect live browser state
Do NOT use this skill when:
- Creating new tests (use test-generator skill)
- Building Page Objects (use page-object-builder skill)
- Refactoring test code (use test-maintainer skill)
Prerequisites
Before using this skill:
- Failing test file or error message
- Test execution output or logs
- Optional: Screenshots from test failure
- Optional: Playwright trace file
- Optional: Playwright MCP access for live debugging
Instructions
Step 1: Gather Failure Information
Ask the user for:
- Error message and stack trace
- Test file location and test name
- Expected vs actual behavior
- Screenshots (if available)
- Trace file path (if available)
- Frequency: Does it fail always or intermittently (flaky)?
- Environment: Local, CI, specific browser?
Step 2: Analyze the Error
Identify the error type:
Timeout Errors:
Timeout 30000ms exceededwaiting for selectorwaiting for navigation
Selector Errors:
Element not foundstrict mode violationNo node found
Assertion Errors:
Expected ... but received ...toBe,toContain,toBeVisiblefailures
Navigation Errors:
Target closedNavigation failedERR_CONNECTION_REFUSED
Race Conditions:
- Intermittent failures
- Works locally but fails in CI
- Different results on different runs
Step 3: Use Playwright MCP (Optional)
If Playwright MCP is available and needed:
- Use MCP tools to inspect browser state
- Navigate to the problematic page
- Check element visibility and attributes
- Verify data-testid values exist
- Test locator strategies
- Capture screenshots
Step 4: Identify Root Cause
Common root causes:
1. Missing or Wrong data-testid:
- Element has different testid than expected
- testid doesn't exist in HTML
- Multiple elements with same testid
2. Timing Issues:
- Element not yet loaded when accessed
- No explicit wait before interaction
- Network requests still pending
- Animations or transitions in progress
3. Element State:
- Element exists but not visible
- Element disabled or not clickable
- Element covered by another element
- Element in different frame/iframe
4. Test Isolation:
- Tests depend on each other
- Shared state between tests
- Cleanup not performed
- Browser context pollution
5. Environment Differences:
- Different viewport sizes
- Different network speeds
- CI vs local differences
- Browser-specific issues
Step 5: Provide Solution
For each root cause, provide:
- Explanation: What's wrong
- Fix: Code changes needed
- Prevention: How to avoid in future
- Verification: How to confirm it's fixed
Step 6: Apply the Fix
For Selector Issues:
// ❌ Before
await page.locator('[data-testid="wrong-id"]').click();
// ✅ After
await page.locator('[data-testid="correct-id"]').click();
For Timeout Issues:
// ❌ Before
await page.locator('[data-testid="submit"]').click();
// ✅ After
await page.locator('[data-testid="submit"]').waitFor({ state: "visible" });
await page.locator('[data-testid="submit"]').click();
For Race Conditions:
// ❌ Before
await page.locator('[data-testid="submit"]').click();
await expect(page.locator('[data-testid="result"]')).toBeVisible();
// ✅ After
await page.locator('[data-testid="submit"]').click();
await page.waitForLoadState("networkidle");
await expect(page.locator('[data-testid="result"]')).toBeVisible();
Step 7: Verify the Fix
Guide the user to:
- Run the test locally multiple times (3-5 times)
- Check if error is resolved
- Verify test passes consistently
- Run related tests to ensure no regression
- Consider running in CI if flakiness was CI-specific
Examples
Example 1: Timeout Error
Input:
Test failed with error:
TimeoutError: Timeout 30000ms exceeded.
=========================== logs ===========================
waiting for locator('[data-testid="submit-button"]')
Analysis:
- Timeout error waiting for element
- Element may not be loading
- Selector may be incorrect
Solution:
// Check if the data-testid is correct in the HTML
// Add explicit wait with better error message
await page.locator('[data-testid="submit-button"]').waitFor({
state: "visible",
timeout: 30000,
});
// If still failing, verify element exists in DOM
const exists = await page.locator('[data-testid="submit-button"]').count();
console.log(`Submit button count: ${exists}`); // Should be 1
// Check if page has loaded
await page.waitForLoadState("domcontentloaded");
// Final solution
await page.waitForLoadState("domcontentloaded");
await page
.locator('[data-testid="submit-button"]')
.waitFor({ state: "visible" });
await page.locator('[data-testid="submit-button"]').click();
Prevention:
- Always add explicit waits before interactions
- Verify data-testid values in HTML
- Use
waitForLoadStateafter navigation
Example 2: Element Not Found
Input:
Error: Element not found
locator.click: Target closed
locator('[data-testid="user-menu"]')
Analysis:
- Element may not exist in current page state
- Possible strict mode violation (multiple elements)
- Element may be in a different frame
Solution:
// Step 1: Verify element exists
const count = await page.locator('[data-testid="user-menu"]').count();
console.log(`Found ${count} elements`);
// If count = 0: Element doesn't exist, check data-testid in HTML
// If count > 1: Multiple elements, need to be more specific
// Step 2: If multiple elements, use .first() or filter
await page.locator('[data-testid="user-menu"]').first().click();
// Step 3: If in iframe, switch to frame first
const frame = page.frameLocator('[data-testid="app-frame"]');
await frame.locator('[data-testid="user-menu"]').click();
// Step 4: Add proper wait
await page.locator('[data-testid="user-menu"]').waitFor({ state: "attached" });
await page.locator('[data-testid="user-menu"]').click();
Prevention:
- Ensure data-testid values are unique on the page
- Check for elements in frames/iframes
- Add waits before interaction
Example 3: Flaky Test (Passes Sometimes)
Input:
Test fails intermittently:
- Passes 70% of the time locally
- Fails 90% of the time in CI
Error: expect(received).toContainText(expected)
Expected substring: "Success"
Received string: ""
Analysis:
- Classic race condition
- Element loads but content not yet populated
- Likely caused by async data fetching
- CI is slower so more likely to fail
Solution:
// ❌ Before: No wait for content
await page.locator('[data-testid="submit"]').click();
await expect(page.locator('[data-testid="message"]')).toContainText("Success");
// ✅ After: Wait for specific condition
await page.locator('[data-testid="submit"]').click();
// Option 1: Wait for network to settle
await page.waitForLoadState("networkidle");
await expect(page.locator('[data-testid="message"]')).toContainText("Success");
// Option 2: Wait for specific API call
await Promise.all([
page.waitForResponse("**/api/submit"),
page.locator('[data-testid="submit"]').click(),
]);
await expect(page.locator('[data-testid="message"]')).toContainText("Success");
// Option 3: Use Playwright's auto-waiting in assertion
await page.locator('[data-testid="submit"]').click();
await expect(page.locator('[data-testid="message"]')).toContainText("Success", {
timeout: 10000, // Explicit timeout for slow operations
});
Prevention:
- Wait for network requests to complete
- Use explicit timeouts for slow operations
- Run tests multiple times to catch flakiness
- Enable retries in playwright.config.ts
Example 4: Assertion Failure
Input:
Error: expect(received).toBeVisible()
locator('[data-testid="success-message"]')
Expected: visible
Received: hidden
Analysis:
- Element exists but is hidden
- May need to wait for element to appear
- Check if element has conditional visibility
- Verify test logic is correct
Solution:
// Step 1: Verify element exists
const exists = await page.locator('[data-testid="success-message"]').count();
console.log(`Element count: ${exists}`);
// Step 2: Check element state
const isVisible = await page
.locator('[data-testid="success-message"]')
.isVisible();
console.log(`Is visible: ${isVisible}`);
// Step 3: Wait for visibility with timeout
await page.locator('[data-testid="success-message"]').waitFor({
state: "visible",
timeout: 10000,
});
// Step 4: If still not visible, check CSS
const display = await page
.locator('[data-testid="success-message"]')
.evaluate((el) => window.getComputedStyle(el).display);
console.log(`Display property: ${display}`);
// Step 5: Final solution
await page.locator('[data-testid="submit"]').click();
await page.waitForLoadState("networkidle");
await expect(page.locator('[data-testid="success-message"]')).toBeVisible({
timeout: 10000,
});
Prevention:
- Add explicit waits for elements that appear after actions
- Verify success conditions in the application logic
- Use appropriate timeout values
Example 5: Using Playwright MCP for Debugging
Input: "My test is failing but I can't figure out why. The error says element not found but I see it in the screenshot."
Solution (Using MCP):
1. Use Playwright MCP to navigate to the page:
- Navigate to the page where test fails
- Take screenshot to verify page state
2. Use MCP to check if element exists:
- Use MCP to find elements by data-testid
- Check how many elements match
- Inspect element attributes
3. Use MCP to test the locator:
- Try different locator strategies
- Check element visibility
- Verify element is in correct frame
4. Based on MCP findings, update the test:
- If element has different testid: Update locator
- If element in iframe: Add frame handling
- If multiple matches: Make locator more specific
Best Practices
Debugging Process
- Read error carefully: Error messages are usually accurate
- Check test in isolation: Run the single failing test
- Use debugging tools: Screenshots, traces, MCP
- Add console logs: Temporary logs to understand state
- Verify assumptions: Check that data-testid values are correct
- Test incrementally: Fix one issue at a time
Common Debugging Techniques
- Add explicit waits: Most failures are timing-related
- Check element count: Verify unique selectors
- Use page.pause(): Interactive debugging mode
- Enable headed mode: See what's happening visually
- Slow motion: Add
slowMoin config to slow down actions - Check traces: Use Playwright trace viewer
Preventing Future Issues
- Always use data-testid: Stable locators
- Add explicit waits: Don't rely on auto-waiting alone
- Test isolation: Each test should be independent
- Proper cleanup: Reset state between tests
- Handle async: Wait for network/animations
- Run multiple times: Catch flaky tests early
Common Issues and Solutions
Issue 1: Test Passes Locally but Fails in CI
Problem: Test works on developer machine but fails in CI environment
Solutions:
-
Viewport difference: CI may use different screen size
await page.setViewportSize({ width: 1920, height: 1080 }); -
Slower CI: Increase timeouts for CI
timeout: process.env.CI ? 60000 : 30000; -
Headless issues: Test in headless mode locally
npx playwright test --headed=false -
Network speed: Add retries in config for CI
Issue 2: Cannot Find Element with Correct data-testid
Problem: Selector looks correct but element not found
Solutions:
-
Check element is in main page, not iframe
-
Verify element is not dynamically loaded later
-
Check for typos in data-testid value
-
Use Playwright MCP to inspect actual HTML
-
Add wait for element to be added to DOM
await page.waitForSelector('[data-testid="element"]');
Issue 3: Test Works First Time but Fails on Reruns
Problem: First run passes, subsequent runs fail
Solutions:
-
State leaking: Tests aren't isolated
- Use
test.beforeEachto reset state - Use fixtures for clean context
- Use
-
Storage persistence: Clear local storage/cookies
await page.context().clearCookies(); await page.evaluate(() => localStorage.clear()); -
Database state: Reset test database between runs
Issue 4: Element Found but Click Doesn't Work
Problem: element.click() doesn't do anything or throws error
Solutions:
-
Element covered: Another element is covering it
await page.locator('[data-testid="modal-close"]').click({ force: true }); -
Element disabled: Check if element is enabled
await expect(page.locator('[data-testid="submit"]')).toBeEnabled(); -
Wrong element: Multiple elements match, clicking wrong one
await page.locator('[data-testid="item"]').first().click(); -
Animation in progress: Wait for animations to complete
await page.waitForTimeout(500); // Avoid this // Better: Wait for element to be stable await page.locator('[data-testid="element"]').waitFor({ state: "visible" }); await page.waitForLoadState("networkidle");
Issue 5: Assertion Timing Out
Problem: expect() assertion times out after 5 seconds
Solutions:
-
Increase timeout: For slow operations
await expect(locator).toBeVisible({ timeout: 15000 }); -
Wait for condition: Add wait before assertion
await page.waitForLoadState("networkidle"); await expect(locator).toBeVisible(); -
Wrong expectation: Verify what you're asserting is correct
- Check expected text/value in application
- Verify element actually appears on success
Resources
The resources/ directory contains helpful references:
debugging-checklist.md- Step-by-step debugging guidecommon-errors.md- List of common errors and quick fixesplaywright-commands.md- Useful Playwright debugging commands