| name | browser |
| description | Browser automation with Playwright: web scraping, UI testing, form filling, screenshot capture, PDF generation, session recording |
Browser Skill
When to activate
- Automating any workflow that requires a real browser (login flows, SPAs, JS-rendered content)
- Writing end-to-end tests for web UIs
- Scraping websites that block headless requests or require JavaScript
- Generating PDFs or screenshots of web pages programmatically
- Filling forms, clicking through multi-step flows, simulating user behaviour
- Visual regression testing
When NOT to use
- APIs that return JSON — use
fetch/requests directly, no browser needed
- Simple static HTML scraping —
cheerio or BeautifulSoup is faster
- Performance-critical scrapers at scale — Playwright is slow for thousands of pages
- When a site's API is available and documented — always prefer the API
Instructions
Setup
npm install playwright
npx playwright install chromium
pip install playwright
playwright install chromium
Basic page interaction (TypeScript)
import { chromium, type Page } from 'playwright'
const browser = await chromium.launch({ headless: true })
const context = await browser.newContext()
const page = await context.newPage()
await page.goto('https://example.com')
await page.waitForSelector('button[data-testid="login"]')
await page.click('button[data-testid="login"]')
await page.fill('input[name="email"]', 'user@example.com')
await page.fill('input[name="password"]', process.env.PASSWORD!)
await page.press('input[name="password"]', 'Enter')
await page.waitForURL('**/dashboard')
const text = await page.textContent('h1')
const items = await page.$$eval('ul.results li', els => els.map(el => el.textContent))
await browser.close()
Authentication — persist sessions
const context = await browser.newContext()
const page = await context.newPage()
await page.goto('/login')
await page.fill('#email', 'user@example.com')
await page.fill('#password', process.env.PASSWORD!)
await page.click('button[type=submit]')
await page.waitForURL('**/dashboard')
await context.storageState({ path: 'session.json' })
await browser.close()
const context2 = await browser.newContext({ storageState: 'session.json' })
Web scraping (JS-rendered content)
await page.goto('https://spa-app.com/products')
await page.waitForSelector('.product-card', { timeout: 10000 })
const products = await page.$$eval('.product-card', cards =>
cards.map(card => ({
name: card.querySelector('.name')?.textContent?.trim(),
price: card.querySelector('.price')?.textContent?.trim(),
url: card.querySelector('a')?.href,
}))
)
while (true) {
const nextBtn = await page.$('button.next-page:not([disabled])')
if (!nextBtn) break
await nextBtn.click()
await page.waitForLoadState('networkidle')
}
Screenshot and PDF
await page.screenshot({ path: 'page.png', fullPage: true })
const element = await page.$('.report-card')
await element?.screenshot({ path: 'card.png' })
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
})
End-to-end tests (with Playwright Test)
import { test, expect } from '@playwright/test'
test('complete checkout flow', async ({ page }) => {
await page.goto('/shop')
await page.click('[data-testid="product-1"] button.add-to-cart')
await page.click('[data-testid="cart-icon"]')
await expect(page.locator('.cart-count')).toHaveText('1')
await page.click('button.checkout')
await page.fill('#card-number', '4242 4242 4242 4242')
await page.fill('#expiry', '12/28')
await page.fill('#cvc', '123')
await page.click('button.pay')
await expect(page).toHaveURL('/order-confirmation')
await expect(page.locator('h1')).toContainText('Order confirmed')
})
npx playwright test
npx playwright test --headed
npx playwright test --debug
npx playwright show-report
Handling anti-bot measures
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)...',
viewport: { width: 1280, height: 800 },
locale: 'en-US',
timezoneId: 'America/New_York',
})
await page.waitForTimeout(500 + Math.random() * 1000)
await page.route('**/*.{png,jpg,jpeg,webp}', route => route.abort())
await page.route('**/api/data', route => {
const body = route.request().postData()
route.continue({ postData: body?.replace('original', 'modified') })
})
Python equivalent
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://example.com")
page.wait_for_selector("h1")
title = page.text_content("h1")
page.screenshot(path="page.png", full_page=True)
browser.close()
Example
User: Scrape product listings from an e-commerce site that requires login and uses JavaScript to render products.
Expected output:
scripts/scrape-products.ts
storageState: 'session.json' for auth persistence
waitForSelector('.product-grid') before extraction
- Pagination loop with
waitForLoadState('networkidle')
- Results written to
products.json
- Rate limiting:
waitForTimeout(1000) between pages