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-debugging

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
13.0 KB
---
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`.

Overview

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.

How this skill works

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.

When to use it

  • A script that used to work now fails after a site or test change
  • Intermittent (flaky) tests that pass sometimes and fail other times
  • "Element not found", "not visible", or timeout errors during actions
  • Tests that behave differently in CI than locally
  • When selectors seem brittle or elements are intermittently covered/disabled

Best practices

  • Reproduce the failure in headed mode and isolate a single failing test before debugging
  • Prefer condition-based waits (expect().toBeVisible(), waitForResponse) over fixed timeouts
  • Use Playwright UI, --debug, and trace viewer to get time-travel logs and screenshots
  • Re-query locators after navigation; never hold stale element handles across page changes
  • Use role-based or data-testid selectors to reduce brittleness and improve stability

Example use cases

  • Replace waitForTimeout calls with expect(locator).toBeVisible() to fix flaky timing issues
  • Run npx playwright test --ui to visually step through a failing workflow and edit selectors live
  • Record a trace for a failing CI run and analyze network/DOM snapshots in the trace viewer
  • Diagnose "element not clickable" by checking computed styles, z-index, overlays, and animations
  • Fix a form submission failure by inspecting form.action, triggering blur/input events, or calling form.submit()

FAQ

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.