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:
Auto-detect dev servers - For localhost testing, ALWAYS run server detection FIRST:
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
Write scripts to /tmp - NEVER write test files to skill directory; always use /tmp/playwright-test-*.js
Use visible browser by default - Always use headless: false unless user specifically requests headless mode
Parameterize URLs - Always make URLs configurable via environment variable or constant at top of script
How It Works
-
You describe what you want to test/automate
-
I auto-detect running dev servers (or ask for URL if testing external site)
-
I write custom Playwright code in /tmp/playwright-test-*.js (won't clutter your project)
-
I execute it via: cd $SKILL_DIR && node run.js /tmp/playwright-test-*.js
-
Results displayed in real-time, browser window visible for debugging
-
Test files auto-cleaned from /tmp by your OS
Setup (First Time)
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)
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
// /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
cd $SKILL_DIR && node run.js /tmp/playwright-test-page.js
Common Patterns
Test a Page (Multiple Viewports)
// /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
// /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
// /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
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
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
// /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:
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 :
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):
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):
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() :
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() :
const context = await browser.newContext( getContextOptionsWithHeaders({ viewport: { width: 1920, height: 1080 } }) );
Advanced Usage
For comprehensive Playwright API documentation, see 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:
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?
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