Agent Skills: Playwright Debugging

Use when Playwright scripts fail, tests are flaky, selectors stop working, or timeouts occur - provides systematic debugging approach for browser automation issues

UncategorizedID: ed3dai/ed3d-plugins/playwright-debugging

Install this agent skill to your local

pnpm dlx add-skill https://github.com/ed3dai/ed3d-plugins/tree/HEAD/plugins/ed3d-playwright/skills/playwright-debugging

Skill Files

Browse the full folder contents for playwright-debugging.

Download Skill

Loading file tree…

plugins/ed3d-playwright/skills/playwright-debugging/SKILL.md

Skill Metadata

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

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?

// 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:

# 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:

# 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:

// 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:

  • 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:

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:

// 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);

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:

// 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:

// 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:

// 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:

// 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:

// 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());

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:

// 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:

// 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:

// 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.