doing-a-simple-two-sta…
Use when analyzing a large corpus of text, code, or data that exceeds a single agent's effective context - orchestrates parallel…
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
Claude Code
$ npx -y skills add ed3dai/ed3d-plugins --skill playwright-patterns --agent claude-code
Codex
$ npx skills add ed3dai/ed3d-plugins --skill playwright-patterns -a codex -y
Cursor
$ npx skills add ed3dai/ed3d-plugins --skill playwright-patterns -a cursor -y
Gemini CLI
$ npx skills add ed3dai/ed3d-plugins --skill playwright-patterns -a gemini-cli -y
GitHub Copilot
$ npx skills add ed3dai/ed3d-plugins --skill playwright-patterns -a github-copilot -y
OpenCode
$ npx skills add ed3dai/ed3d-plugins --skill playwright-patterns -a opencode -y
Windsurf
$ npx skills add ed3dai/ed3d-plugins --skill playwright-patterns -a windsurf -y
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
Reliable browser automation requires strategic selector choice, proper waiting, and defensive coding. This skill provides patterns that minimize test flakiness and maximize maintainability.
**When NOT to use:**
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();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 this// 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();❌ **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();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:**
// Bypass checks (use with caution)
await page.click('button', { force: true });
// Test without acting (trial run)
await page.click('button', { trial: true });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 end// 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
);// 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.locatEd's repo of Claude Code plugins, centered around a research-plan-implement workflow. Only a tiny bit cursed. If you're lucky.
Use when analyzing a large corpus of text, code, or data that exceeds a single agent's effective context - orchestrates parallel…
Use when creating a new Claude Code plugin or setting up plugin structure - provides complete file organization, manifest format,…
Use when creating specialized subagents for Claude Code plugins or the Task tool - covers description writing for…
Use when creating, releasing, or maintaining a Claude Code Plugin Marketplace - covers marketplace.json schema, version…
Use when completing development phases or branches to identify and update CLAUDE.md or AGENTS.md files that may have become stale…
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
Ask which skill or flow fits your situation. A router over the skills in this repo.