Runs Playwright locally for browser automation, web scraping, and testing with stealth mode, error fixes for CI hangs/Ubuntu, screenshots, and PDFs.
From evolv3ainpx claudepluginhub evolv3ai/claude-skills-archive --plugin projectThis skill uses the workspace's default tool permissions.
README.mdassets/example-template.txtreferences/common-blocks.mdreferences/selector-strategies.mdreferences/stealth-techniques.mdscripts/install-browsers.shtemplates/authenticated-session.tstemplates/basic-scrape.tstemplates/infinite-scroll.tstemplates/pdf-generation.tstemplates/screenshot-capture.tstemplates/stealth-mode.tsGuides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Executes pre-written implementation plans: critically reviews, follows bite-sized steps exactly, runs verifications, tracks progress with checkpoints, uses git worktrees, stops on blockers.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
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.
Node.js:
npm install -D playwright
npx playwright install chromium
Python:
pip install playwright
playwright install chromium
Why this matters:
playwright install downloads browser binaries (~400MB for Chromium)chromium, firefox, or webkit~/.cache/ms-playwright/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:
await browser.close() to avoid zombie processeswaitUntil: 'networkidle' for dynamic content (SPAs)timeout: 60000 if needed# Node.js
npx tsx scrape.ts
# Python
python scrape.py
| 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
⚠️ 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
npm install playwright-extra playwright-stealth
For puppeteer-extra compatibility:
npm install puppeteer-extra puppeteer-extra-plugin-stealth
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=AutomationControlled removes navigator.webdriver flagawait 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)
);
});
// 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 });
}
}
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,
});
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);
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.webdriver should be undefined (not false)✅ 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
❌ 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)
Playwright v1.56 introduced new methods for capturing debug information without setting up event listeners:
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()));
});
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);
});
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.
The steps option provides fine-grained control over mouse movement, useful for:
// 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 });
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.
This skill prevents 10 documented issues:
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();
}
}
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();
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');
}
}
}
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');
}
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
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());
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;
}
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'],
});
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
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
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 flagWait 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:
When to Use:
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
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
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
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
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
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
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
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 exportExample 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
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 bypassesWhen Claude should load these: When troubleshooting bot detection, selector issues, or site-specific blocks
scripts/install-browsers.sh - Install all Playwright browsersUsage:
chmod +x ~/.claude/skills/playwright-local/scripts/install-browsers.sh
~/.claude/skills/playwright-local/scripts/install-browsers.sh
Microsoft provides an official Playwright MCP Server for AI agent integration:
# Initialize AI agent configurations
npx playwright init-agents
This generates configuration files for:
Key Features:
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.
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:
Use Cases:
waitForTimeout() callsOfficial 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 LTSRun 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:
:latest)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:
npx tsx scrape.ts https://example.com// 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:
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
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;
}
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;
}
| 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 |
| 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) |
| 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 |
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',
]
Required:
Optional (Node.js stealth):
Optional (Python stealth):
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{
"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
This skill is based on production web scraping systems:
Solution:
npx playwright install chromium
# Or for all browsers:
npx playwright install
Solution: Add shared memory size
# In Dockerfile
RUN playwright install --with-deps chromium
# Run with:
docker run --shm-size=2gb your-image
Solution: Wait for content to load
await page.goto(url, { waitUntil: 'networkidle' });
await page.waitForTimeout(1000); // Extra buffer
await page.screenshot({ path: 'output.png' });
Solution: Reduce concurrency or add memory
const browser = await chromium.launch({
args: ['--disable-dev-shm-usage'], // Use /tmp instead of /dev/shm
});
Solution:
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
Use this checklist to verify your setup:
npm list playwright or pip show playwright)npx playwright install chromium)headless: false firstheadless: trueQuestions? Issues?
references/common-blocks.md for site-specific blocksnpx playwright install chromiumLast 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