Browser automation and E2E testing with Playwright. Auto-detects dev servers, writes clean test scripts. Test pages, fill forms, take screenshots, check responsive design, validate UX, test login flows, check links, automate any browser task. Use for cross-browser testing, visual regression, API testing, component testing in TypeScript/JavaScript and Python projects.
/plugin marketplace add secondsky/claude-skills/plugin install secondsky-playwright-plugins-playwright@secondsky/claude-skillsThis skill is limited to using the following tools:
bun.lockexamples/form-test.jsexamples/link-checker.jsexamples/responsive-test.jslib/helpers.jspackage.jsonreferences/API_REFERENCE.mdrun.jsExpert knowledge for browser automation and end-to-end testing with Playwright - a modern cross-browser testing framework.
IMPORTANT - Path Resolution:
This skill can be installed in different locations. Before executing commands, determine the skill directory based on where you loaded this SKILL.md file, and use that path in all commands. Replace $SKILL_DIR with the actual discovered path.
Common installation paths:
~/.claude/plugins/*/playwright/skills/playwright~/.claude/skills/playwright<project>/.claude/skills/playwrightWhen automating browser tasks:
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)))"
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 constant at top of script
Execute via run.js - Always run: cd $SKILL_DIR && node run.js /tmp/playwright-test-*.js
# Navigate to skill directory
cd $SKILL_DIR
# Install using bun (preferred)
bun run setup
# Or using npm
npm run setup:npm
This installs Playwright and Chromium browser. Only needed once.
# Using Bun (preferred)
bun add -d @playwright/test
bunx playwright install
# Using npm
npm init playwright@latest
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests',
fullyParallel: true,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
webServer: {
command: 'bun run dev',
url: 'http://localhost:3000',
},
})
/tmp/playwright-test-*.jscd $SKILL_DIR && node run.js /tmp/playwright-test-*.js// /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();
})();
Execute: cd $SKILL_DIR && node run.js /tmp/playwright-test-responsive.js
// /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"]');
await page.waitForURL('**/dashboard');
console.log('✅ Login successful, redirected to dashboard');
await browser.close();
})();
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();
})();
# Run all tests
bunx playwright test
# Headed mode (see browser)
bunx playwright test --headed
# Specific file
bunx playwright test tests/login.spec.ts
# Debug mode
bunx playwright test --debug
# UI mode (interactive)
bunx playwright test --ui
# Specific browser
bunx playwright test --project=chromium
# Generate report
bunx playwright show-report
import { test, expect } from '@playwright/test'
test.describe('Login flow', () => {
test('successful login', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: 'Login' }).click()
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('password123')
await page.getByRole('button', { name: 'Sign in' }).click()
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible()
})
test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('wrong@example.com')
await page.getByLabel('Password').fill('wrongpassword')
await page.getByRole('button', { name: 'Sign in' }).click()
await expect(page.getByText('Invalid credentials')).toBeVisible()
})
})
// ✅ Role-based (recommended)
await page.getByRole('button', { name: 'Submit' })
await page.getByRole('link', { name: 'Home' })
// ✅ Text/Label
await page.getByText('Hello World')
await page.getByLabel('Email')
// ✅ Test ID (fallback)
await page.getByTestId('submit-button')
// ❌ Avoid CSS selectors (brittle)
await page.locator('.btn-primary')
// Visibility
await expect(page.getByText('Success')).toBeVisible()
await expect(page.getByRole('button')).toBeEnabled()
// Text
await expect(page.getByRole('heading')).toHaveText('Welcome')
await expect(page.getByRole('alert')).toContainText('error')
// Attributes
await expect(page.getByRole('link')).toHaveAttribute('href', '/home')
// URL/Title
await expect(page).toHaveURL('/dashboard')
await expect(page).toHaveTitle('Dashboard')
// Count
await expect(page.getByRole('listitem')).toHaveCount(5)
// Clicking
await page.getByRole('button').click()
await page.getByText('File').dblclick()
// Typing
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Search').press('Enter')
// Selecting
await page.getByLabel('Country').selectOption('us')
// File Upload
await page.getByLabel('Upload').setInputFiles('path/to/file.pdf')
test('mocks API response', async ({ page }) => {
await page.route('**/api/users', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, name: 'Test User' }]),
})
})
await page.goto('/users')
await expect(page.getByText('Test User')).toBeVisible()
})
test('captures screenshot', async ({ page }) => {
await page.goto('/')
await page.screenshot({ path: 'screenshot.png', fullPage: true })
await expect(page).toHaveScreenshot('homepage.png')
})
// Save state after login
setup('authenticate', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('password123')
await page.getByRole('button', { name: 'Sign in' }).click()
await page.context().storageState({ path: 'auth.json' })
})
// Reuse in config
use: { storageState: 'auth.json' }
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test'
export class LoginPage {
readonly emailInput: Locator
readonly passwordInput: Locator
readonly submitButton: Locator
constructor(page: Page) {
this.emailInput = page.getByLabel('Email')
this.passwordInput = page.getByLabel('Password')
this.submitButton = page.getByRole('button', { name: 'Sign in' })
}
async login(email: string, password: string) {
await this.emailInput.fill(email)
await this.passwordInput.fill(password)
await this.submitButton.click()
}
}
// Usage
const loginPage = new LoginPage(page)
await loginPage.login('user@example.com', 'password123')
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');
// Create context with custom headers
const context = await helpers.createContext(browser);
Configure custom headers for all HTTP requests via environment variables:
# 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
Headers are automatically applied when using helpers.createContext().
For quick one-off tasks:
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:
detectDevServers() before writing test code/tmp/playwright-test-*.js, never to skill directoryTARGET_URL constantheadless: false unless explicitly requestedpage.route()slowMo: 100 to make actions visiblewaitForURL, waitForSelector, waitForLoadState instead of fixed timeoutsconsole.log() to track progressPlaywright not installed:
cd $SKILL_DIR && bun 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 })
vitest-testing - Unit and integration testingapi-testing - HTTP API testingtest-quality-analysis - Test quality patternsLoad references/API_REFERENCE.md when you need:
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.