API_REFERENCE.md
# Playwright Skill - Complete API Reference
This document contains the comprehensive Playwright API reference and advanced patterns. For quick-start execution patterns, see [SKILL.md](SKILL.md).
## Table of Contents
- [Installation & Setup](#installation--setup)
- [Core Patterns](#core-patterns)
- [Selectors & Locators](#selectors--locators)
- [Common Actions](#common-actions)
- [Waiting Strategies](#waiting-strategies)
- [Assertions](#assertions)
- [Page Object Model](#page-object-model-pom)
- [Network & API Testing](#network--api-testing)
- [Authentication & Session Management](#authentication--session-management)
- [Visual Testing](#visual-testing)
- [Mobile Testing](#mobile-testing)
- [Debugging](#debugging)
- [Performance Testing](#performance-testing)
- [Parallel Execution](#parallel-execution)
- [Data-Driven Testing](#data-driven-testing)
- [Accessibility Testing](#accessibility-testing)
- [CI/CD Integration](#cicd-integration)
- [Best Practices](#best-practices)
- [Common Patterns & Solutions](#common-patterns--solutions)
- [Troubleshooting](#troubleshooting)
## Installation & Setup
### Prerequisites
Before using this skill, ensure Playwright is available:
```bash
# Check if Playwright is installed
npm list playwright 2>/dev/null || echo "Playwright not installed"
# Install (if needed)
cd ~/.claude/skills/playwright-skill
npm run setup
```
### Basic Configuration
Create `playwright.config.ts`:
```typescript
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
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',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'npm run start',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
```
## Core Patterns
### Basic Browser Automation
```javascript
const { chromium } = require('playwright');
(async () => {
// Launch browser
const browser = await chromium.launch({
headless: false, // Set to true for headless mode
slowMo: 50 // Slow down operations by 50ms
});
const context = await browser.newContext({
viewport: { width: 1280, height: 720 },
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
});
const page = await context.newPage();
// Navigate
await page.goto('https://example.com', {
waitUntil: 'networkidle' // Wait for network to be idle
});
// Your automation here
await browser.close();
})();
```
### Test Structure
```typescript
import { test, expect } from '@playwright/test';
test.describe('Feature Name', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('should do something', async ({ page }) => {
// Arrange
const button = page.locator('button[data-testid="submit"]');
// Act
await button.click();
// Assert
await expect(page).toHaveURL('/success');
await expect(page.locator('.message')).toHaveText('Success!');
});
});
```
## Selectors & Locators
### Best Practices for Selectors
```javascript
// PREFERRED: Data attributes (most stable)
await page.locator('[data-testid="submit-button"]').click();
await page.locator('[data-cy="user-input"]').fill('text');
// GOOD: Role-based selectors (accessible)
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
await page.getByRole('heading', { level: 1 }).click();
// GOOD: Text content (for unique text)
await page.getByText('Sign in').click();
await page.getByText(/welcome back/i).click();
// OK: Semantic HTML
await page.locator('button[type="submit"]').click();
await page.locator('input[name="email"]').fill('test@test.com');
// AVOID: Classes and IDs (can change frequently)
await page.locator('.btn-primary').click(); // Avoid
await page.locator('#submit').click(); // Avoid
// LAST RESORT: Complex CSS/XPath
await page.locator('div.container > form > button').click(); // Fragile
```
### Advanced Locator Patterns
```javascript
// Filter and chain locators
const row = page.locator('tr').filter({ hasText: 'John Doe' });
await row.locator('button').click();
// Nth element
await page.locator('button').nth(2).click();
// Combining conditions
await page.locator('button').and(page.locator('[disabled]')).count();
// Parent/child navigation
const cell = page.locator('td').filter({ hasText: 'Active' });
const row = cell.locator('..');
await row.locator('button.edit').click();
```
## Common Actions
### Form Interactions
```javascript
// Text input
await page.getByLabel('Email').fill('user@example.com');
await page.getByPlaceholder('Enter your name').fill('John Doe');
// Clear and type
await page.locator('#username').clear();
await page.locator('#username').type('newuser', { delay: 100 });
// Checkbox
await page.getByLabel('I agree').check();
await page.getByLabel('Subscribe').uncheck();
// Radio button
await page.getByLabel('Option 2').check();
// Select dropdown
await page.selectOption('select#country', 'usa');
await page.selectOption('select#country', { label: 'United States' });
await page.selectOption('select#country', { index: 2 });
// Multi-select
await page.selectOption('select#colors', ['red', 'blue', 'green']);
// File upload
await page.setInputFiles('input[type="file"]', 'path/to/file.pdf');
await page.setInputFiles('input[type="file"]', [
'file1.pdf',
'file2.pdf'
]);
```
### Mouse Actions
```javascript
// Click variations
await page.click('button'); // Left click
await page.click('button', { button: 'right' }); // Right click
await page.dblclick('button'); // Double click
await page.click('button', { position: { x: 10, y: 10 } }); // Click at position
// Hover
await page.hover('.menu-item');
// Drag and drop
await page.dragAndDrop('#source', '#target');
// Manual drag
await page.locator('#source').hover();
await page.mouse.down();
await page.locator('#target').hover();
await page.mouse.up();
```
### Keyboard Actions
```javascript
// Type with delay
await page.keyboard.type('Hello World', { delay: 100 });
// Key combinations
await page.keyboard.press('Control+A');
await page.keyboard.press('Control+C');
await page.keyboard.press('Control+V');
// Special keys
await page.keyboard.press('Enter');
await page.keyboard.press('Tab');
await page.keyboard.press('Escape');
await page.keyboard.press('ArrowDown');
```
## Waiting Strategies
### Smart Waiting
```javascript
// Wait for element states
await page.locator('button').waitFor({ state: 'visible' });
await page.locator('.spinner').waitFor({ state: 'hidden' });
await page.locator('button').waitFor({ state: 'attached' });
await page.locator('button').waitFor({ state: 'detached' });
// Wait for specific conditions
await page.waitForURL('**/success');
await page.waitForURL(url => url.pathname === '/dashboard');
// Wait for network
await page.waitForLoadState('networkidle');
await page.waitForLoadState('domcontentloaded');
// Wait for function
await page.waitForFunction(() => document.querySelector('.loaded'));
await page.waitForFunction(
text => document.body.innerText.includes(text),
'Content loaded'
);
// Wait for response
const responsePromise = page.waitForResponse('**/api/users');
await page.click('button#load-users');
const response = await responsePromise;
// Wait for request
await page.waitForRequest(request =>
request.url().includes('/api/') && request.method() === 'POST'
);
// Custom timeout
await page.locator('.slow-element').waitFor({
state: 'visible',
timeout: 10000 // 10 seconds
});
```
## Assertions
### Common Assertions
```javascript
import { expect } from '@playwright/test';
// Page assertions
await expect(page).toHaveTitle('My App');
await expect(page).toHaveURL('https://example.com/dashboard');
await expect(page).toHaveURL(/.*dashboard/);
// Element visibility
await expect(page.locator('.message')).toBeVisible();
await expect(page.locator('.spinner')).toBeHidden();
await expect(page.locator('button')).toBeEnabled();
await expect(page.locator('input')).toBeDisabled();
// Text content
await expect(page.locator('h1')).toHaveText('Welcome');
await expect(page.locator('.message')).toContainText('success');
await expect(page.locator('.items')).toHaveText(['Item 1', 'Item 2']);
// Input values
await expect(page.locator('input')).toHaveValue('test@example.com');
await expect(page.locator('input')).toBeEmpty();
// Attributes
await expect(page.locator('button')).toHaveAttribute('type', 'submit');
await expect(page.locator('img')).toHaveAttribute('src', /.*\.png/);
// CSS properties
await expect(page.locator('.error')).toHaveCSS('color', 'rgb(255, 0, 0)');
// Count
await expect(page.locator('.item')).toHaveCount(5);
// Checkbox/Radio state
await expect(page.locator('input[type="checkbox"]')).toBeChecked();
```
## Page Object Model (POM)
### Basic Page Object
```javascript
// pages/LoginPage.js
class LoginPage {
constructor(page) {
this.page = page;
this.usernameInput = page.locator('input[name="username"]');
this.passwordInput = page.locator('input[name="password"]');
this.submitButton = page.locator('button[type="submit"]');
this.errorMessage = page.locator('.error-message');
}
async navigate() {
await this.page.goto('/login');
}
async login(username, password) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async getErrorMessage() {
return await this.errorMessage.textContent();
}
}
// Usage in test
test('login with valid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.navigate();
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL('/dashboard');
});
```
## Network & API Testing
### Intercepting Requests
```javascript
// Mock API responses
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
])
});
});
// Modify requests
await page.route('**/api/**', route => {
const headers = {
...route.request().headers(),
'X-Custom-Header': 'value'
};
route.continue({ headers });
});
// Block resources
await page.route('**/*.{png,jpg,jpeg,gif}', route => route.abort());
```
### Custom Headers via Environment Variables
The skill supports automatic header injection via environment variables:
```bash
# Single header (simple)
PW_HEADER_NAME=X-Automated-By PW_HEADER_VALUE=playwright-skill
# Multiple headers (JSON)
PW_EXTRA_HEADERS='{"X-Automated-By":"playwright-skill","X-Request-ID":"123"}'
```
These headers are automatically applied to all requests when using:
- `helpers.createContext(browser)` - headers merged automatically
- `getContextOptionsWithHeaders(options)` - utility injected by run.js wrapper
**Precedence (highest to lowest):**
1. Headers passed directly in `options.extraHTTPHeaders`
2. Environment variable headers
3. Playwright defaults
**Use case:** Identify automated traffic so your backend can return LLM-optimized responses (e.g., plain text errors instead of styled HTML).
## Visual Testing
### Screenshots
```javascript
// Full page screenshot
await page.screenshot({
path: 'screenshot.png',
fullPage: true
});
// Element screenshot
await page.locator('.chart').screenshot({
path: 'chart.png'
});
// Visual comparison
await expect(page).toHaveScreenshot('homepage.png');
```
## Mobile Testing
```javascript
// Device emulation
const { devices } = require('playwright');
const iPhone = devices['iPhone 12'];
const context = await browser.newContext({
...iPhone,
locale: 'en-US',
permissions: ['geolocation'],
geolocation: { latitude: 37.7749, longitude: -122.4194 }
});
```
## Debugging
### Debug Mode
```bash
# Run with inspector
npx playwright test --debug
# Headed mode
npx playwright test --headed
# Slow motion
npx playwright test --headed --slowmo=1000
```
### In-Code Debugging
```javascript
// Pause execution
await page.pause();
// Console logs
page.on('console', msg => console.log('Browser log:', msg.text()));
page.on('pageerror', error => console.log('Page error:', error));
```
## Performance Testing
```javascript
// Measure page load time
const startTime = Date.now();
await page.goto('https://example.com');
const loadTime = Date.now() - startTime;
console.log(`Page loaded in ${loadTime}ms`);
```
## Parallel Execution
```javascript
// Run tests in parallel
test.describe.parallel('Parallel suite', () => {
test('test 1', async ({ page }) => {
// Runs in parallel with test 2
});
test('test 2', async ({ page }) => {
// Runs in parallel with test 1
});
});
```
## Data-Driven Testing
```javascript
// Parameterized tests
const testData = [
{ username: 'user1', password: 'pass1', expected: 'Welcome user1' },
{ username: 'user2', password: 'pass2', expected: 'Welcome user2' },
];
testData.forEach(({ username, password, expected }) => {
test(`login with ${username}`, async ({ page }) => {
await page.goto('/login');
await page.fill('#username', username);
await page.fill('#password', password);
await page.click('button[type="submit"]');
await expect(page.locator('.message')).toHaveText(expected);
});
});
```
## Accessibility Testing
```javascript
import { injectAxe, checkA11y } from 'axe-playwright';
test('accessibility check', async ({ page }) => {
await page.goto('/');
await injectAxe(page);
await checkA11y(page);
});
```
## CI/CD Integration
### GitHub Actions
```yaml
name: Playwright Tests
on:
push:
branches: [main, master]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run tests
run: npx playwright test
```
## Best Practices
1. **Test Organization** - Use descriptive test names, group related tests
2. **Selector Strategy** - Prefer data-testid attributes, use role-based selectors
3. **Waiting** - Use Playwright's auto-waiting, avoid hard-coded delays
4. **Error Handling** - Add proper error messages, take screenshots on failure
5. **Performance** - Run tests in parallel, reuse authentication state
## Common Patterns & Solutions
### Handling Popups
```javascript
const [popup] = await Promise.all([
page.waitForEvent('popup'),
page.click('button.open-popup')
]);
await popup.waitForLoadState();
```
### File Downloads
```javascript
const [download] = await Promise.all([
page.waitForEvent('download'),
page.click('button.download')
]);
await download.saveAs(`./downloads/${download.suggestedFilename()}`);
```
### iFrames
```javascript
const frame = page.frameLocator('#my-iframe');
await frame.locator('button').click();
```
### Infinite Scroll
```javascript
async function scrollToBottom(page) {
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await page.waitForTimeout(500);
}
```
## Troubleshooting
### Common Issues
1. **Element not found** - Check if element is in iframe, verify visibility
2. **Timeout errors** - Increase timeout, check network conditions
3. **Flaky tests** - Use proper waiting strategies, mock external dependencies
4. **Authentication issues** - Verify auth state is properly saved
## Quick Reference Commands
```bash
# Run tests
npx playwright test
# Run in headed mode
npx playwright test --headed
# Debug tests
npx playwright test --debug
# Generate code
npx playwright codegen https://example.com
# Show report
npx playwright show-report
```
## Additional Resources
- [Playwright Documentation](https://playwright.dev/docs/intro)
- [API Reference](https://playwright.dev/docs/api/class-playwright)
- [Best Practices](https://playwright.dev/docs/best-practices)
SKILL.md
---
name: playwright-skill
description: "IMPORTANT - Path Resolution: This skill can be installed in different locations (plugin system, manual installation, global, or project-specific). Before executing any commands, determine the skill directory based on where you loaded this SKILL.md file, and use that path in all commands below."
risk: unknown
source: community
date_added: "2026-02-27"
---
**IMPORTANT - Path Resolution:**
This skill can be installed in different locations (plugin system, manual installation, global, or project-specific). Before executing any commands, determine the skill directory based on where you loaded this SKILL.md file, and use that path in all commands below. Replace `$SKILL_DIR` with the actual discovered path.
Common installation paths:
- Plugin system: `~/.claude/plugins/marketplaces/playwright-skill/skills/playwright-skill`
- Manual global: `~/.claude/skills/playwright-skill`
- Project-specific: `<project>/.claude/skills/playwright-skill`
# Playwright Browser Automation
General-purpose browser automation skill. I'll write custom Playwright code for any automation task you request and execute it via the universal executor.
**CRITICAL WORKFLOW - Follow these steps in order:**
1. **Auto-detect dev servers** - For localhost testing, ALWAYS run server detection FIRST:
```bash
cd $SKILL_DIR && node -e "require('./lib/helpers').detectDevServers().then(servers => console.log(JSON.stringify(servers)))"
```
- If **1 server found**: Use it automatically, inform user
- If **multiple servers found**: Ask user which one to test
- If **no servers found**: Ask for URL or offer to help start dev server
2. **Write scripts to /tmp** - NEVER write test files to skill directory; always use `/tmp/playwright-test-*.js`
3. **Use visible browser by default** - Always use `headless: false` unless user specifically requests headless mode
4. **Parameterize URLs** - Always make URLs configurable via environment variable or constant at top of script
## How It Works
1. You describe what you want to test/automate
2. I auto-detect running dev servers (or ask for URL if testing external site)
3. I write custom Playwright code in `/tmp/playwright-test-*.js` (won't clutter your project)
4. I execute it via: `cd $SKILL_DIR && node run.js /tmp/playwright-test-*.js`
5. Results displayed in real-time, browser window visible for debugging
6. Test files auto-cleaned from /tmp by your OS
## Setup (First Time)
```bash
cd $SKILL_DIR
npm run setup
```
This installs Playwright and Chromium browser. Only needed once.
## Execution Pattern
**Step 1: Detect dev servers (for localhost testing)**
```bash
cd $SKILL_DIR && node -e "require('./lib/helpers').detectDevServers().then(s => console.log(JSON.stringify(s)))"
```
**Step 2: Write test script to /tmp with URL parameter**
```javascript
// /tmp/playwright-test-page.js
const { chromium } = require('playwright');
// Parameterized URL (detected or user-provided)
const TARGET_URL = 'http://localhost:3001'; // <-- Auto-detected or from user
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto(TARGET_URL);
console.log('Page loaded:', await page.title());
await page.screenshot({ path: '/tmp/screenshot.png', fullPage: true });
console.log('📸 Screenshot saved to /tmp/screenshot.png');
await browser.close();
})();
```
**Step 3: Execute from skill directory**
```bash
cd $SKILL_DIR && node run.js /tmp/playwright-test-page.js
```
## Common Patterns
### Test a Page (Multiple Viewports)
```javascript
// /tmp/playwright-test-responsive.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 100 });
const page = await browser.newPage();
// Desktop test
await page.setViewportSize({ width: 1920, height: 1080 });
await page.goto(TARGET_URL);
console.log('Desktop - Title:', await page.title());
await page.screenshot({ path: '/tmp/desktop.png', fullPage: true });
// Mobile test
await page.setViewportSize({ width: 375, height: 667 });
await page.screenshot({ path: '/tmp/mobile.png', fullPage: true });
await browser.close();
})();
```
### Test Login Flow
```javascript
// /tmp/playwright-test-login.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto(`${TARGET_URL}/login`);
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
// Wait for redirect
await page.waitForURL('**/dashboard');
console.log('✅ Login successful, redirected to dashboard');
await browser.close();
})();
```
### Fill and Submit Form
```javascript
// /tmp/playwright-test-form.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: false, slowMo: 50 });
const page = await browser.newPage();
await page.goto(`${TARGET_URL}/contact`);
await page.fill('input[name="name"]', 'John Doe');
await page.fill('input[name="email"]', 'john@example.com');
await page.fill('textarea[name="message"]', 'Test message');
await page.click('button[type="submit"]');
// Verify submission
await page.waitForSelector('.success-message');
console.log('✅ Form submitted successfully');
await browser.close();
})();
```
### Check for Broken Links
```javascript
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto('http://localhost:3000');
const links = await page.locator('a[href^="http"]').all();
const results = { working: 0, broken: [] };
for (const link of links) {
const href = await link.getAttribute('href');
try {
const response = await page.request.head(href);
if (response.ok()) {
results.working++;
} else {
results.broken.push({ url: href, status: response.status() });
}
} catch (e) {
results.broken.push({ url: href, error: e.message });
}
}
console.log(`✅ Working links: ${results.working}`);
console.log(`❌ Broken links:`, results.broken);
await browser.close();
})();
```
### Take Screenshot with Error Handling
```javascript
const { chromium } = require('playwright');
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
try {
await page.goto('http://localhost:3000', {
waitUntil: 'networkidle',
timeout: 10000,
});
await page.screenshot({
path: '/tmp/screenshot.png',
fullPage: true,
});
console.log('📸 Screenshot saved to /tmp/screenshot.png');
} catch (error) {
console.error('❌ Error:', error.message);
} finally {
await browser.close();
}
})();
```
### Test Responsive Design
```javascript
// /tmp/playwright-test-responsive-full.js
const { chromium } = require('playwright');
const TARGET_URL = 'http://localhost:3001'; // Auto-detected
(async () => {
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
const viewports = [
{ name: 'Desktop', width: 1920, height: 1080 },
{ name: 'Tablet', width: 768, height: 1024 },
{ name: 'Mobile', width: 375, height: 667 },
];
for (const viewport of viewports) {
console.log(
`Testing ${viewport.name} (${viewport.width}x${viewport.height})`,
);
await page.setViewportSize({
width: viewport.width,
height: viewport.height,
});
await page.goto(TARGET_URL);
await page.waitForTimeout(1000);
await page.screenshot({
path: `/tmp/${viewport.name.toLowerCase()}.png`,
fullPage: true,
});
}
console.log('✅ All viewports tested');
await browser.close();
})();
```
## Inline Execution (Simple Tasks)
For quick one-off tasks, you can execute code inline without creating files:
```bash
# Take a quick screenshot
cd $SKILL_DIR && node run.js "
const browser = await chromium.launch({ headless: false });
const page = await browser.newPage();
await page.goto('http://localhost:3001');
await page.screenshot({ path: '/tmp/quick-screenshot.png', fullPage: true });
console.log('Screenshot saved');
await browser.close();
"
```
**When to use inline vs files:**
- **Inline**: Quick one-off tasks (screenshot, check if element exists, get page title)
- **Files**: Complex tests, responsive design checks, anything user might want to re-run
## Available Helpers
Optional utility functions in `lib/helpers.js`:
```javascript
const helpers = require('./lib/helpers');
// Detect running dev servers (CRITICAL - use this first!)
const servers = await helpers.detectDevServers();
console.log('Found servers:', servers);
// Safe click with retry
await helpers.safeClick(page, 'button.submit', { retries: 3 });
// Safe type with clear
await helpers.safeType(page, '#username', 'testuser');
// Take timestamped screenshot
await helpers.takeScreenshot(page, 'test-result');
// Handle cookie banners
await helpers.handleCookieBanner(page);
// Extract table data
const data = await helpers.extractTableData(page, 'table.results');
```
See `lib/helpers.js` for full list.
## Custom HTTP Headers
Configure custom headers for all HTTP requests via environment variables. Useful for:
- Identifying automated traffic to your backend
- Getting LLM-optimized responses (e.g., plain text errors instead of styled HTML)
- Adding authentication tokens globally
### Configuration
**Single header (common case):**
```bash
PW_HEADER_NAME=X-Automated-By PW_HEADER_VALUE=playwright-skill \
cd $SKILL_DIR && node run.js /tmp/my-script.js
```
**Multiple headers (JSON format):**
```bash
PW_EXTRA_HEADERS='{"X-Automated-By":"playwright-skill","X-Debug":"true"}' \
cd $SKILL_DIR && node run.js /tmp/my-script.js
```
### How It Works
Headers are automatically applied when using `helpers.createContext()`:
```javascript
const context = await helpers.createContext(browser);
const page = await context.newPage();
// All requests from this page include your custom headers
```
For scripts using raw Playwright API, use the injected `getContextOptionsWithHeaders()`:
```javascript
const context = await browser.newContext(
getContextOptionsWithHeaders({ viewport: { width: 1920, height: 1080 } }),
);
```
## Advanced Usage
For comprehensive Playwright API documentation, see [API_REFERENCE.md](API_REFERENCE.md):
- Selectors & Locators best practices
- Network interception & API mocking
- Authentication & session management
- Visual regression testing
- Mobile device emulation
- Performance testing
- Debugging techniques
- CI/CD integration
## Tips
- **CRITICAL: Detect servers FIRST** - Always run `detectDevServers()` before writing test code for localhost testing
- **Custom headers** - Use `PW_HEADER_NAME`/`PW_HEADER_VALUE` env vars to identify automated traffic to your backend
- **Use /tmp for test files** - Write to `/tmp/playwright-test-*.js`, never to skill directory or user's project
- **Parameterize URLs** - Put detected/provided URL in a `TARGET_URL` constant at the top of every script
- **DEFAULT: Visible browser** - Always use `headless: false` unless user explicitly asks for headless mode
- **Headless mode** - Only use `headless: true` when user specifically requests "headless" or "background" execution
- **Slow down:** Use `slowMo: 100` to make actions visible and easier to follow
- **Wait strategies:** Use `waitForURL`, `waitForSelector`, `waitForLoadState` instead of fixed timeouts
- **Error handling:** Always use try-catch for robust automation
- **Console output:** Use `console.log()` to track progress and show what's happening
## Troubleshooting
**Playwright not installed:**
```bash
cd $SKILL_DIR && npm run setup
```
**Module not found:**
Ensure running from skill directory via `run.js` wrapper
**Browser doesn't open:**
Check `headless: false` and ensure display available
**Element not found:**
Add wait: `await page.waitForSelector('.element', { timeout: 10000 })`
## Example Usage
```
User: "Test if the marketing page looks good"
Claude: I'll test the marketing page across multiple viewports. Let me first detect running servers...
[Runs: detectDevServers()]
[Output: Found server on port 3001]
I found your dev server running on http://localhost:3001
[Writes custom automation script to /tmp/playwright-test-marketing.js with URL parameterized]
[Runs: cd $SKILL_DIR && node run.js /tmp/playwright-test-marketing.js]
[Shows results with screenshots from /tmp/]
```
```
User: "Check if login redirects correctly"
Claude: I'll test the login flow. First, let me check for running servers...
[Runs: detectDevServers()]
[Output: Found servers on ports 3000 and 3001]
I found 2 dev servers. Which one should I test?
- http://localhost:3000
- http://localhost:3001
User: "Use 3001"
[Writes login automation to /tmp/playwright-test-login.js]
[Runs: cd $SKILL_DIR && node run.js /tmp/playwright-test-login.js]
[Reports: ✅ Login successful, redirected to /dashboard]
```
## Notes
- Each automation is custom-written for your specific request
- Not limited to pre-built scripts - any browser task possible
- Auto-detects running dev servers to eliminate hardcoded URLs
- Test scripts written to `/tmp` for automatic cleanup (no clutter)
- Code executes reliably with proper module resolution via `run.js`
- Progressive disclosure - API_REFERENCE.md loaded only when advanced features needed
## When to Use
This skill is applicable to execute the workflow or actions described in the overview.
lib/helpers.js
// playwright-helpers.js
// Reusable utility functions for Playwright automation
const { chromium, firefox, webkit } = require('playwright');
/**
* Parse extra HTTP headers from environment variables.
* Supports two formats:
* - PW_HEADER_NAME + PW_HEADER_VALUE: Single header (simple, common case)
* - PW_EXTRA_HEADERS: JSON object for multiple headers (advanced)
* Single header format takes precedence if both are set.
* @returns {Object|null} Headers object or null if none configured
*/
function getExtraHeadersFromEnv() {
const headerName = process.env.PW_HEADER_NAME;
const headerValue = process.env.PW_HEADER_VALUE;
if (headerName && headerValue) {
return { [headerName]: headerValue };
}
const headersJson = process.env.PW_EXTRA_HEADERS;
if (headersJson) {
try {
const parsed = JSON.parse(headersJson);
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
return parsed;
}
console.warn('PW_EXTRA_HEADERS must be a JSON object, ignoring...');
} catch (e) {
console.warn('Failed to parse PW_EXTRA_HEADERS as JSON:', e.message);
}
}
return null;
}
/**
* Launch browser with standard configuration
* @param {string} browserType - 'chromium', 'firefox', or 'webkit'
* @param {Object} options - Additional launch options
*/
async function launchBrowser(browserType = 'chromium', options = {}) {
const defaultOptions = {
headless: process.env.HEADLESS !== 'false',
slowMo: process.env.SLOW_MO ? parseInt(process.env.SLOW_MO) : 0,
args: ['--no-sandbox', '--disable-setuid-sandbox']
};
const browsers = { chromium, firefox, webkit };
const browser = browsers[browserType];
if (!browser) {
throw new Error(`Invalid browser type: ${browserType}`);
}
return await browser.launch({ ...defaultOptions, ...options });
}
/**
* Create a new page with viewport and user agent
* @param {Object} context - Browser context
* @param {Object} options - Page options
*/
async function createPage(context, options = {}) {
const page = await context.newPage();
if (options.viewport) {
await page.setViewportSize(options.viewport);
}
if (options.userAgent) {
await page.setExtraHTTPHeaders({
'User-Agent': options.userAgent
});
}
// Set default timeout
page.setDefaultTimeout(options.timeout || 30000);
return page;
}
/**
* Smart wait for page to be ready
* @param {Object} page - Playwright page
* @param {Object} options - Wait options
*/
async function waitForPageReady(page, options = {}) {
const waitOptions = {
waitUntil: options.waitUntil || 'networkidle',
timeout: options.timeout || 30000
};
try {
await page.waitForLoadState(waitOptions.waitUntil, {
timeout: waitOptions.timeout
});
} catch (e) {
console.warn('Page load timeout, continuing...');
}
// Additional wait for dynamic content if selector provided
if (options.waitForSelector) {
await page.waitForSelector(options.waitForSelector, {
timeout: options.timeout
});
}
}
/**
* Safe click with retry logic
* @param {Object} page - Playwright page
* @param {string} selector - Element selector
* @param {Object} options - Click options
*/
async function safeClick(page, selector, options = {}) {
const maxRetries = options.retries || 3;
const retryDelay = options.retryDelay || 1000;
for (let i = 0; i < maxRetries; i++) {
try {
await page.waitForSelector(selector, {
state: 'visible',
timeout: options.timeout || 5000
});
await page.click(selector, {
force: options.force || false,
timeout: options.timeout || 5000
});
return true;
} catch (e) {
if (i === maxRetries - 1) {
console.error(`Failed to click ${selector} after ${maxRetries} attempts`);
throw e;
}
console.log(`Retry ${i + 1}/${maxRetries} for clicking ${selector}`);
await page.waitForTimeout(retryDelay);
}
}
}
/**
* Safe text input with clear before type
* @param {Object} page - Playwright page
* @param {string} selector - Input selector
* @param {string} text - Text to type
* @param {Object} options - Type options
*/
async function safeType(page, selector, text, options = {}) {
await page.waitForSelector(selector, {
state: 'visible',
timeout: options.timeout || 10000
});
if (options.clear !== false) {
await page.fill(selector, '');
}
if (options.slow) {
await page.type(selector, text, { delay: options.delay || 100 });
} else {
await page.fill(selector, text);
}
}
/**
* Extract text from multiple elements
* @param {Object} page - Playwright page
* @param {string} selector - Elements selector
*/
async function extractTexts(page, selector) {
await page.waitForSelector(selector, { timeout: 10000 });
return await page.$$eval(selector, elements =>
elements.map(el => el.textContent?.trim()).filter(Boolean)
);
}
/**
* Take screenshot with timestamp
* @param {Object} page - Playwright page
* @param {string} name - Screenshot name
* @param {Object} options - Screenshot options
*/
async function takeScreenshot(page, name, options = {}) {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const filename = `${name}-${timestamp}.png`;
await page.screenshot({
path: filename,
fullPage: options.fullPage !== false,
...options
});
console.log(`Screenshot saved: ${filename}`);
return filename;
}
/**
* Handle authentication
* @param {Object} page - Playwright page
* @param {Object} credentials - Username and password
* @param {Object} selectors - Login form selectors
*/
async function authenticate(page, credentials, selectors = {}) {
const defaultSelectors = {
username: 'input[name="username"], input[name="email"], #username, #email',
password: 'input[name="password"], #password',
submit: 'button[type="submit"], input[type="submit"], button:has-text("Login"), button:has-text("Sign in")'
};
const finalSelectors = { ...defaultSelectors, ...selectors };
await safeType(page, finalSelectors.username, credentials.username);
await safeType(page, finalSelectors.password, credentials.password);
await safeClick(page, finalSelectors.submit);
// Wait for navigation or success indicator
await Promise.race([
page.waitForNavigation({ waitUntil: 'networkidle' }),
page.waitForSelector(selectors.successIndicator || '.dashboard, .user-menu, .logout', { timeout: 10000 })
]).catch(() => {
console.log('Login might have completed without navigation');
});
}
/**
* Scroll page
* @param {Object} page - Playwright page
* @param {string} direction - 'down', 'up', 'top', 'bottom'
* @param {number} distance - Pixels to scroll (for up/down)
*/
async function scrollPage(page, direction = 'down', distance = 500) {
switch (direction) {
case 'down':
await page.evaluate(d => window.scrollBy(0, d), distance);
break;
case 'up':
await page.evaluate(d => window.scrollBy(0, -d), distance);
break;
case 'top':
await page.evaluate(() => window.scrollTo(0, 0));
break;
case 'bottom':
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
break;
}
await page.waitForTimeout(500); // Wait for scroll animation
}
/**
* Extract table data
* @param {Object} page - Playwright page
* @param {string} tableSelector - Table selector
*/
async function extractTableData(page, tableSelector) {
await page.waitForSelector(tableSelector);
return await page.evaluate((selector) => {
const table = document.querySelector(selector);
if (!table) return null;
const headers = Array.from(table.querySelectorAll('thead th')).map(th =>
th.textContent?.trim()
);
const rows = Array.from(table.querySelectorAll('tbody tr')).map(tr => {
const cells = Array.from(tr.querySelectorAll('td'));
if (headers.length > 0) {
return cells.reduce((obj, cell, index) => {
obj[headers[index] || `column_${index}`] = cell.textContent?.trim();
return obj;
}, {});
} else {
return cells.map(cell => cell.textContent?.trim());
}
});
return { headers, rows };
}, tableSelector);
}
/**
* Wait for and dismiss cookie banners
* @param {Object} page - Playwright page
* @param {number} timeout - Max time to wait
*/
async function handleCookieBanner(page, timeout = 3000) {
const commonSelectors = [
'button:has-text("Accept")',
'button:has-text("Accept all")',
'button:has-text("OK")',
'button:has-text("Got it")',
'button:has-text("I agree")',
'.cookie-accept',
'#cookie-accept',
'[data-testid="cookie-accept"]'
];
for (const selector of commonSelectors) {
try {
const element = await page.waitForSelector(selector, {
timeout: timeout / commonSelectors.length,
state: 'visible'
});
if (element) {
await element.click();
console.log('Cookie banner dismissed');
return true;
}
} catch (e) {
// Continue to next selector
}
}
return false;
}
/**
* Retry a function with exponential backoff
* @param {Function} fn - Function to retry
* @param {number} maxRetries - Maximum retry attempts
* @param {number} initialDelay - Initial delay in ms
*/
async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
const delay = initialDelay * Math.pow(2, i);
console.log(`Attempt ${i + 1} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
/**
* Create browser context with common settings
* @param {Object} browser - Browser instance
* @param {Object} options - Context options
*/
async function createContext(browser, options = {}) {
const envHeaders = getExtraHeadersFromEnv();
// Merge environment headers with any passed in options
const mergedHeaders = {
...envHeaders,
...options.extraHTTPHeaders
};
const defaultOptions = {
viewport: { width: 1280, height: 720 },
userAgent: options.mobile
? 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1'
: undefined,
permissions: options.permissions || [],
geolocation: options.geolocation,
locale: options.locale || 'en-US',
timezoneId: options.timezoneId || 'America/New_York',
// Only include extraHTTPHeaders if we have any
...(Object.keys(mergedHeaders).length > 0 && { extraHTTPHeaders: mergedHeaders })
};
return await browser.newContext({ ...defaultOptions, ...options });
}
/**
* Detect running dev servers on common ports
* @param {Array<number>} customPorts - Additional ports to check
* @returns {Promise<Array>} Array of detected server URLs
*/
async function detectDevServers(customPorts = []) {
const http = require('http');
// Common dev server ports
const commonPorts = [3000, 3001, 3002, 5173, 8080, 8000, 4200, 5000, 9000, 1234];
const allPorts = [...new Set([...commonPorts, ...customPorts])];
const detectedServers = [];
console.log('🔍 Checking for running dev servers...');
for (const port of allPorts) {
try {
await new Promise((resolve, reject) => {
const req = http.request({
hostname: 'localhost',
port: port,
path: '/',
method: 'HEAD',
timeout: 500
}, (res) => {
if (res.statusCode < 500) {
detectedServers.push(`http://localhost:${port}`);
console.log(` ✅ Found server on port ${port}`);
}
resolve();
});
req.on('error', () => resolve());
req.on('timeout', () => {
req.destroy();
resolve();
});
req.end();
});
} catch (e) {
// Port not available, continue
}
}
if (detectedServers.length === 0) {
console.log(' ❌ No dev servers detected');
}
return detectedServers;
}
module.exports = {
launchBrowser,
createPage,
waitForPageReady,
safeClick,
safeType,
extractTexts,
takeScreenshot,
authenticate,
scrollPage,
extractTableData,
handleCookieBanner,
retryWithBackoff,
createContext,
detectDevServers,
getExtraHeadersFromEnv
};
package.json
{
"name": "playwright-skill",
"version": "4.1.0",
"description": "General-purpose browser automation with Playwright for Claude Code with auto-detection and smart test management",
"author": "lackeyjb",
"main": "run.js",
"scripts": {
"setup": "npm install && npx playwright install chromium",
"install-all-browsers": "npx playwright install chromium firefox webkit"
},
"keywords": [
"playwright",
"automation",
"browser-testing",
"web-automation",
"claude-skill",
"general-purpose"
],
"dependencies": {
"playwright": "^1.57.0"
},
"engines": {
"node": ">=14.0.0"
},
"license": "MIT"
}
run.js
#!/usr/bin/env node
/**
* Universal Playwright Executor for Claude Code
*
* Executes Playwright automation code from:
* - File path: node run.js script.js
* - Inline code: node run.js 'await page.goto("...")'
* - Stdin: cat script.js | node run.js
*
* Ensures proper module resolution by running from skill directory.
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Change to skill directory for proper module resolution
process.chdir(__dirname);
/**
* Check if Playwright is installed
*/
function checkPlaywrightInstalled() {
try {
require.resolve('playwright');
return true;
} catch (e) {
return false;
}
}
/**
* Install Playwright if missing
*/
function installPlaywright() {
console.log('📦 Playwright not found. Installing...');
try {
execSync('npm install', { stdio: 'inherit', cwd: __dirname });
execSync('npx playwright install chromium', { stdio: 'inherit', cwd: __dirname });
console.log('✅ Playwright installed successfully');
return true;
} catch (e) {
console.error('❌ Failed to install Playwright:', e.message);
console.error('Please run manually: cd', __dirname, '&& npm run setup');
return false;
}
}
/**
* Get code to execute from various sources
*/
function getCodeToExecute() {
const args = process.argv.slice(2);
// Case 1: File path provided
if (args.length > 0 && fs.existsSync(args[0])) {
const filePath = path.resolve(args[0]);
console.log(`📄 Executing file: ${filePath}`);
return fs.readFileSync(filePath, 'utf8');
}
// Case 2: Inline code provided as argument
if (args.length > 0) {
console.log('⚡ Executing inline code');
return args.join(' ');
}
// Case 3: Code from stdin
if (!process.stdin.isTTY) {
console.log('📥 Reading from stdin');
return fs.readFileSync(0, 'utf8');
}
// No input
console.error('❌ No code to execute');
console.error('Usage:');
console.error(' node run.js script.js # Execute file');
console.error(' node run.js "code here" # Execute inline');
console.error(' cat script.js | node run.js # Execute from stdin');
process.exit(1);
}
/**
* Clean up old temporary execution files from previous runs
*/
function cleanupOldTempFiles() {
try {
const files = fs.readdirSync(__dirname);
const tempFiles = files.filter(f => f.startsWith('.temp-execution-') && f.endsWith('.js'));
if (tempFiles.length > 0) {
tempFiles.forEach(file => {
const filePath = path.join(__dirname, file);
try {
fs.unlinkSync(filePath);
} catch (e) {
// Ignore errors - file might be in use or already deleted
}
});
}
} catch (e) {
// Ignore directory read errors
}
}
/**
* Wrap code in async IIFE if not already wrapped
*/
function wrapCodeIfNeeded(code) {
// Check if code already has require() and async structure
const hasRequire = code.includes('require(');
const hasAsyncIIFE = code.includes('(async () => {') || code.includes('(async()=>{');
// If it's already a complete script, return as-is
if (hasRequire && hasAsyncIIFE) {
return code;
}
// If it's just Playwright commands, wrap in full template
if (!hasRequire) {
return `
const { chromium, firefox, webkit, devices } = require('playwright');
const helpers = require('./lib/helpers');
// Extra headers from environment variables (if configured)
const __extraHeaders = helpers.getExtraHeadersFromEnv();
/**
* Utility to merge environment headers into context options.
* Use when creating contexts with raw Playwright API instead of helpers.createContext().
* @param {Object} options - Context options
* @returns {Object} Options with extraHTTPHeaders merged in
*/
function getContextOptionsWithHeaders(options = {}) {
if (!__extraHeaders) return options;
return {
...options,
extraHTTPHeaders: {
...__extraHeaders,
...(options.extraHTTPHeaders || {})
}
};
}
(async () => {
try {
${code}
} catch (error) {
console.error('❌ Automation error:', error.message);
if (error.stack) {
console.error(error.stack);
}
process.exit(1);
}
})();
`;
}
// If has require but no async wrapper
if (!hasAsyncIIFE) {
return `
(async () => {
try {
${code}
} catch (error) {
console.error('❌ Automation error:', error.message);
if (error.stack) {
console.error(error.stack);
}
process.exit(1);
}
})();
`;
}
return code;
}
/**
* Main execution
*/
async function main() {
console.log('🎭 Playwright Skill - Universal Executor\n');
// Clean up old temp files from previous runs
cleanupOldTempFiles();
// Check Playwright installation
if (!checkPlaywrightInstalled()) {
const installed = installPlaywright();
if (!installed) {
process.exit(1);
}
}
// Get code to execute
const rawCode = getCodeToExecute();
const code = wrapCodeIfNeeded(rawCode);
// Create temporary file for execution
const tempFile = path.join(__dirname, `.temp-execution-${Date.now()}.js`);
try {
// Write code to temp file
fs.writeFileSync(tempFile, code, 'utf8');
// Execute the code
console.log('🚀 Starting automation...\n');
require(tempFile);
// Note: Temp file will be cleaned up on next run
// This allows long-running async operations to complete safely
} catch (error) {
console.error('❌ Execution failed:', error.message);
if (error.stack) {
console.error('\n📋 Stack trace:');
console.error(error.stack);
}
process.exit(1);
}
}
// Run main function
main().catch(error => {
console.error('❌ Fatal error:', error.message);
process.exit(1);
});