Playwright Debugging logo

Playwright Debugging

Organization
ed3dai
playwright-debugging

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

Overview

Publishered3dai
Repositoryed3d-plugins
Skill nameplaywright-debugging
Stars
249
Forks
33
Bundled files
Instructions only
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • Self-contained

    Everything the model needs lives in the instructions — no extra files to sync.

  • Open source

    Published by ed3dai on GitHub. Read the source before you install it.

Installation

Install the Playwright Debugging AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/ed3dai/ed3d-plugins.git /tmp/ed3d-plugins
mkdir -p .claude/skills
cp -r /tmp/ed3d-plugins/plugins/ed3d-playwright/skills/playwright-debugging .claude/skills/playwright-debugging
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Playwright Debugging in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Playwright Debugging on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Playwright Debugging is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

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

ProblemFirst Action
Timeout on locatorRun 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 CIUse waitForLoadState('networkidle'), increase timeout
Element not clickableCheck if covered by overlay, wait for animations to complete
Stale elementRe-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"]', '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

MistakeWhy It's WrongRight Approach
Adding waitForTimeout(5000)Masks timing issues, makes tests slower, unreliableUse condition-based waits: expect().toBeVisible()
Force-clicking without understanding whyBypasses Playwright's actionability checksDiagnose WHY element isn't clickable, fix root cause
Not using modern debugging toolsSlower diagnosis, guessing at issuesStart with --ui or --debug for visual debugging
Testing only in headed modeHides timing issues that appear in CIAlways test in headless mode too
Using brittle selectorsBreaks when HTML structure changesUse role-based or data-testid selectors
Skipping trace viewerMiss detailed timeline of what happenedEnable 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

ToolCommandUse When
UI Mode--uiTime-travel debugging with visual timeline (best for local dev)
Inspector--debugStep through test execution, pick locators live
Headed mode--headedNeed to see browser
Slow motion--slow-mo=1000Actions too fast to observe
Debug modePWDEBUG=1Open Inspector (older approach, prefer --debug)
Console debugPWDEBUG=consoleAccess browser DevTools with playwright object
Trace viewershow-trace trace.zipNeed full timeline analysis
Screenshotpage.screenshot()Need visual evidence
Console logsDEBUG=pw:apiNeed API call details
Pauseawait 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.

Frequently asked questions

What does the Playwright Debugging AI skill do?

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

Why use Playwright Debugging on TypingMind?

Because you install it once and use it with any model. Playwright Debugging is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Playwright Debugging in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/ed3dai/ed3d-plugins/tree/main/plugins/ed3d-playwright/skills/playwright-debugging. TypingMind reads its SKILL.md and installs it as a skill you can enable per chat.

Which AI models can use Playwright Debugging?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Playwright Debugging?

As many as you like. As long as a model supports skills, you can use Playwright Debugging with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Playwright Debugging AI skill free?

It is published on GitHub by ed3dai. Check the repository for licensing terms. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇