Playwright — Browser Automation Skill
Intelligent browser automation executor. Analyzes the user's request, selects the optimal pattern from 30+ built-in templates, generates production-grade Playwright code, and executes it with real-time reporting.
Setup
Playwright must be available in the environment. If not installed:
npm init -y
npm install playwright @playwright/test
npx playwright install chromium
For all 3 browsers:
npx playwright install
Check what's installed:
npx playwright install --dry-run
Trigger Decision Tree
When user asks for browser automation, classify the task:
User request
├── "screenshot" / "capture" / "take a picture"
│ → screenshot template
├── "responsive" / "mobile" / "different sizes"
│ → responsive check + per-viewport screenshots
├── "login" / "sign in" / "authenticate"
│ → login flow with error detection
├── "form" / "fill" / "submit" / "input"
│ → form testing with validation check
├── "broken links" / "check links" / "link validation"
│ → broken link scanner
├── "scrape" / "extract" / "get data" / "crawl"
│ → data extraction (single page or crawl)
├── "mock" / "intercept" / "stub" / "fake api"
│ → API mocking with route interception
├── "accessibility" / "a11y" / "axe" / "wcag"
│ → accessibility audit (requires axe-core)
├── "performance" / "lighthouse" / "speed" / "load time"
│ → performance audit with budgets
├── "visual" / "visual regression" / "diff"
│ → visual comparison screenshots
├── "download" / "file download"
│ → file download handler
├── "console" / "errors" / "logs"
│ → console error detector
├── "pdf" / "generate pdf"
│ → page-to-PDF converter
└── else → generic browse + report
Workflow
Step 1: Detect the environment
Before writing any code, determine what's available:
- Dev servers: Check common ports (3000, 3001, 5173, 8080, 8000, 4200, 5000, 9000) for running processes
- Installed browsers: Run
npx playwright install --dry-runto see which browsers are available - Framework indicators: Look for
package.jsondependencies (react, vue, svelte, next, nuxt) to understand the app under test - Available credentials: Check if the user mentioned login credentials or has
.envfiles
Step 2: Select template + generate code
Based on task classification and environment info, select the appropriate template and customize it with:
- Detected URL (dev server or user-supplied)
- Any user-specific parameters (credentials, selectors, viewports)
- Best-practice patterns auto-inserted (waitForSelector instead of fixed waits, graceful error handling, cleanup in finally)
Rules for code generation:
- Use
getByRole/getByText/getByLabelover CSS selectors (stable, accessible) - Parameterize URL in
TARGET_URLconstant at top - Never use
waitForTimeoutfor conditions — usewaitForSelector,waitForURL,waitForResponse - Always wrap in try/catch/finally with
browser.close()in finally - Include console.error logging at each step
- Comment each logical block
- Write generated file to OS temp dir as
playwright-task-{timestamp}.js
Step 3: Execute
Run the generated script directly with Node:
node <path-to-generated-file>
For inline one-off tasks (quick screenshot, check title):
node -e "
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto('$URL');
console.log('Title:', await page.title());
await browser.close();
})();
"
Step 4: Report results
Present results based on task type:
| Task | Output format |
|---|---|
| Screenshot | Display path + preview description |
| Responsive | Table: viewport × status × screenshot path |
| Login | Pass/fail + redirect URL + session file |
| Broken links | Summary: working/broken + detailed table |
| Scrape | Number of records + sample rows |
| A11y | Violation count + top 3 issues |
| Performance | Score table + largest offenders |
| Visual diff | Pass/fail + mismatch pixels |
Smart Defaults
These are applied automatically to every generated script:
| Setting | Default | Why |
|---|---|---|
headless |
false |
User sees what's happening |
slowMo |
100 |
Visible transitions for debugging |
viewport |
1280x720 |
Standard desktop |
| Timeout | 15000 (15s) |
Balance patience vs feedback |
args |
--no-sandbox |
Linux/WSL compatibility |
| Trace | On first failure | Diagnostic data |
| Video | On failure | Visual debug |
| Screenshot | On failure | Visual assert |
| Console listener | Active | Capture browser errors |
| Page error listener | Active | Catch JS exceptions |
Important Rules
- No hardcoded URLs: Always parameterize as
TARGET_URL - No fixed waits: Use
waitForSelector,waitForURL,waitForResponse,waitForLoadState - Auth state reuse: Save to
.auth/directory for multi-step flows - Temp files only: Write scripts to OS temp dir, never to project or skill dir
- Failure screenshots: Always capture on error for debugging
- Close browser: Always
browser.close()infinallyblock
Error Recovery
| Error | Diagnosis | Response |
|---|---|---|
| ECONNREFUSED | Server not running | Suggest dev server or check URL |
| Timeout 30s | Element/page not loading | Retry with waitForSelector, suggest specific selector |
| NoSuchElement | Wrong selector | Auto-capture page HTML snippet, suggest better selector |
| Auth failure | Login credentials wrong | Capture error message, suggest alternative flow |
| 404/500 | Bad URL or server error | Log status + body, offer to check URL |
Common Fast Patterns
Quick screenshot
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.setViewportSize({ width: 1920, height: 1080 });
await page.goto(TARGET_URL, { waitUntil: 'networkidle' });
await page.screenshot({ path: `${require('os').tmpdir()}/screenshot.png`, fullPage: true });
console.log('Screenshot saved to temp/screenshot.png');
await browser.close();
})();
Quick page info
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(TARGET_URL);
console.log('Title:', await page.title());
console.log('URL:', page.url());
const meta = await page.$('meta[name="description"]');
console.log('Meta description:', meta ? await meta.getAttribute('content') : 'N/A');
await browser.close();
})();
Quick form test
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(TARGET_URL);
const inputs = await page.locator('input, textarea, select').count();
const buttons = await page.locator('button, input[type="submit"]').count();
console.log(`Form has ${inputs} inputs and ${buttons} buttons`);
await browser.close();
})();
Advanced Patterns
Login flow with auth state reuse
const { chromium } = require('playwright');
const path = require('path');
const AUTH_FILE = path.join(__dirname, '.auth', 'state.json');
(async () => {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
// Try to reuse saved auth state
try {
const state = require(AUTH_FILE);
await context.addCookies(state.cookies);
} catch {}
const page = await context.newPage();
await page.goto(TARGET_URL);
// Check if logged in
const isLoggedIn = await page.$('[data-logged-in]');
if (!isLoggedIn) {
await page.fill('[name="username"]', USERNAME);
await page.fill('[name="password"]', PASSWORD);
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard');
// Save auth state
const cookies = await context.cookies();
require('fs').writeFileSync(AUTH_FILE, JSON.stringify({ cookies }, null, 2));
}
// Continue with authenticated actions...
await browser.close();
})();
API mocking with route interception
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
// Mock API responses
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Test User' }])
});
});
// Block analytics/tracking requests
await page.route(/google-analytics|facebook|gtag/, route => route.abort());
await page.goto(TARGET_URL);
// Test with mocked data...
await browser.close();
})();
Accessibility audit (requires axe-core)
npm install @axe-core/playwright
const { chromium } = require('playwright');
const { injectAxe, checkA11y } = require('@axe-core/playwright');
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(TARGET_URL);
await injectAxe(page);
const results = await checkA11y(page, null, {
detailedReport: true,
detailedReportOptions: { html: true }
});
console.log(`Violations: ${results.violations.length}`);
results.violations.forEach(v => console.log(`- ${v.help}: ${v.nodes.length} instances`));
await browser.close();
})();
Anti-Patterns
| Anti-Pattern | Why It's Bad | Do This Instead |
|---|---|---|
page.waitForTimeout(5000) |
Flaky, slows tests, false passes | page.waitForSelector('.loaded') |
page.$eval('.css-selector', ...) |
Brittle, breaks on DOM changes | page.getByRole('button', { name: 'Submit' }) |
Hardcoded localhost:3000 |
Fails when port changes | Parameterize as TARGET_URL env var |
No browser.close() |
Hangs, leaks resources | Always in finally block |
page.click() without wait |
Race condition, element not ready | page.click() already waits for actionability |
| Sequential tests in one script | One failure kills everything | Separate scripts or use test runner |
networkidle on SPAs |
Never resolves on polling apps | Use load + explicit element wait |
| Capturing fullPage on infinite scroll | Huge images, timeouts | Clip viewport only |
| Reusing selectors between environments | Dev vs prod DOM differs | Use data-testid or role-based locators |
Error Handling
Playwright-specific errors and remedies
| Error | Likely Cause | Resolution |
|---|---|---|
browserType.launch: Executable doesn't exist |
Chromium not installed | Run npx playwright install chromium |
page.goto: net::ERR_CONNECTION_REFUSED |
Dev server not running | Start dev server or verify URL |
page.click: Target closed |
Navigation happened during click | Use page.waitForNavigation() before click or Promise.all([page.waitForNavigation(), page.click()]) |
locator.click: Timeout 30000ms exceeded |
Element never becomes actionable | Check selector; maybe element is hidden/disabled/covered |
page.fill: Element is not an <input> |
Wrong element type targeted | Use getByRole('textbox') for proper targeting |
route.fulfill: Request already served |
Multiple routes matched same request | Order routes most-specific first; use route.fallback() for pass-through |
browser.newContext: Cookie domain mismatch |
Auth state reuse on wrong domain | Ensure cookies match the target URL's domain |
page.pdf: Protocol error |
Headless mode disabled | PDF generation requires headless: true |
page.waitForSelector: Target closed |
Navigation destroyed the page | Wrap navigation + post-navigation actions in a single await chain |
require('playwright'): Cannot find module |
Install incomplete | npm install playwright then verify node_modules/playwright exists |
Checklist
- Locator uses
getByRole,getByText, orgetByTestId— never XPath or CSS selectors with fragile class names - Auto-waiting relied on (no manual
waitForTimeoutorsleep) - Test isolates state (clean DB or API mock per test)
- Cross-browser tested (Chromium + Firefox + WebKit) for critical flows
- Trace viewer enabled on failure for debugging CI flakes
Sources
- Playwright official documentation (playwright.dev) — locator strategies, auto-waiting, fixtures
- Testing Trophy methodology — 80% integration / 20% unit testing ratio
- @axe-core/playwright for accessibility audits
- Lighthouse CI for performance budgets
- Production E2E patterns from 30+ project codebases