Help us improve
Share bugs, ideas, or general feedback.
From playwright
Debug failing Playwright E2E tests by analyzing error messages, stack traces, screenshots, and Playwright traces. Provides actionable solutions for common test failures including timeouts, selector issues, race conditions, and unexpected behaviors. Optionally uses Playwright MCP for live debugging.
npx claudepluginhub joel611/claude-plugins --plugin playwrightHow this skill is triggered — by the user, by Claude, or both
Slash command
/playwright:test-debuggerThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use this skill when you need to:
Investigates and fixes failing Playwright E2E tests using captured action data, screenshots, DOM snapshots, network requests, and console output.
Debugs Playwright test failures, flakiness, selector issues, timeouts using UI mode, inspector, screenshots, slow-mo, and verbose logging.
Share bugs, ideas, or general feedback.
Use this skill when you need to:
Do NOT use this skill when:
Before using this skill:
Ask the user for:
Identify the error type:
Timeout Errors:
Timeout 30000ms exceededwaiting for selectorwaiting for navigationSelector Errors:
Element not foundstrict mode violationNo node foundAssertion Errors:
Expected ... but received ...toBe, toContain, toBeVisible failuresNavigation Errors:
Target closedNavigation failedERR_CONNECTION_REFUSEDRace Conditions:
If Playwright MCP is available and needed:
Common root causes:
1. Missing or Wrong data-testid:
2. Timing Issues:
3. Element State:
4. Test Isolation:
5. Environment Differences:
For each root cause, provide:
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();
Guide the user to:
Input:
Test failed with error:
TimeoutError: Timeout 30000ms exceeded.
=========================== logs ===========================
waiting for locator('[data-testid="submit-button"]')
Analysis:
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:
waitForLoadState after navigationInput:
Error: Element not found
locator.click: Target closed
locator('[data-testid="user-menu"]')
Analysis:
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:
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:
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:
Input:
Error: expect(received).toBeVisible()
locator('[data-testid="success-message"]')
Expected: visible
Received: hidden
Analysis:
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:
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
slowMo in config to slow down actionsProblem: 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
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"]');
Problem: First run passes, subsequent runs fail
Solutions:
State leaking: Tests aren't isolated
test.beforeEach to reset stateStorage persistence: Clear local storage/cookies
await page.context().clearCookies();
await page.evaluate(() => localStorage.clear());
Database state: Reset test database between runs
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");
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
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