home / skills / ed3dai / ed3d-plugins / playwright-debugging
This skill provides a systematic Playwright debugging workflow to diagnose flaky tests, timeouts, and element interactability issues quickly.
npx playbooks add skill ed3dai/ed3d-plugins --skill playwright-debuggingReview the files below or copy the command above to add this skill to your agents.
---
name: playwright-debugging
description: Use when Playwright scripts fail, tests are flaky, selectors stop working, or timeouts occur - provides systematic debugging approach for browser automation issues
user-invocable: false
---
# Playwright Debugging
## Overview
Browser automation failures fall into predictable categories. This skill provides a systematic approach to diagnose and fix issues quickly.
## When to Use
- Scripts that worked before now fail
- Intermittent test failures (flakiness)
- "Element not found" errors
- Timeout errors
- Unexpected behavior in automation
- Elements not interactable
**When NOT to use:**
- Writing new automation (use playwright-patterns skill)
- API or backend debugging
## Quick Reference
| 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 |
## Diagnostic Framework
### 1. Reproduce and Isolate
**First step: Can you reproduce it?**
```javascript
// 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:**
- Does it fail consistently or intermittently?
- Does it fail in all browsers or just one?
- Does it fail in headed and headless mode?
- Did something change recently (site update, code change)?
### 2. Add Visibility
**Use UI Mode for interactive debugging:**
```bash
# Best for local development - provides time-travel debugging
npx playwright test --ui
```
UI Mode gives you:
- Visual timeline of all actions
- Watch mode for re-running on file changes
- Network and console tabs
- Time-travel through test execution
**Use Inspector to step through tests:**
```bash
# Step through test execution with live browser
npx playwright test --debug
```
Inspector allows:
- Stepping through actions one at a time
- Picking locators directly from the browser
- Editing selectors live and seeing results
- Viewing actionability logs
**Take screenshots at failure point:**
```javascript
// 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:**
```bash
# 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:
```javascript
// 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:**
```javascript
// 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:**
```javascript
// 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:**
```javascript
// 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:
- Live debugging with breakpoints in VS Code
- Locator highlighting in browser while editing
- "Show Browser" option for real-time feedback
- Right-click "Debug Test" on any test
This integrates debugging directly into your editor workflow.
### 3. Inspect Element State
**Check if element exists:**
```javascript
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);
```
### 4. Verify Selector
**Test selector in browser console:**
```javascript
// 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:**
```javascript
// 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);
```
## Common Issues and Fixes
### Issue: Element Not Found
**Causes:**
- Selector is wrong
- Element hasn't loaded yet
- Element is in iframe
- Element is dynamically created
**Debug steps:**
```javascript
// 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);
```
### Issue: Element Not Visible/Clickable
**Causes:**
- Element is hidden (CSS: display:none, visibility:hidden)
- Element is covered by another element
- Element is outside viewport
- Element hasn't finished animating
**Debug steps:**
```javascript
// 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 });
```
### Issue: Timing/Race Conditions
**Causes:**
- Network requests not complete
- JavaScript still executing
- Animations in progress
- Dynamic content loading
**Debug steps:**
```javascript
// 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);
```
### Issue: Stale Element Reference
**Causes:**
- Page refreshed or navigated
- Element was removed and re-added to DOM
- Dynamic content replaced element
**Fix:**
```javascript
// 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
```
### Issue: Form Submission Not Working
**Causes:**
- JavaScript validation preventing submit
- Event listeners not attached yet
- Form action not set correctly
**Debug steps:**
```javascript
// 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"]', '[email protected]');
await page.dispatchEvent('input[name="email"]', 'blur');
// 3. Use form.submit() instead of clicking button
await page.evaluate(() => document.querySelector('form').submit());
```
## Common Mistakes
| 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 |
## Debugging Checklist
When automation fails, check in this order:
1. ☐ Can I reproduce the failure consistently?
2. ☐ Does it fail in headed mode with slow motion?
3. ☐ Have I taken screenshots before/after the failure?
4. ☐ Does the selector actually match an element?
5. ☐ Is the element visible and enabled?
6. ☐ Is the element in an iframe?
7. ☐ Have I waited for page load to complete?
8. ☐ Is there dynamic content that needs time to load?
9. ☐ Are there network requests still in flight?
10. ☐ Have I checked browser console for JavaScript errors?
## Debugging Tools Reference
| 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 |
## Flakiness Patterns
### Flaky: Works 80% of the time
**Likely cause:** Race condition
**Fix:**
```javascript
// Replace arbitrary waits
await page.waitForTimeout(2000); // BAD
// With condition-based waits
await expect(page.locator('.result')).toBeVisible(); // GOOD
```
### Flaky: Fails on CI but works locally
**Likely cause:** Timing differences
**Fix:**
```javascript
// Increase default timeout for CI
test.setTimeout(60000);
page.setDefaultTimeout(30000);
// Wait for network idle
await page.waitForLoadState('networkidle');
```
### Flaky: Fails with "element not clickable"
**Likely cause:** Overlapping elements or animations
**Fix:**
```javascript
// 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();
```
## Remember
**Debugging priorities:**
1. Reproduce the issue reliably
2. Add visibility (screenshots, logs, traces)
3. Verify element state and selector
4. Check timing and waits
5. Test in different modes (headed, browsers)
**Auto-waiting advantages:**
Playwright automatically waits for elements to be:
- Attached to DOM
- Visible
- Enabled and stable
- Not covered by overlays
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`.
This skill provides a systematic Playwright debugging workflow for diagnosing and fixing browser automation failures quickly. It focuses on reproducible steps, visibility tools (UI, debugger, traces), selector and element-state checks, and race-condition fixes. Use it when tests are flaky, selectors stop working, or timeouts occur to get actionable next steps and concrete checks.
The skill walks you through reproduce-and-isolate steps, adds visibility using Playwright UI/Inspector/trace/screenshot tools, and inspects element state and selectors with small code snippets. It highlights common failure categories (not found, not visible/clickable, timing/race conditions, stale references, form issues) and gives targeted fixes like condition-based waits, re-querying locators, and trace analysis. Practical commands and tiny code patterns are provided so you can apply fixes immediately.
What first step should I take when a test fails intermittently?
Run the single test in headed mode (--headed) or slow motion (--slow-mo) to observe behavior and determine if failure is a race condition or environment difference.
When should I use force clicks or force options?
Only as a last resort after diagnosing why the element is not actionable; use force-clicking sparingly because it masks the root cause.