/playwright-patterns
Use when writing Playwright automation code, building web scrapers, or creating E2E tests - provides best practices for selector strategies, waiting patterns, and robust automation that minimizes flakiness
$ npx -y skills add ed3dai/ed3d-plugins --skill playwright-patterns --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.
- You can call itInvoke it directly when you want it.
- Slash command
/playwright-patterns
Context preview
The summary Claude sees to decide when to auto-load this skill.
Use when writing Playwright automation code, building web scrapers, or creating E2E tests - provides best practices for selector strategies, waiting patterns, and robust automation that minimizes flakiness
SKILL.md
playwright-patterns.SKILL.mdname: playwright-patterns
description: Use when writing Playwright automation code, building web scrapers, or creating E2E tests - provides best practices for selector strategies, waiting patterns, and robust automation that minimizes flakiness
user-invocable: false
Playwright Automation Patterns
Overview
Reliable browser automation requires strategic selector choice, proper waiting, and defensive coding. This skill provides patterns that minimize test flakiness and maximize maintainability.
When to Use
- Writing new Playwright scripts or tests
- Debugging flaky automation
- Refactoring unreliable selectors
- Building web scrapers that need to handle dynamic content
- Creating E2E tests that must be maintainable
**When NOT to use:**
- Simple one-time browser tasks
- When you need Playwright API documentation (use context7 MCP)
Selector Strategy
Priority Order
Use user-facing locators first (most resilient), then test IDs, then CSS/XPath as last resort:
1. **Role-based locators** (best - user-centric)
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com');2. **Other user-facing locators**
await page.getByLabel('Password').fill('secret');
await page.getByPlaceholder('Search...').fill('query');
await page.getByText('Submit Order').click();3. **Test ID attributes** (explicit contract)
// Default uses data-testid
await page.getByTestId('submit-button').click();
// Can customize in playwright.config.ts:
// use: { testIdAttribute: 'data-pw' }4. **CSS/ID selectors** (fragile, avoid if possible)
await page.locator('#submit-btn').click();
await page.locator('.btn.btn-primary.submit').click();Strictness and Specificity
Locators are strict by default - operations throw if multiple elements match:
// ERROR if 2+ buttons exist
await page.getByRole('button').click();
// Solutions:
// 1. Make locator more specific
await page.getByRole('button', { name: 'Submit' }).click();
// 2. Filter to narrow down
await page.getByRole('button')
.filter({ hasText: 'Submit' })
.click();
// 3. Chain locators to scope
await page.locator('.product-card')
.getByRole('button', { name: 'Add to cart' })
.click();
// Avoid: Using first() makes tests fragile
await page.getByRole('button').first().click(); // Don't do thisLocator Filtering and Chaining
// Filter by text content
await page.getByRole('listitem')
.filter({ hasText: 'Product 2' })
.getByRole('button')
.click();
// Filter by child element
await page.getByRole('listitem')
.filter({ has: page.getByRole('heading', { name: 'Product 2' }) })
.getByRole('button', { name: 'Buy' })
.click();
// Filter by NOT having text
await expect(
page.getByRole('listitem')
.filter({ hasNot: page.getByText('Out of stock') })
).toHaveCount(5);
// Handle "either/or" scenarios
const loginOrWelcome = await page.getByRole('button', { name: 'Login' })
.or(page.getByText('Welcome back'))
.first();
await expect(loginOrWelcome).toBeVisible();Anti-Patterns to Avoid
❌ **Fragile CSS paths**
// BAD: Breaks when HTML structure changes
await page.click('div.container > div:nth-child(2) > button.submit');✅ **Stable semantic selectors**
// GOOD: Survives structural changes
await page.getByRole('button', { name: 'Submit' }).click();❌ **XPath with positions**
// BAD: Brittle
await page.locator('xpath=//div[3]/button[1]').click();✅ **XPath with content**
// BETTER: More stable
await page.locator('xpath=//button[contains(text(), "Submit")]').click();Waiting Patterns
Built-in Auto-Waiting
Playwright auto-waits before most actions. Trust it.
// Auto-waits for element to be visible, enabled, and stable
await page.click('button');
await page.fill('input[name="email"]', 'test@example.com');**What auto-waiting checks:**
- Element is attached to DOM
- Element is visible
- Element is stable (not animating)
- Element is enabled
- Element receives events (not obscured)
// Bypass checks (use with caution)
await page.click('button', { force: true });
// Test without acting (trial run)
await page.click('button', { trial: true });Web-First Assertions
Use web-first assertions - they retry until condition is met:
// WRONG - no retry, immediate check
expect(await page.getByText('welcome').isVisible()).toBe(true);
// CORRECT - auto-retries until timeout
await expect(page.getByText('welcome')).toBeVisible();
await expect(page.getByText('Status')).toHaveText('Complete');
await expect(page.getByRole('listitem')).toHaveCount(5);
// Soft assertions - continue test even on failure
await expect.soft(page.getByTestId('status')).toHaveText('Success');
await page.getByRole('link', { name: 'next' }).click();
// Test continues, failures reported at endExplicit Waits for Dynamic Content
// Wait for specific element (modern - use web-first assertions)
await expect(page.locator('.results-loaded')).toBeVisible();
// Wait for network to be idle
await page.waitForLoadState('networkidle');
// Wait for custom condition
await page.waitForFunction(() =>
document.querySelectorAll('.item').length > 10
);Handling Asynchronous Updates
// Known count - assert exact number
await expect(page.locator('.item')).toHaveCount(5);
// Unknown count - wait for container, then extract
await expect(page.locator('.search-results')).toBeVisible();
const items = await page.locator('.item').all();
// Loading spinner - wait for absence then presence
await expect(page.locator('.loading-spinner')).not.toBeVisible();
await expect(page.locator('.results')).toBeVisible();
// Wait for text content to appear
await expect(page.locatRead more
name: playwright-patterns description: Use when writing Playwright automation code, building web scrapers, or creating E2E tests - provides best practices for selector strategies, waiting patterns, and robust automation that minimizes flakiness user-invocable: false
Playwright Automation Patterns
Overview
Reliable browser automation requires strategic selector choice, proper waiting, and defensive coding. This skill provides patterns that minimize test flakiness and maximize maintainability.
When to Use
- Writing new Playwright scripts or tests
- Debugging flaky automation
- Refactoring unreliable selectors
- Building web scrapers that need to handle dynamic content
- Creating E2E tests that must be maintainable
**When NOT to use:**
- Simple one-time browser tasks
- When you need Playwright API documentation (use context7 MCP)
Selector Strategy
Priority Order
Use user-facing locators first (most resilient), then test IDs, then CSS/XPath as last resort:
1. **Role-based locators** (best - user-centric)
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com');2. **Other user-facing locators**
await page.getByLabel('Password').fill('secret');
await page.getByPlaceholder('Search...').fill('query');
await page.getByText('Submit Order').click();3. **Test ID attributes** (explicit contract)
// Default uses data-testid
await page.getByTestId('submit-button').click();
// Can customize in playwright.config.ts:
// use: { testIdAttribute: 'data-pw' }4. **CSS/ID selectors** (fragile, avoid if possible)
await page.locator('#submit-btn').click();
await page.locator('.btn.btn-primary.submit').click();Strictness and Specificity
Locators are strict by default - operations throw if multiple elements match:
// ERROR if 2+ buttons exist
await page.getByRole('button').click();
// Solutions:
// 1. Make locator more specific
await page.getByRole('button', { name: 'Submit' }).click();
// 2. Filter to narrow down
await page.getByRole('button')
.filter({ hasText: 'Submit' })
.click();
// 3. Chain locators to scope
await page.locator('.product-card')
.getByRole('button', { name: 'Add to cart' })
.click();
// Avoid: Using first() makes tests fragile
await page.getByRole('button').first().click(); // Don't do thisLocator Filtering and Chaining
// Filter by text content
await page.getByRole('listitem')
.filter({ hasText: 'Product 2' })
.getByRole('button')
.click();
// Filter by child element
await page.getByRole('listitem')
.filter({ has: page.getByRole('heading', { name: 'Product 2' }) })
.getByRole('button', { name: 'Buy' })
.click();
// Filter by NOT having text
await expect(
page.getByRole('listitem')
.filter({ hasNot: page.getByText('Out of stock') })
).toHaveCount(5);
// Handle "either/or" scenarios
const loginOrWelcome = await page.getByRole('button', { name: 'Login' })
.or(page.getByText('Welcome back'))
.first();
await expect(loginOrWelcome).toBeVisible();Anti-Patterns to Avoid
❌ **Fragile CSS paths**
// BAD: Breaks when HTML structure changes
await page.click('div.container > div:nth-child(2) > button.submit');✅ **Stable semantic selectors**
// GOOD: Survives structural changes
await page.getByRole('button', { name: 'Submit' }).click();❌ **XPath with positions**
// BAD: Brittle
await page.locator('xpath=//div[3]/button[1]').click();✅ **XPath with content**
// BETTER: More stable
await page.locator('xpath=//button[contains(text(), "Submit")]').click();Waiting Patterns
Built-in Auto-Waiting
Playwright auto-waits before most actions. Trust it.
// Auto-waits for element to be visible, enabled, and stable
await page.click('button');
await page.fill('input[name="email"]', 'test@example.com');**What auto-waiting checks:**
- Element is attached to DOM
- Element is visible
- Element is stable (not animating)
- Element is enabled
- Element receives events (not obscured)
// Bypass checks (use with caution)
await page.click('button', { force: true });
// Test without acting (trial run)
await page.click('button', { trial: true });Web-First Assertions
Use web-first assertions - they retry until condition is met:
// WRONG - no retry, immediate check
expect(await page.getByText('welcome').isVisible()).toBe(true);
// CORRECT - auto-retries until timeout
await expect(page.getByText('welcome')).toBeVisible();
await expect(page.getByText('Status')).toHaveText('Complete');
await expect(page.getByRole('listitem')).toHaveCount(5);
// Soft assertions - continue test even on failure
await expect.soft(page.getByTestId('status')).toHaveText('Success');
await page.getByRole('link', { name: 'next' }).click();
// Test continues, failures reported at endExplicit Waits for Dynamic Content
// Wait for specific element (modern - use web-first assertions)
await expect(page.locator('.results-loaded')).toBeVisible();
// Wait for network to be idle
await page.waitForLoadState('networkidle');
// Wait for custom condition
await page.waitForFunction(() =>
document.querySelectorAll('.item').length > 10
);Handling Asynchronous Updates
// Known count - assert exact number
await expect(page.locator('.item')).toHaveCount(5);
// Unknown count - wait for container, then extract
await expect(page.locator('.search-results')).toBeVisible();
const items = await page.locator('.item').all();
// Loading spinner - wait for absence then presence
await expect(page.locator('.loading-spinner')).not.toBeVisible();
await expect(page.locator('.results')).toBeVisible();
// Wait for text content to appear
await expect(page.locatShowing the first part of this file.
This is my collection of plugins that I use on a day-to-day basis for getting stuff done with Claude Code. Most of these are development-oriented in some way or another, but also often end up being useful for other things.
Repo: ed3dai/ed3d-plugins
Other skills on ed3d-plugins.
- /doing-a-simple-two-stage-fanout
Use when analyzing a large corpus of text, code, or data that exceeds a single agent's effective context - orchestrates parallel Worker subagents, Critic review subagents, and a final Summarizer subagent with task tracking and failure recovery
Open skill - /using-generic-agents
Use to decide what kind of generic agent you should use
Open skill - /creating-a-plugin
Use when creating a new Claude Code plugin or setting up plugin structure - provides complete file organization, manifest format, and component definitions for commands, agents, skills, hooks, and MCP servers
Open skill - /creating-an-agent
Use when creating specialized subagents for Claude Code plugins or the Task tool - covers description writing for auto-delegation, tool selection, prompt structure, and testing agents
Open skill - /maintaining-a-marketplace
Use when creating, releasing, or maintaining a Claude Code Plugin Marketplace - covers marketplace.json schema, version management, release checklists, changelog conventions, and validation to prevent sync drift between plugin.json and marketplace.json
Open skill - /maintaining-project-context
Use when completing development phases or branches to identify and update CLAUDE.md or AGENTS.md files that may have become stale - analyzes what changed, determines affected contracts and documentation, and coordinates updates
Open skill

