Playwright Fundamentals for Reliable Automation
Use Playwright to drive browsers reliably across Chrome, Firefox, and
Playwright is the modern browser automation framework: cross-browser, with first-class support for waiting on dynamic content and intercepting network requests. Replaces Selenium for most use cases. Used in test suites, scrapers, and agent-driven workflows.
## Key Points
- `getByRole` — accessibility-tree-based. Most resilient to markup changes. Preferred.
- `getByLabel` — for form fields. Resilient to wrapping changes.
- `getByText` — for visible text. Use exact matching to avoid partial matches.
- `getByTestId` — for test-only attributes. Stable across UI changes if the team agrees on test IDs.
- `getByPlaceholder`, `getByTitle`, `getByAltText` — for specific element attributes.
- `locator(css)` — fall-back. Use only when accessibility-based locators don't work.
- Attached to the DOM
- Stable (not animating)
- Receiving events (not covered)
1. The locator is matching the wrong element (a hidden one).
2. The page is in an unexpected state.
3. There's a real race condition the test should validate.
## Quick Example
```ts
await expect(page.getByText('Welcome, Alice')).toBeVisible();
```
```ts
// don't do this
await page.waitFor(2000);
const text = await page.textContent('h1');
expect(text).toContain('Welcome, Alice');
```skilldb get browser-automation-skills/Playwright Fundamentals for Reliable AutomationFull skill: 212 linesPlaywright is the modern browser automation framework: cross-browser, with first-class support for waiting on dynamic content and intercepting network requests. Replaces Selenium for most use cases. Used in test suites, scrapers, and agent-driven workflows.
This skill covers the patterns that produce reliable Playwright code. The framework is forgiving; the writing of reliable code is not. The same Playwright primitive can produce a flake-free suite or a constantly-broken one depending on how you use it.
Locators, Not Selectors
Playwright's modern API uses locators, not raw selectors. A locator is a representation of how to find an element; it is re-evaluated on every action.
// modern, reliable
const submit = page.getByRole('button', { name: 'Submit' });
await submit.click();
// older, fragile
await page.click('button.submit-btn');
The locator API:
getByRole— accessibility-tree-based. Most resilient to markup changes. Preferred.getByLabel— for form fields. Resilient to wrapping changes.getByText— for visible text. Use exact matching to avoid partial matches.getByTestId— for test-only attributes. Stable across UI changes if the team agrees on test IDs.getByPlaceholder,getByTitle,getByAltText— for specific element attributes.locator(css)— fall-back. Use only when accessibility-based locators don't work.
Use the resilient locators by default. CSS selectors break the moment a designer renames a class. Role-based locators survive most refactors.
Auto-Waiting
Playwright auto-waits before every action. click() waits for the element to be:
- Attached to the DOM
- Visible
- Stable (not animating)
- Receiving events (not covered)
- Enabled
This eliminates 95% of flakes that plagued Selenium suites. You don't need await page.waitFor(500) before a click; Playwright is already waiting.
Don't fight it. If you find yourself adding manual waits, the test is doing something wrong. Either:
- The locator is matching the wrong element (a hidden one).
- The page is in an unexpected state.
- There's a real race condition the test should validate.
await page.waitForTimeout(N) is almost always wrong. Replace with:
await page.waitForLoadState('networkidle')— for "page is fully loaded."await expect(locator).toBeVisible()— for "this element should exist."await page.waitForResponse(predicate)— for "this network call should complete."
Web-First Assertions
Use Playwright's expect (different from Jest/Vitest's). It auto-waits.
await expect(page.getByText('Welcome, Alice')).toBeVisible();
This polls until the text appears or the timeout expires. Replaces:
// don't do this
await page.waitFor(2000);
const text = await page.textContent('h1');
expect(text).toContain('Welcome, Alice');
The web-first assertion is shorter, more readable, and not flaky.
Network Interception
Playwright can intercept and modify network requests. Useful for:
- Stubbing API responses in tests (no backend dependency).
- Routing third-party requests through fakes.
- Modifying responses to test edge cases (errors, slow connections).
- Logging traffic for debugging.
await page.route('**/api/users/me', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: 1, name: 'Test User' }),
});
});
For agent-driven scraping, route interception lets you abort unnecessary requests (analytics, ads, fonts) for faster page loads:
await page.route('**/*.{png,jpg,jpeg}', (route) => route.abort());
Parallelism
Playwright runs tests in parallel by default. Each test gets its own browser context (a fresh, isolated environment with its own cookies, storage, and cache).
For testing:
- Tests run in parallel within a file (configurable:
test.describe.parallel/test.describe.serial). - Files run in parallel by default.
- Set
workersin config to control concurrency.
For scraping:
- Each scraping task should be independent and isolated.
- Share a browser instance across tasks; create new contexts per task.
- Don't share contexts across tasks; cookies and state leak.
Browser contexts are cheap; create them liberally. Browsers themselves are expensive to launch; reuse them.
Stealth and Anti-Detection
For agent or scraping use cases where the target site detects automation:
- Use
playwright-extrawith thepuppeteer-extra-plugin-stealth(works with Playwright too). - Use a real residential or rotating proxy if the site IP-blocks.
- Slow down — add jitter between actions.
- Use a real user agent and viewport size.
- Don't run headless when the site detects headless; use headful with
--headless=new.
Stealth is an arms race. Sites get better at detecting; libraries get better at hiding. Stay current with the libraries; expect breakage.
Authentication and Session Reuse
Re-authenticating on every test or task is slow. Playwright supports session reuse:
// auth.setup.ts (run once)
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com/login');
await page.fill('[name=email]', 'test@example.com');
await page.fill('[name=password]', 'password');
await page.click('button[type=submit]');
await page.waitForURL('**/dashboard');
await context.storageState({ path: 'auth.json' });
// in test
test.use({ storageState: 'auth.json' });
The storage state captures cookies and localStorage; subsequent tests load it without re-auth. Refresh it daily or on session expiration.
For scraping, the same pattern: log in once, save state, reuse across many tasks.
Headless and Headful
Playwright runs headless by default (no UI). Faster, lower resource use, suitable for CI and servers.
Run headful when:
- Debugging a flaky test ("why is this clicking the wrong element?").
- A site detects headless and serves different content.
- You need to interact with the browser manually during a paused test.
PWDEBUG=1 npx playwright test runs headful with the inspector open — single-step through the test. The most powerful debugging tool Playwright provides.
Tracing
Playwright's tracing captures the full execution: every action, network request, console message, screenshot, DOM snapshot at each step.
// in playwright.config.ts
use: {
trace: 'on-first-retry',
}
When a test fails, view the trace with npx playwright show-trace trace.zip. The trace lets you scrub through the test like a video; you see what was happening at the moment of failure.
Tracing has overhead; enable it for retries or failures, not every test.
Error Recovery in Long-Running Tasks
For agent or scraping tasks that run for hours:
- Wrap each operation in try/catch; log failures.
- On error, screenshot the page and save the DOM for diagnosis.
- Reset the browser context every N tasks; long-lived contexts accumulate state and degrade.
- Implement exponential backoff for retries.
- Have a circuit breaker: if 10 consecutive tasks fail, stop and alert.
Long-running automation accumulates errors. Plan for it.
Anti-Patterns
Hardcoded waits. await page.waitForTimeout(2000). Brittle and slow. Use auto-waits and web-first assertions.
CSS selectors as primary locators. Breaks on every UI refactor. Use getByRole and friends.
Sharing browser contexts across tasks. State leaks; tests interfere with each other. Create a context per test or per task.
No tracing on failure. Failure investigation requires reading code, running locally, and guessing. Enable tracing.
Not reusing auth. Each test re-logs in. The suite takes 10× longer. Use storage state.
Headless-only when sites detect headless. Tests pass locally (where you ran headful), fail in CI. Run headless in CI; verify locally with --headed.
Install this skill directly: skilldb add browser-automation-skills
Related Skills
Agent-Driven Browser Tasks
Connect an LLM agent to a browser to perform tasks: navigation, form
Debugging Flaky Browser Tests
Diagnose and fix flaky end-to-end tests. Covers the categories of
Web Scraping at Scale
Build scrapers that run reliably across thousands of pages, handle
Adversarial Code Review
Adversarial implementation review methodology that validates code completeness against requirements with fresh objectivity. Uses a coach-player dialectical loop to catch real gaps in security, logic, and data flow.
API Design Testing
Design, document, and test APIs following RESTful principles, consistent
Architecture
Design software systems with sound architecture — choosing patterns, defining boundaries,