Browser automation and E2E testing with Playwright the way an agent should run it: resilient locators that survive redesigns, session reuse via storageState, web-first assertions, failure recovery, and flake triage. Triggers when writing Playwright tests, debugging flaky E2E suites, automating browser tasks for an agent, or auditing a Playwright codebase.
./install.sh playwright-agent-patternsname: "playwright_agent_patterns" description: "Browser automation and E2E testing with Playwright the way an agent should run it: resilient locators that survive redesigns, session reuse via storageState, web-first assertions, failure recovery, and flake triage. Triggers when writing Playwright tests, debugging flaky E2E suites, automating browser tasks for an agent, or auditing a Playwright codebase."
Playwright is the standard tool for browser automation and end-to-end testing, and it is also one of the best tools an agent has for driving real websites. Most failures in Playwright work come from four places: brittle selectors, hand-rolled waits, re-logging-in per test, and retries that hide root causes. This skill gives you the patterns that eliminate all four. Detailed locator catalogs, recovery recipes, and session recipes live in references/; load only what the task needs.
getByRole with a name, then getByLabel, getByPlaceholder, getByText, getByAltText, getByTitle, and getByTestId last. Chain and filter to narrow scope: page.getByRole('listitem').filter({ hasText: 'Product 2' }).getByRole('button', { name: 'Add to cart' }). Never reach for CSS class chains, XPath traversals, or nth-child indexing unless the element has no accessible identity at all, and document why in a comment when you do. Locator catalog and strict-mode rules are in references/locator-patterns.md.toBeVisible, toHaveText, toHaveURL) retries for up to 5 seconds. Write the assertion, not the wait. Never page.waitForTimeout. Never waitForLoadState('networkidle'); it is notoriously flaky. If you need a value, confirm the state with a retrying assertion first, then extract it. Assertion patterns are in references/assertions-and-waiting.md.await page.context().storageState({ path: '.auth/session.json' }), then use: { storageState: '.auth/session.json' } in config. Where the login API exists, authenticate through context.request.post and skip the UI entirely. Session recipes and auth patterns are in references/session-management.md.retries: 2 on CI, 0 locally) and keep first-failure evidence: trace: 'on-first-retry', screenshot: 'only-on-failure', video: 'retain-on-failure'. A test that fails then passes is labeled flaky, and flaky is a maintenance signal, not a green build. Classify every failure into the taxonomy (Timing/Async, Isolation, Environment, Infrastructure) and fix the root cause; do not raise the retry count to make it quiet. Recovery patterns and the triage command cheat sheet are in references/recovery-and-debugging.md.bin/pw_audit.py against the spec files. Fix every finding it flags. Then burn-in the changed specs with --repeat-each=10 to surface flakes before they reach CI.When this skill is done, you deliver:
networkidle, and no positional .first()/.nth() used to silence strict-mode violations.forbidOnly gaps.pw_audit.py run and a passing burn-in (--repeat-each=10) on the specs you touched.textContent, getAttribute, isVisible) happen only after a retrying assertion has confirmed the state.ts
import { test, expect } from '@playwright/test';
test('adds product to cart', async ({ page }) => {
await page.goto('/products');
await page
.getByRole('listitem')
.filter({ hasText: 'Acme Widget' })
.getByRole('button', { name: 'Add to cart' })
.click();
// Web-first: retries until the badge updates or 5s passes.
await expect(page.getByRole('link', { name: /cart/i })).toContainText('1');
// Only extract after the retrying assertion confirmed the state.
const badge = await page.getByRole('link', { name: /cart/i }).textContent();
expect(badge).toMatch(/1/);
});
ts
// scripts/save-session.ts — run once (or in global setup)
import { chromium } from '@playwright/test';
const browser = await chromium.launch();
const page = await (await browser.newContext()).newPage();
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.E2E_EMAIL!);
await page.getByLabel('Password').fill(process.env.E2E_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await page.waitForURL('/dashboard');
await page.context().storageState({ path: '.auth/session.json' });
await browser.close();
ts
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: [['html', { open: 'never' }], ['list']],
use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:3000',
storageState: '.auth/session.json',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
});
bin/pw_audit.py scans spec files for the patterns this skill forbids: hard waitForTimeout sleeps, networkidle waits, CSS class-chain locators, XPath traversals, positional .first()/.nth(), and non-retrying reads used as assertions. Run it from the repo root: python3 <skill-dir>/bin/pw_audit.py tests/. Exit code is non-zero when findings exist, so it fits in CI.
microsoft/playwright-cli project is a CLI built for coding agents and is more token-efficient than loading the full MCP tool schemas.publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e
No ratings yet — be the first to rate it.