
playwright-local
Build browser automation and web scraping with Playwright on your local machine. Prevents 10 documented errors including CI timeout hangs, extension testing failures, and Ubuntu compatibility issues. Includes stealth mode for anti-bot bypass, authenticated sessions, infinite scroll handling, screenshot/PDF generation, and v1.57 Speedboard performance analysis. Use when: automating browsers, scraping protected sites, testing with real IPs, bypassing bot detection, generating screenshots/PDFs, or troubleshooting "target closed", "page.pause() hangs CI", "permission prompts block tests", or "Ubuntu 25.10 installation" errors.
- Installs
- 505
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/jezweb/claude-skills --skill playwright-local- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads playwright-local automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
Playwright Local Browser Automation
Status: Production Ready ✅ Last Updated: 2026-01-21 Dependencies: Node.js 20+ (Node.js 18 deprecated) or Python 3.9+ Latest Versions: playwright@1.57.0, playwright-stealth@0.0.1, puppeteer-extra-plugin-stealth@2.11.2 Browser Versions: Chromium 143.0.7499.4 | Firefox 144.0.2 | WebKit 26.0
⚠️ v1.57 Breaking Change: Playwright now uses Chrome for Testing builds instead of Chromium. This provides more authentic browser behavior but changes the browser icon and title bar.
Quick Start (5 Minutes)
1. Install Playwright
Node.js:
npm install -D playwright
npx playwright install chromium
Python:
pip install playwright
playwright install chromium
Why this matters:
playwright installdownloads browser binaries (~400MB for Chromium)- Install only needed browsers:
chromium,firefox, orwebkit - Binaries stored in
~/.cache/ms-playwright/
2. Basic Page Scrape
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
const title = await page.title();
const content = await page.textContent('body');
await browser.close();
console.log({ title, content });
CRITICAL:
- Always close browser with
await browser.close()to avoid zombie processes - Use
waitUntil: 'networkidle'for dynamic content (SPAs) - Default timeout is 30 seconds - adjust with
timeout: 60000if needed
3. Test Locally
# Node.js
npx tsx scrape.ts
# Python
python scrape.py
Why Playwright Local vs Cloudflare Browser Rendering
| Feature | Playwright Local | Cloudflare Browser Rendering |
|---|---|---|
| IP Address | Residential (your ISP) | Datacenter (easily detected) |
| Stealth Plugins | Full support | Not available |
| Rate Limits | None | 2,000 requests/day free tier |
| Cost | Free (your CPU) | $5/10k requests after free tier |
| Browser Control | All Playwright features | Limited API |
| Concurrency | Your hardware limit | Account-based limits |
| Session Persistence | Full cookie/storage control | Limited session management |
| Use Case | Bot-protected sites, auth flows | Simple scraping, serverless |
When to use Cloudflare: Serverless environments, simple scraping, cost-efficient at scale When to use Local: Anti-bot bypass needed, residential IP required, complex automation
The 7-Step Stealth Setup Process
⚠️ 2025 Reality Check: Stealth plugins work well against basic anti-bot measures, but advanced detection systems (Cloudflare Bot Management, PerimeterX, DataDome) have evolved significantly. The detection landscape now includes:
- Behavioral analysis (mouse patterns, scroll timing, keystroke dynamics)
- TLS fingerprinting (JA3/JA4 signatures)
- Canvas and WebGL fingerprinting
- HTTP/2 fingerprinting
Recommendations:
- Stealth plugins are a good starting point, not a complete solution
- Combine with realistic user behavior simulation (use
stepsoption)- Consider residential proxies for heavily protected sites
- "What works today may not work tomorrow" - test regularly
- For advanced scenarios, research alternatives like
nodriverorundetected-chromedriver
Step 1: Install Stealth Plugin (Node.js)
npm install playwright-extra playwright-stealth
For puppeteer-extra compatibility:
npm install puppeteer-extra puppeteer-extra-plugin-stealth
Step 2: Configure Stealth Mode
playwright-extra:
import { chromium } from 'playwright-extra';
import stealth from 'puppeteer-extra-plugin-stealth';
chromium.use(stealth());
const browser = await chromium.launch({
headless: true,
args: [
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
'--disable-setuid-sandbox',
],
});
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
viewport: { width: 1920, height: 1080 },
locale: 'en-US',
timezoneId: 'America/New_York',
});
Key Points:
--disable-blink-features=AutomationControlledremovesnavigator.webdriverflag- Randomize viewport sizes to avoid fingerprinting
- Match user agent to browser version (Chrome 120 example above)
Step 3: Mask WebDriver Detection
await page.addInitScript(() => {
// Remove webdriver property
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
});
// Mock plugins
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5],
});
// Mock languages
Object.defineProperty(navigator, 'languages', {
get: () => ['en-US', 'en'],
});
// Consistent permissions
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters) => (
parameters.name === 'notifications' ?
Promise.resolve({ state: Notification.permission }) :
originalQuery(parameters)
);
});
Step 4: Human-Like Mouse Movement
// Simulate human cursor movement
async function humanClick(page, selector) {
const element = await page.locator(selector);
const box = await element.boundingBox();
if (box) {
// Move to random point within element
const x = box.x + box.width * Math.random();
const y = box.y + box.height * Math.random();
await page.mouse.move(x, y, { steps: 10 });
await page.mouse.click(x, y, { delay: 100 });
}
}
Step 5: Rotate User Agents
const userAgents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
];
const randomUA = userAgents[Math.floor(Math.random() * userAgents.length)];
const context = await browser.newContext({
userAgent: randomUA,
});
Step 6: Cookie and Session Persistence
import { chromium } from 'playwright';
import fs from 'fs/promises';
// Save session
const context = await browser.newContext();
const page = await context.newPage();
// ... perform login ...
const cookies = await context.cookies();
await fs.writeFile('session.json', JSON.stringify(cookies, null, 2));
await context.close();
// Restore session
const savedCookies = JSON.parse(await fs.readFile('session.json', 'utf-8'));
const newContext = await browser.newContext();
await newContext.addCookies(savedCookies);
Step 7: Verify Stealth
Test your setup at: https://bot.sannysoft.com/
const page = await context.newPage();
await page.goto('https://bot.sannysoft.com/', { waitUntil: 'networkidle' });
await page.screenshot({ path: 'stealth-test.png', fullPage: true });
What to check:
navigator.webdrivershould beundefined(notfalse)- Chrome should be detected
- Plugins should be populated
- No red flags on the page
Critical Rules
Always Do
✅ Use waitUntil: 'networkidle' for SPAs (React, Vue, Angular)
✅ Close browsers with await browser.close() to prevent memory leaks
✅ Wrap automation in try/catch/finally blocks
✅ Set explicit timeouts for unreliable sites
✅ Save screenshots on errors for debugging
✅ Use page.waitForSelector() before interacting with elements
✅ Rotate user agents for high-volume scraping
✅ Test with headless: false first, then switch to headless: true
Never Do
❌ Use page.click() without waiting for element (waitForSelector first)
❌ Rely on fixed setTimeout() for waits (use waitForSelector, waitForLoadState)
❌ Scrape without rate limiting (add delays between requests)
❌ Use same user agent for all requests (rotate agents)
❌ Ignore navigation errors (catch and retry with backoff)
❌ Run headless without testing headed mode first (visual debugging catches issues)
❌ Store credentials in code (use environment variables)
Debug Methods (v1.56+)
Playwright v1.56 introduced new methods for capturing debug information without setting up event listeners:
Console Messages
import { test, expect } from '@playwright/test';
test('capture console output', async ({ page }) => {
await page.goto('https://example.com');
// Get all recent console messages
const messages = page.consoleMessages();
// Filter by type
const errors = messages.filter(m => m.type() === 'error');
const logs = messages.filter(m => m.type() === 'log');
console.log('Console errors:', errors.map(m => m.text()));
});
Page Errors
test('check for JavaScript errors', async ({ page }) => {
await page.goto('https://example.com');
// Get all page errors (uncaught exceptions)
const errors = page.pageErrors();
// Fail test if any errors occurred
expect(errors).toHaveLength(0);
});
Network Requests
test('inspect API calls', async ({ page }) => {
await page.goto('https://example.com');
// Get all recent network requests
const requests = page.requests();
// Filter for API calls
const apiCalls = requests.filter(r => r.url().includes('/api/'));
console.log('API calls made:', apiCalls.length);
// Check for failed requests
const failed = requests.filter(r => r.failure());
expect(failed).toHaveLength(0);
});
When to use: Debugging test failures, verifying no console errors, auditing network activity.
Advanced Mouse Control (v1.57+)
The steps option provides fine-grained control over mouse movement, useful for:
- Appearing more human-like to anti-bot detection
- Testing drag-and-drop with smooth animations
- Debugging visual interactions
Click with Steps
// Move to element in 10 intermediate steps (smoother, more human-like)
await page.locator('button.submit').click({ steps: 10 });
// Fast click (fewer steps)
await page.locator('button.cancel').click({ steps: 2 });
Drag with Steps
const source = page.locator('#draggable');
const target = page.locator('#dropzone');
// Smooth drag animation (20 steps)
await source.dragTo(target, { steps: 20 });
// Quick drag (5 steps)
await source.dragTo(target, { steps: 5 });
Anti-detection benefit: Many bot detection systems look for instantaneous mouse movements. Using steps: 10 or higher simulates realistic human mouse behavior.
Known Issues Prevention
This skill prevents 10 documented issues:
Issue #1: Target Closed Error
Error: Protocol error (Target.sendMessageToTarget): Target closed.
Source: https://github.com/microsoft/playwright/issues/2938
Why It Happens: Page was closed before action completed, or browser crashed
Prevention:
try {
await page.goto(url, { timeout: 30000 });
} catch (error) {
if (error.message.includes('Target closed')) {
console.log('Browser crashed, restarting...');
await browser.close();
browser = await chromium.launch();
}
}
Issue #2: Element Not Found
Error: TimeoutError: waiting for selector "button" failed: timeout 30000ms exceeded
Source: https://playwright.dev/docs/actionability
Why It Happens: Element doesn't exist, selector is wrong, or page hasn't loaded
Prevention:
// Use waitForSelector with explicit timeout
const button = await page.waitForSelector('button.submit', {
state: 'visible',
timeout: 10000,
});
await button.click();
// Or use locator with auto-wait
await page.locator('button.submit').click();
Issue #3: Navigation Timeout
Error: TimeoutError: page.goto: Timeout 30000ms exceeded.
Source: https://playwright.dev/docs/navigations
Why It Happens: Slow page load, infinite loading spinner, blocked by firewall
Prevention:
try {
await page.goto(url, {
waitUntil: 'domcontentloaded', // Less strict than networkidle
timeout: 60000, // Increase for slow sites
});
} catch (error) {
if (error.name === 'TimeoutError') {
console.log('Navigation timeout, checking if page loaded...');
const title = await page.title();
if (title) {
console.log('Page loaded despite timeout');
}
}
}
Issue #4: Detached Frame Error
Error: Error: Execution context was destroyed, most likely because of a navigation.
Source: https://github.com/microsoft/playwright/issues/3934
Why It Happens: SPA navigation re-rendered the element
Prevention:
// Re-query element after navigation
async function safeClick(page, selector) {
await page.waitForSelector(selector);
await page.click(selector);
await page.waitForLoadState('networkidle');
}
Issue #5: Bot Detection (403/Captcha)
Error: Page returns 403 or shows captcha
Source: https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth
Why It Happens: Site detects navigator.webdriver, datacenter IP, or fingerprint mismatch
Prevention: Use stealth mode (Step 2-7 above) + residential IP
Issue #6: File Download Not Completing
Error: Download starts but never finishes Source: https://playwright.dev/docs/downloads Why It Happens: Download event not awaited, file stream not closed Prevention:
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('a.download-link'),
]);
const path = await download.path();
await download.saveAs('./downloads/' + download.suggestedFilename());
Issue #7: Infinite Scroll Not Loading More
Error: Scroll reaches bottom but no new content loads Source: https://playwright.dev/docs/input#scrolling Why It Happens: Scroll event not triggered correctly, or scroll too fast Prevention:
let previousHeight = 0;
while (true) {
const currentHeight = await page.evaluate(() => document.body.scrollHeight);
if (currentHeight === previousHeight) {
break; // No more content
}
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(2000); // Wait for new content to load
previousHeight = currentHeight;
}
Issue #8: WebSocket Connection Failed
Error: WebSocket connection to 'ws://...' failed
Source: https://playwright.dev/docs/api/class-browser
Why It Happens: Browser launched without --no-sandbox in restrictive environments
Prevention:
const browser = await chromium.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
});
Issue #9: page.pause() Disables Timeout in Headless Mode
Error: Tests hang indefinitely in CI when page.pause() is present
Source: GitHub Issue #38754
Why It Happens: page.pause() is ignored in headless mode but disables test timeout, causing subsequent failing assertions to hang forever
Prevention:
// Conditional debugging - only pause in local development
if (!process.env.CI && !process.env.HEADLESS) {
await page.pause();
}
// Or use environment variable
const shouldPause = process.env.DEBUG_MODE === 'true';
if (shouldPause) {
await page.pause();
}
Impact: HIGH - Can cause CI pipelines to hang indefinitely on failing assertions
Issue #10: Permission Prompts Block Extension Testing in CI
Error: Tests hang on permission prompts when testing browser extensions
Source: GitHub Issue #38670
Why It Happens: launchPersistentContext with extensions shows non-dismissible permission prompts (clipboard-read/write, local-network-access) that cannot be auto-granted
Prevention:
// Don't use persistent context for extensions in CI
// Use regular context instead
const context = await browser.newContext({
permissions: ['clipboard-read', 'clipboard-write']
});
// For extensions, pre-grant permissions where possible
const context = await browser.newContext({
permissions: ['notifications', 'geolocation']
});
Impact: HIGH - Blocks automated extension testing in CI/CD environments
Configuration Files Reference
playwright.config.ts (Full Example)
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30000,
expect: {
timeout: 5000,
},
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
// Anti-detection settings
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
viewport: { width: 1920, height: 1080 },
locale: 'en-US',
timezoneId: 'America/New_York',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'stealth',
use: {
...devices['Desktop Chrome'],
launchOptions: {
args: [
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
],
},
},
},
],
});
Why these settings:
trace: 'on-first-retry'- Captures full trace for debugging failed testsscreenshot: 'only-on-failure'- Saves disk spaceviewport: { width: 1920, height: 1080 }- Common desktop resolution--disable-blink-features=AutomationControlled- Removes webdriver flag
Dynamic Web Server Configuration (v1.57+)
Wait for web server output before starting tests using regular expressions:
import { defineConfig } from '@playwright/test';
export default defineConfig({
webServer: {
command: 'npm run dev',
// Wait for server to print port
wait: {
stdout: '/Server running on port (?<SERVER_PORT>\\d+)/'
},
},
use: {
// Use captured port in tests
baseURL: `http://localhost:${process.env.SERVER_PORT ?? 3000}`
}
});
Benefits:
- Handles dynamic ports from dev servers (Vite, Next.js dev mode)
- No need for HTTP readiness checks
- Named capture groups become environment variables
- Works with services that only log readiness messages
When to Use:
- Dev servers with random ports
- Services without HTTP endpoints
- Containerized environments with port mapping
Common Patterns
Pattern 1: Authenticated Session Scraping
import { chromium } from 'playwright';
import fs from 'fs/promises';
async function scrapeWithAuth() {
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext();
const page = await context.newPage();
// Login
await page.goto('https://example.com/login');
await page.fill('input[name="email"]', process.env.EMAIL);
await page.fill('input[name="password"]', process.env.PASSWORD);
await page.click('button[type="submit"]');
await page.waitForURL('**/dashboard', { timeout: 10000 });
// Save session
const cookies = await context.cookies();
await fs.writeFile('session.json', JSON.stringify(cookies));
// Navigate to protected page
await page.goto('https://example.com/protected-data');
const data = await page.locator('.data-table').textContent();
await browser.close();
return data;
}
When to use: Sites requiring login, scraping user-specific content
Pattern 2: Infinite Scroll with Deduplication
async function scrapeInfiniteScroll(page, selector) {
const items = new Set();
let previousCount = 0;
let noChangeCount = 0;
while (noChangeCount < 3) {
const elements = await page.locator(selector).all();
for (const el of elements) {
const text = await el.textContent();
items.add(text);
}
if (items.size === previousCount) {
noChangeCount++;
} else {
noChangeCount = 0;
}
previousCount = items.size;
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(1500);
}
return Array.from(items);
}
When to use: Twitter feeds, product listings, news sites with infinite scroll
Pattern 3: Multi-Tab Orchestration
async function scrapeMultipleTabs(urls) {
const browser = await chromium.launch();
const context = await browser.newContext();
const results = await Promise.all(
urls.map(async (url) => {
const page = await context.newPage();
await page.goto(url);
const title = await page.title();
await page.close();
return { url, title };
})
);
await browser.close();
return results;
}
When to use: Scraping multiple pages concurrently, comparison shopping
Pattern 4: Screenshot Full Page
async function captureFullPage(url, outputPath) {
const browser = await chromium.launch();
const page = await browser.newPage({
viewport: { width: 1920, height: 1080 },
});
await page.goto(url, { waitUntil: 'networkidle' });
await page.screenshot({
path: outputPath,
fullPage: true,
type: 'png',
});
await browser.close();
}
When to use: Visual regression testing, page archiving, documentation
Pattern 5: PDF Generation
async function generatePDF(url, outputPath) {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle' });
await page.pdf({
path: outputPath,
format: 'A4',
printBackground: true,
margin: {
top: '1cm',
right: '1cm',
bottom: '1cm',
left: '1cm',
},
});
await browser.close();
}
When to use: Report generation, invoice archiving, content preservation
Pattern 6: Form Automation with Validation
async function fillFormWithValidation(page) {
// Fill fields
await page.fill('input[name="firstName"]', 'John');
await page.fill('input[name="lastName"]', 'Doe');
await page.fill('input[name="email"]', 'john@example.com');
// Handle dropdowns
await page.selectOption('select[name="country"]', 'US');
// Handle checkboxes
await page.check('input[name="terms"]');
// Wait for validation
await page.waitForSelector('input[name="email"]:valid');
// Submit
await page.click('button[type="submit"]');
// Wait for success message
await page.waitForSelector('.success-message', { timeout: 10000 });
}
When to use: Account creation, form testing, data entry automation
Pattern 7: Retry with Exponential Backoff
async function retryWithBackoff(fn, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s
console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// Usage
await retryWithBackoff(async () => {
await page.goto('https://unreliable-site.com');
});
When to use: Flaky networks, rate-limited APIs, unreliable sites
Using Bundled Resources
Templates (templates/)
All templates are ready-to-use TypeScript files. Copy from ~/.claude/skills/playwright-local/templates/:
basic-scrape.ts- Simple page scrapingstealth-mode.ts- Full stealth configurationauthenticated-session.ts- Login + scrape patterninfinite-scroll.ts- Scroll until no new contentscreenshot-capture.ts- Full page screenshotspdf-generation.ts- PDF export
Example Usage:
# Copy template
cp ~/.claude/skills/playwright-local/templates/stealth-mode.ts ./scrape.ts
# Edit for your use case
# Run with tsx
npx tsx scrape.ts
References (references/)
Documentation Claude can load when needed:
references/stealth-techniques.md- Complete anti-detection guidereferences/selector-strategies.md- CSS vs XPath vs text selectorsreferences/common-blocks.md- Known blocking patterns and bypasses
When Claude should load these: When troubleshooting bot detection, selector issues, or site-specific blocks
Scripts (scripts/)
scripts/install-browsers.sh- Install all Playwright browsers
Usage:
chmod +x ~/.claude/skills/playwright-local/scripts/install-browsers.sh
~/.claude/skills/playwright-local/scripts/install-browsers.sh
Advanced Topics
Playwright MCP Server (v1.56+)
Microsoft provides an official Playwright MCP Server for AI agent integration:
# Initialize AI agent configurations
npx playwright init-agents
This generates configuration files for:
- VS Code - Copilot integration
- Claude Desktop - Claude MCP client
- opencode - Open-source AI coding tools
Key Features:
- Uses accessibility tree instead of screenshots (faster, more reliable)
- LLM-friendly structured data format
- Integrated with GitHub Copilot's Coding Agent
- Model Context Protocol (MCP) compliant
MCP Server Setup:
# Install globally
npm install -g @anthropic/mcp-playwright
# Or add to Claude Desktop config
{
"mcpServers": {
"playwright": {
"command": "npx",
"args": ["@anthropic/mcp-playwright"]
}
}
}
When to use: Building AI agents that need browser automation, integrating Playwright with Claude or other LLMs.
Performance Analysis with Speedboard (v1.57+)
Playwright v1.57 introduced Speedboard in the HTML reporter - a dedicated tab for identifying slow tests and performance bottlenecks.
Enable in Config:
export default defineConfig({
reporter: 'html',
});
View Speedboard:
npx playwright test --reporter=html
npx playwright show-report
What Speedboard Shows:
- All tests sorted by execution time (slowest first)
- Breakdown of wait times
- Network request durations
- Helps identify inefficient selectors and unnecessary waits
Use Cases:
- Optimize test suite runtime
- Find tests with excessive
waitForTimeout()calls - Identify slow API responses affecting tests
- Prioritize refactoring efforts for slowest tests
Docker Deployment
Official Docker images provide consistent, reproducible environments:
Current Image (v1.57.0):
FROM mcr.microsoft.com/playwright:v1.57.0-noble
# Create non-root user for security
RUN groupadd -r pwuser && useradd -r -g pwuser pwuser
USER pwuser
WORKDIR /app
COPY --chown=pwuser:pwuser . .
RUN npm ci
CMD ["npx", "playwright", "test"]
Available Tags:
:v1.57.0-noble- Ubuntu 24.04 LTS (recommended):v1.57.0-jammy- Ubuntu 22.04 LTS
Run with Recommended Flags:
docker run -it --init --ipc=host my-playwright-tests
| Flag | Purpose |
|---|---|
--init | Prevents zombie processes (handles PID=1) |
--ipc=host | Prevents Chromium memory exhaustion |
--cap-add=SYS_ADMIN | Only for local dev (enables sandbox) |
Python Image:
FROM mcr.microsoft.com/playwright/python:v1.57.0-noble
Security Notes:
- Always create a non-root user inside the container
- Root user disables Chromium sandbox (security risk)
- Use seccomp profile for untrusted websites
- Pin to specific version tags (avoid
:latest)
Running Playwright in Claude Code via Bash Tool
Claude Code can orchestrate browser automation:
// scrape.ts
import { chromium } from 'playwright';
async function scrape(url: string) {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto(url);
const data = await page.evaluate(() => {
return {
title: document.title,
headings: Array.from(document.querySelectorAll('h1, h2'))
.map(el => el.textContent),
};
});
await browser.close();
console.log(JSON.stringify(data, null, 2));
}
scrape(process.argv[2]);
Claude Code workflow:
- Write script with Bash tool:
npx tsx scrape.ts https://example.com - Capture JSON output
- Parse and analyze results
- Generate summary or next actions
Screenshot Review Workflow
// screenshot-review.ts
import { chromium } from 'playwright';
async function captureForReview(url: string) {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(url);
await page.screenshot({ path: '/tmp/review.png', fullPage: true });
await browser.close();
console.log('Screenshot saved to /tmp/review.png');
}
captureForReview(process.argv[2]);
Claude Code can then:
- Run script via Bash tool
- Read screenshot with Read tool
- Analyze visual layout
- Suggest improvements
Parallel Browser Contexts for Speed
import { chromium } from 'playwright';
async function scrapeConcurrently(urls: string[]) {
const browser = await chromium.launch();
// Use separate contexts for isolation
const results = await Promise.all(
urls.map(async (url) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(url);
const title = await page.title();
await context.close();
return { url, title };
})
);
await browser.close();
return results;
}
Performance gain: 10 URLs in parallel takes ~same time as 1 URL
Browser Fingerprinting Defense
async function setupStealthContext(browser) {
const context = await browser.newContext({
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
viewport: { width: 1920, height: 1080 },
locale: 'en-US',
timezoneId: 'America/New_York',
// WebGL fingerprinting defense
screen: {
width: 1920,
height: 1080,
},
// Geolocation (if needed)
geolocation: { longitude: -74.006, latitude: 40.7128 },
permissions: ['geolocation'],
});
return context;
}
Handling Dynamic Content Loading
async function waitForDynamicContent(page, selector) {
// Wait for initial element
await page.waitForSelector(selector);
// Wait for content to stabilize (no changes for 2s)
let previousContent = '';
let stableCount = 0;
while (stableCount < 4) {
await page.waitForTimeout(500);
const currentContent = await page.locator(selector).textContent();
if (currentContent === previousContent) {
stableCount++;
} else {
stableCount = 0;
}
previousContent = currentContent;
}
return previousContent;
}
Quick Reference
Selector Strategies
| Strategy | Example | When to Use |
|---|---|---|
| CSS | page.click('button.submit') | Standard HTML elements |
| XPath | page.click('xpath=//button[text()="Submit"]') | Complex DOM queries |
| Text | page.click('text=Submit') | When text is unique |
| Data attributes | page.click('[data-testid="submit"]') | Test automation |
| Nth child | page.click('ul > li:nth-child(2)') | Position-based |
Wait Strategies
| Method | Use Case |
|---|---|
waitUntil: 'load' | All resources loaded (default) |
waitUntil: 'domcontentloaded' | DOM ready, faster for slow sites |
waitUntil: 'networkidle' | No network activity for 500ms (SPAs) |
page.waitForSelector(selector) | Element appears in DOM |
page.waitForLoadState('networkidle') | After navigation |
page.waitForTimeout(ms) | Fixed delay (avoid if possible) |
Browser Launch Options
| Option | Value | Purpose |
|---|---|---|
headless | true/false | Show browser UI |
slowMo | 100 (ms) | Slow down for debugging |
args | ['--no-sandbox'] | Disable sandbox (Docker) |
executablePath | /path/to/chrome | Use custom browser |
downloadsPath | ./downloads | Download location |
Common Launch Args for Stealth
args: [
'--disable-blink-features=AutomationControlled',
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--no-first-run',
'--no-zygote',
'--single-process',
'--disable-gpu',
]
Dependencies
Required:
- playwright@1.57.0 - Core browser automation library
- Node.js 20+ (Node.js 18 deprecated) or Python 3.9+ - Runtime
Optional (Node.js stealth):
- playwright-extra@4.3.6 - Plugin system for Playwright
- puppeteer-extra-plugin-stealth@2.11.2 - Anti-detection plugin
Optional (Python stealth):
- playwright-stealth@0.0.1 - Python stealth library
Docker Images:
mcr.microsoft.com/playwright:v1.57.0-noble- Ubuntu 24.04, Node.js 22 LTSmcr.microsoft.com/playwright/python:v1.57.0-noble- Python variant
Official Documentation
- Playwright: https://playwright.dev/docs/intro
- Playwright API: https://playwright.dev/docs/api/class-playwright
- Stealth Plugin: https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin-stealth
- Context7 Library ID: /microsoft/playwright
Package Versions (Verified 2026-01-10)
{
"devDependencies": {
"playwright": "^1.57.0",
"@playwright/test": "^1.57.0",
"playwright-extra": "^4.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2"
}
}
Python:
playwright==1.57.0
playwright-stealth==0.0.1
Production Example
This skill is based on production web scraping systems:
- Daily Scraping Volume: 10,000+ pages/day
- Success Rate: 98%+ (with retries)
- Bot Detection Bypass: 95%+ success on protected sites
- Errors: All 8 known issues prevented via patterns above
- Validation: ✅ Residential IP, stealth mode, session persistence tested
Troubleshooting
Problem: "Executable doesn't exist" error
Solution:
npx playwright install chromium
# Or for all browsers:
npx playwright install
Problem: Slow performance in Docker
Solution: Add shared memory size
# In Dockerfile
RUN playwright install --with-deps chromium
# Run with:
docker run --shm-size=2gb your-image
Problem: Screenshots are blank
Solution: Wait for content to load
await page.goto(url, { waitUntil: 'networkidle' });
await page.waitForTimeout(1000); // Extra buffer
await page.screenshot({ path: 'output.png' });
Problem: "Page crashed" errors
Solution: Reduce concurrency or add memory
const browser = await chromium.launch({
args: ['--disable-dev-shm-usage'], // Use /tmp instead of /dev/shm
});
Problem: Captcha always appears
Solution:
- Verify stealth mode is active (check bot.sannysoft.com)
- Rotate user agents
- Add random delays between actions
- Use residential proxy if needed
Problem: Ubuntu 25.10 installation fails
Error: Unable to locate package libicu74, Package 'libxml2' has no installation candidate
Source: GitHub Issue #38874
Solution:
# Use Ubuntu 24.04 Docker image (officially supported)
docker pull mcr.microsoft.com/playwright:v1.57.0-noble
# Or wait for Ubuntu 25.10 support in future releases
# Track issue: https://github.com/microsoft/playwright/issues/38874
Temporary workaround (if Docker not an option):
# Manually install compatible libraries
sudo apt-get update
sudo apt-get install libicu72 libxml2
Complete Setup Checklist
Use this checklist to verify your setup:
- Playwright installed (
npm list playwrightorpip show playwright) - Browsers downloaded (
npx playwright install chromium) - Basic script runs successfully
- Stealth mode configured (if needed)
- Session persistence works
- Screenshots save correctly
- Error handling includes retries
- Browser closes properly (no zombie processes)
- Tested with
headless: falsefirst - Production script uses
headless: true
Questions? Issues?
- Check
references/common-blocks.mdfor site-specific blocks - Verify stealth setup at https://bot.sannysoft.com/
- Check official docs: https://playwright.dev/docs/intro
- Ensure browser binaries are installed:
npx playwright install chromium
Last verified: 2026-01-21 | Skill version: 3.1.0 | Changes: Added 2 critical CI issues (page.pause() timeout, extension permission prompts), v1.57 features (Speedboard, webServer wait config), Ubuntu 25.10 compatibility guidance
FAQ
What does the playwright-local skill do?
Build browser automation and web scraping with Playwright on your local machine. Prevents 10 documented errors including CI timeout hangs, extension testing failures, and Ubuntu compatibility issues. Includes stealth mode for anti-bot bypass, authenticated sessions, infinite scroll handling, screenshot/PDF generation, and v1.57 Speedboard performance analysis. Use when: automating browsers, scraping protected sites, testing with real IPs, bypassing bot detection, generating screenshots/PDFs, or troubleshooting "target closed", "page.pause() hangs CI", "permission prompts block tests", or "Ubuntu 25.10 installation" errors.
How do I install playwright-local?
Run npx skills add https://github.com/jezweb/claude-skills --skill playwright-local in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/playwright-local/ (personal) or .claude/skills/playwright-local/ (project).
Which AI agents can use playwright-local?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is playwright-local safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains playwright-local?
It is published by jezweb in jezweb/claude-skills. It has 505 recorded installs.
More from jezweb/claude-skills
shadcn-ui
jezweb/claude-skills
Install and configure shadcn/ui components for React projects. Guides component selection, installation order, dependency management, customisation with semantic tokens, and common UI recipes (forms, data tables, navigation, modals). Use after tailwind-theme-builder has set up the theme infrastructure, when adding components, building forms, creating data tables, or setting up navigation.
color-palette
jezweb/claude-skills
Generate complete, accessible colour palettes from a single brand hex. Produces 11-shade scale (50-950), semantic tokens, dark mode variants, Tailwind v4 CSS output, WCAG contrast checks. Use whenever the user supplies a brand hex and asks for a palette, mentions setting up a design system, wants Tailwind theme colours from a brand colour, or asks to check colour accessibility / contrast.
tailwind-theme-builder
jezweb/claude-skills
Set up Tailwind v4 + shadcn/ui themed UI with dark mode. Install deps, configure CSS variables via @theme inline, wire dark mode toggle, verify. Use whenever the user mentions Tailwind v4, setting up Tailwind theming, shadcn/ui colours, dark mode, or troubleshooting colours not working, tw-animate-css errors, @theme inline conflicts, @apply breaking after upgrade, or v3 → v4 migration issues.
wordpress-elementor
jezweb/claude-skills
Edit Elementor pages and manage templates on WordPress sites via browser automation (for visual / structural changes) or WP-CLI (for safe text replacements). Use whenever the user wants to edit an Elementor page, update text in Elementor widgets, apply or manage Elementor templates, or make content changes to pages built with the Elementor page builder.
ux-audit
jezweb/claude-skills
Walk through a live web app AS a real user to find usability + behavioural bugs that static reviews miss. REQUIRES proof of interaction (typing, clicking, sending, observing) before any verdict — a sweep that didn't interact terminates with verdict 'Incomplete'. Walks threads, exercises every element, runs the multi-pane stress matrix, visual polish sweep, component perfection checklist, automated a11y (axe-core), pragmatic performance budget (LCP/CLS/INP), scenario battery (11 scenarios), and stress recipes including the real-flavour data battery. Hard gates: console errors/warnings = 0, network 5xx = 0, layout collapse = 0, axe Critical/Serious = 0, perf budget green. Audit-the-audit meta-check rejects rushed reports. Each finding has reproduction steps, evidence path, and suspected code location. Trigger with 'ux audit', 'walkthrough', 'qa sweep', 'audit the app', 'dogfood this', 'check all pages', 'find what's broken', 'stress the UI'.
tailwind-v4-shadcn
jezweb/claude-skills
Set up Tailwind v4 with shadcn/ui using @theme inline pattern and CSS variable architecture. Four-step pattern: CSS variables, Tailwind mapping, base styles, automatic dark mode. Prevents 8 documented errors. Use when initializing React projects with Tailwind v4, or fixing colors not working, tw-animate-css errors, @theme inline dark mode conflicts, @apply breaking, v3 migration issues.
Popular testing & qa skills
tdd
mattpocock/skills
Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.
grilling
mattpocock/skills
Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases.
migrate-to-shoehorn
mattpocock/skills
Migrate test files from as type assertions to @total-typescript/shoehorn. Use when user mentions shoehorn, wants to replace as in tests, or needs partial test data.
run-train
lllllllama/rigorpilot-skills
Rigor Train skill for deep learning research repositories. Use when a documented or selected training command should be run conservatively for startup verification, short-run verification, full kickoff, or resume, with command, config, seed, log, checkpoint, status, and metric evidence written to standardized trainoutputs/. Do not use for environment setup, exploratory sweeps, speculative idea implementation, or end-to-end orchestration.
test-driven-development
obra/superpowers
Use when implementing any feature or bugfix, before writing implementation code
finishing-a-development-branch
obra/superpowers
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work