playwright-local

|

jezweb/claude-skills512 installsMITSynced Aug 26

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
name: playwright-local
description: |
license: MIT
---

# 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](https://developer.chrome.com/blog/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**:
```bash
npm install -D playwright
npx playwright install chromium
```

**Python**:
```bash
pip install playwright
playwright install chromium
```

**Why this matters:**
- `playwright install` downloads browser binaries (~400MB for Chromium)
- Install only needed browsers: `chromium`, `firefox`, or `webkit`
- Binaries stored in `~/.cache/ms-playwright/`

### 2. Basic Page Scrape

```typescript
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: 60000` if needed

### 3. Test Locally

```bash
# 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 `steps` option)
> - Consider residential proxies for heavily protected sites
> - "What works today may not work tomorrow" - test regularly
> - For advanced scenarios, research alternatives like `nodriver` or `undetected-chromedriver`

### Step 1: Install Stealth Plugin (Node.js)

```bash
npm install playwright-extra playwright-stealth
```

**For puppeteer-extra compatibility**:
```bash
npm install puppeteer-extra puppeteer-extra-plugin-stealth
```

### Step 2: Configure Stealth Mode

**playwright-extra**:
```typescript
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` flag
- Randomize viewport sizes to avoid fingerprinting
- Match user agent to browser version (Chrome 120 example above)

### Step 3: Mask WebDriver Detection

```typescript
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

```typescript
// 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

```typescript
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

```typescript
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/

```typescript
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`)
- 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

```typescript
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

```typescript
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

```typescript
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

```typescript
// 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

```typescript
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**:
```typescript
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**:
```typescript
// 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**:
```typescript
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**:
```typescript
// 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**:
```typescript
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**:
```typescript
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**:
```typescript
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](https://github.com/microsoft/playwright/issues/38754)
**Why It Happens**: `page.pause()` is ignored in headless mode but disables test timeout, causing subsequent failing assertions to hang forever
**Prevention**:
```typescript
// 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](https://github.com/microsoft/playwright/issues/38670)
**Why It Happens**: `launchPersistentContext` with extensions shows non-dismissible permission prompts (clipboard-read/write, local-network-access) that cannot be auto-granted
**Prevention**:
```typescript
// 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)

```typescript
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 tests
- `screenshot: 'only-on-failure'` - Saves disk space
- `viewport: { 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:

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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 scraping
- `stealth-mode.ts` - Full stealth configuration
- `authenticated-session.ts` - Login + scrape pattern
- `infinite-scroll.ts` - Scroll until no new content
- `screenshot-capture.ts` - Full page screenshots
- `pdf-generation.ts` - PDF export

**Example Usage:**
```bash
# 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 guide
- `references/selector-strategies.md` - CSS vs XPath vs text selectors
- `references/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:**
```bash
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](https://github.com/microsoft/playwright-mcp) for AI agent integration:

```bash
# 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**:
```bash
# 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**:
```typescript
export default defineConfig({
  reporter: 'html',
});
```

**View Speedboard**:
```bash
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):
```dockerfile
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**:
```bash
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**:
```dockerfile
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:

```typescript
// 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:**
1. Write script with Bash tool: `npx tsx scrape.ts https://example.com`
2. Capture JSON output
3. Parse and analyze results
4. Generate summary or next actions

### Screenshot Review Workflow

```typescript
// 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:**
1. Run script via Bash tool
2. Read screenshot with Read tool
3. Analyze visual layout
4. Suggest improvements

### Parallel Browser Contexts for Speed

```typescript
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

```typescript
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

```typescript
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

```typescript
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 LTS
- `mcr.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)

```json
{
  "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**:
```bash
npx playwright install chromium
# Or for all browsers:
npx playwright install
```

### Problem: Slow performance in Docker
**Solution**: Add shared memory size
```dockerfile
# 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
```typescript
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
```typescript
const browser = await chromium.launch({
  args: ['--disable-dev-shm-usage'], // Use /tmp instead of /dev/shm
});
```

### Problem: Captcha always appears
**Solution**:
1. Verify stealth mode is active (check bot.sannysoft.com)
2. Rotate user agents
3. Add random delays between actions
4. 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](https://github.com/microsoft/playwright/issues/38874)
**Solution**:
```bash
# 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):
```bash
# 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 playwright` or `pip 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: false` first
- [ ] Production script uses `headless: true`

---

**Questions? Issues?**

1. Check `references/common-blocks.md` for site-specific blocks
2. Verify stealth setup at https://bot.sannysoft.com/
3. Check official docs: https://playwright.dev/docs/intro
4. 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

More Testing skills

← All Testing skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY