Help us improve
Share bugs, ideas, or general feedback.
From ed3d-playwright
Debugs Playwright test failures, flakiness, selector issues, timeouts using UI mode, inspector, screenshots, slow-mo, and verbose logging.
npx claudepluginhub ed3dai/ed3d-plugins --plugin ed3d-playwrightHow this skill is triggered — by the user, by Claude, or both
Slash command
/ed3d-playwright:playwright-debuggingThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Browser automation failures fall into predictable categories. This skill provides a systematic approach to diagnose and fix issues quickly.
Provides Playwright patterns for resilient selectors, locator chaining, filtering, and waiting strategies to minimize flakiness in E2E tests and web scrapers.
Writes Playwright E2E tests using Page Object Model, sets up test infrastructure and fixtures, debugs flaky tests, adds CI integration, implements API mocking, and performs visual regression testing.
Share bugs, ideas, or general feedback.
Browser automation failures fall into predictable categories. This skill provides a systematic approach to diagnose and fix issues quickly.
When NOT to use:
| Problem | First Action |
|---|---|
| Timeout on locator | Run with --ui mode, check element state with .count(), .isVisible() |
| Flaky test (passes sometimes) | Replace waitForTimeout() with condition-based waits |
| "Element not visible" | Check computed styles, wait for overlays to disappear |
| Works locally, fails CI | Use waitForLoadState('networkidle'), increase timeout |
| Element not clickable | Check if covered by overlay, wait for animations to complete |
| Stale element | Re-query after navigation instead of storing locator |
First step: Can you reproduce it?
// Run single test to isolate issue
npx playwright test path/to/test.spec.js
// Run with headed mode to observe
npx playwright test --headed
// Run with slow motion
npx playwright test --headed --slow-mo=1000
Questions to answer:
Use UI Mode for interactive debugging:
# Best for local development - provides time-travel debugging
npx playwright test --ui
UI Mode gives you:
Use Inspector to step through tests:
# Step through test execution with live browser
npx playwright test --debug
Inspector allows:
Take screenshots at failure point:
// Before failing action
await page.screenshot({ path: 'before-action.png', fullPage: true });
// Try action
try {
await page.click('.button');
} catch (error) {
await page.screenshot({ path: 'after-error.png', fullPage: true });
throw error;
}
Enable verbose logging:
# API-level debugging
DEBUG=pw:api npx playwright test
# Browser DevTools with playwright object
PWDEBUG=console npx playwright test
With PWDEBUG=console, you get DevTools access to:
// In browser console
playwright.$('.selector') // Query with Playwright engine
playwright.$$('selector') // Get all matches
playwright.inspect('selector') // Highlight in Elements panel
playwright.locator('selector') // Create locator
Use trace viewer:
// Record trace
await context.tracing.start({ screenshots: true, snapshots: true });
// ... your test code
await context.tracing.stop({ path: 'trace.zip' });
// View trace
npx playwright show-trace trace.zip
Organize traces with test steps:
// Group actions in trace viewer
await test.step('Login', async () => {
await page.fill('input[name="username"]', 'user');
await page.click('button[type="submit"]');
});
await test.step('Navigate to dashboard', async () => {
await page.click('a[href="/dashboard"]');
});
Add descriptions to locators for clarity:
// Descriptions appear in trace viewer and reports
const submitButton = page.locator('#submit').describe('Submit button');
await submitButton.click();
VS Code debugging:
Install the Playwright VS Code extension for:
This integrates debugging directly into your editor workflow.
Check if element exists:
const element = page.locator('.button');
// Does it exist in DOM?
const count = await element.count();
console.log(`Found ${count} elements`);
// Is it visible?
const isVisible = await element.isVisible();
console.log(`Visible: ${isVisible}`);
// Is it enabled?
const isEnabled = await element.isEnabled();
console.log(`Enabled: ${isEnabled}`);
// Get all attributes
const attrs = await element.evaluate(el => ({
classes: el.className,
id: el.id,
display: window.getComputedStyle(el).display,
visibility: window.getComputedStyle(el).visibility,
opacity: window.getComputedStyle(el).opacity
}));
console.log(attrs);
Test selector in browser console:
// Use page.evaluate to test selector
const found = await page.evaluate(() => {
const el = document.querySelector('.button');
return el ? {
text: el.textContent,
visible: el.offsetParent !== null,
enabled: !el.disabled
} : null;
});
console.log('Selector test:', found);
Check for multiple matches:
// Are there multiple elements?
const all = await page.locator('.button').all();
console.log(`Found ${all.length} matching elements`);
// Get text of all matches
const texts = await page.locator('.button').allTextContents();
console.log('All matching texts:', texts);
Causes:
Debug steps:
// 1. Check if selector exists at all
const exists = await page.locator('.button').count() > 0;
console.log('Element exists:', exists);
// 2. Wait for element explicitly (modern approach)
await page.locator('.button').waitFor({ timeout: 10000 });
// Or let auto-waiting handle it:
await page.locator('.button').click();
// 3. Check if in iframe
const frame = page.frameLocator('iframe');
await frame.locator('.button').click();
// 4. Dump all matching elements
const all = await page.evaluate(() => {
return Array.from(document.querySelectorAll('button')).map(el => ({
text: el.textContent,
classes: el.className,
id: el.id
}));
});
console.log('All buttons on page:', all);
Causes:
Debug steps:
// 1. Check computed styles
const styles = await page.locator('.button').evaluate(el => ({
display: window.getComputedStyle(el).display,
visibility: window.getComputedStyle(el).visibility,
opacity: window.getComputedStyle(el).opacity,
zIndex: window.getComputedStyle(el).zIndex
}));
console.log('Element styles:', styles);
// 2. Scroll into view
await page.locator('.button').scrollIntoViewIfNeeded();
// 3. Wait for element to be stable (not animating)
await expect(page.locator('.button')).toBeVisible();
await page.waitForTimeout(100); // Brief wait for animation
// 4. Force click if needed (last resort)
await page.locator('.button').click({ force: true });
Causes:
Debug steps:
// 1. Wait for network to be idle
await page.goto('https://example.com');
await page.waitForLoadState('networkidle');
// 2. Wait for specific network request
await page.waitForResponse(resp =>
resp.url().includes('/api/data') && resp.status() === 200
);
// 3. Wait for JavaScript condition
await page.waitForFunction(() =>
window.dataLoaded === true
);
// 4. Wait for element count to stabilize
await expect(page.locator('.item')).toHaveCount(10);
Causes:
Fix:
// DON'T store element handles across navigation
const button = page.locator('.button'); // BAD: might become stale
await page.goto('/other-page');
await button.click(); // ERROR: stale
// DO re-query after navigation
await page.goto('/other-page');
await page.locator('.button').click(); // GOOD: fresh query
Causes:
Debug steps:
// 1. Verify form state before submit
const formState = await page.evaluate(() => {
const form = document.querySelector('form');
return {
action: form?.action,
method: form?.method,
valid: form?.checkValidity()
};
});
console.log('Form state:', formState);
// 2. Trigger form events manually
await page.fill('input[name="email"]', 'test@example.com');
await page.dispatchEvent('input[name="email"]', 'blur');
// 3. Use form.submit() instead of clicking button
await page.evaluate(() => document.querySelector('form').submit());
| Mistake | Why It's Wrong | Right Approach |
|---|---|---|
Adding waitForTimeout(5000) | Masks timing issues, makes tests slower, unreliable | Use condition-based waits: expect().toBeVisible() |
| Force-clicking without understanding why | Bypasses Playwright's actionability checks | Diagnose WHY element isn't clickable, fix root cause |
| Not using modern debugging tools | Slower diagnosis, guessing at issues | Start with --ui or --debug for visual debugging |
| Testing only in headed mode | Hides timing issues that appear in CI | Always test in headless mode too |
| Using brittle selectors | Breaks when HTML structure changes | Use role-based or data-testid selectors |
| Skipping trace viewer | Miss detailed timeline of what happened | Enable tracing for failing tests |
When automation fails, check in this order:
| Tool | Command | Use When |
|---|---|---|
| UI Mode | --ui | Time-travel debugging with visual timeline (best for local dev) |
| Inspector | --debug | Step through test execution, pick locators live |
| Headed mode | --headed | Need to see browser |
| Slow motion | --slow-mo=1000 | Actions too fast to observe |
| Debug mode | PWDEBUG=1 | Open Inspector (older approach, prefer --debug) |
| Console debug | PWDEBUG=console | Access browser DevTools with playwright object |
| Trace viewer | show-trace trace.zip | Need full timeline analysis |
| Screenshot | page.screenshot() | Need visual evidence |
| Console logs | DEBUG=pw:api | Need API call details |
| Pause | await page.pause() | Need to inspect manually |
Likely cause: Race condition
Fix:
// Replace arbitrary waits
await page.waitForTimeout(2000); // BAD
// With condition-based waits
await expect(page.locator('.result')).toBeVisible(); // GOOD
Likely cause: Timing differences
Fix:
// Increase default timeout for CI
test.setTimeout(60000);
page.setDefaultTimeout(30000);
// Wait for network idle
await page.waitForLoadState('networkidle');
Likely cause: Overlapping elements or animations
Fix:
// Wait for element to be actionable
await expect(page.locator('.button')).toBeVisible();
await expect(page.locator('.button')).toBeEnabled();
// Or wait for overlay to disappear
await expect(page.locator('.loading-overlay')).not.toBeVisible();
Debugging priorities:
Auto-waiting advantages: Playwright automatically waits for elements to be:
Most actions (click, fill, etc.) include auto-waiting. Explicit waits are only needed for complex conditions.
Most Playwright issues are timing-related. Replace arbitrary timeouts with condition-based waits. When in doubt, slow down and observe in headed mode with --ui or --debug.