Agent skill
qa-testing-playwright
End-to-end web application testing with Playwright: scope control, stable selectors, parallelization/sharding, flake control, network mocking vs real services, visual testing tradeoffs, and CI/CD integration.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/qa-testing-playwright
SKILL.md
QA Testing (Playwright, Dec 2025) — Quick Reference
This skill enables high-signal, cost-aware E2E testing of web applications using Playwright.
Core references: Playwright best practices (https://playwright.dev/docs/best-practices), locators (https://playwright.dev/docs/locators), retries (https://playwright.dev/docs/test-retries), sharding (https://playwright.dev/docs/test-sharding), and the trace viewer (https://playwright.dev/docs/trace-viewer).
Core QA (Default)
Scope Control (What E2E Is For)
- E2E exists to protect critical user journeys and high-risk integrations.
- Keep E2E thin; push most coverage down to unit/integration/contract tests.
- Test the contract and user intent, not CSS structure.
Selector Strategy (Stability First)
- Prefer user-facing locators:
getByRole,getByLabel,getByText(Playwright locators: https://playwright.dev/docs/locators). - Use
data-testidas a last resort for complex widgets or non-semantic UI. - Avoid XPath and brittle CSS selectors.
Flake Control (Make CI Reliable)
- Avoid sleeps; rely on Playwright auto-wait and web-first assertions (https://playwright.dev/docs/best-practices).
- Use retries as a signal, not a crutch; investigate rerun-pass tests as flakes (https://playwright.dev/docs/test-retries).
- Always capture traces/screenshots/video on failure; use trace viewer to localize root cause (https://playwright.dev/docs/trace-viewer).
- If clicks are intercepted by overlays, prefer keyboard activation or close the overlay rather than
force: true.
CI Economics (Parallelize and Shard)
- Run smoke E2E on PRs; run full regression on schedule or per-release [Inference].
- Shard long suites across machines to keep PR feedback fast (https://playwright.dev/docs/test-sharding).
Do / Avoid
Do:
- Make tests independent and deterministic (isolated state, stable data).
- Use network mocking for third-party dependencies; test your integration contract, not their UI.
Avoid:
- “Test everything E2E” as a default.
- Weakening assertions to “fix” flakes (silent regressions).
Quick Reference
| Task | Command | When to Use |
|---|---|---|
| Init Playwright | npm init playwright@latest |
New project setup |
| Run all tests | npx playwright test |
Full test suite |
| Run with UI | npx playwright test --ui |
Debugging, visual mode |
| Run specific test | npx playwright test login.spec.ts |
Targeted testing |
| Generate code | npx playwright codegen |
Record interactions |
| Show report | npx playwright show-report |
View test results |
When to Use This Skill
Claude should invoke this skill when a user requests:
- Write E2E tests for web application
- Test user authentication flows
- Verify form submissions and validation
- Test responsive designs across viewports
- Automate browser interactions
- Set up Playwright in CI/CD
- Debug failing E2E tests
Quick Start
Installation
# Initialize Playwright
npm init playwright@latest
# Or add to existing project
npm install -D @playwright/test
npx playwright install
Project Structure
project/
├── playwright.config.ts
├── tests/
│ ├── auth.spec.ts
│ ├── checkout.spec.ts
│ └── fixtures/
│ └── auth.fixture.ts
├── pages/
│ ├── LoginPage.ts
│ └── DashboardPage.ts
└── .github/workflows/
└── playwright.yml
Core Testing Patterns
Browser Policy (Bundled vs Stable Channels)
- Default: bundled browsers are designed for reliable automation.
- When you must test against stable Chrome/Edge (policy/compliance, media codecs), use browser channels (see Playwright “Browsers”: https://playwright.dev/docs/browsers).
Locator Priority (2025 Best Practice)
Use role locators as the primary approach — they test from the user's perspective:
// 1. Role locators (preferred)
await page.getByRole('textbox', { name: 'Email' }).fill('[email protected]');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
// 2. Label/text locators
await page.getByLabel('Email').fill('[email protected]');
await page.getByText('Sign in').click();
// 3. Test IDs (fallback for complex scenarios)
await page.getByTestId('user-avatar').click();
Common UI Flake Patterns
- Overlay intercepts pointer events: use keyboard activation to follow the accessible path.
- Menus: open via
buttonrole, selectmenuitemrole to avoid hidden link clicks.
const menuButton = page.getByRole('button', { name: /open menu/i });
await menuButton.focus();
await menuButton.press('Enter');
const menuItem = page.getByRole('menuitem', { name: /year ahead/i });
await menuItem.focus();
await menuItem.press('Enter');
Basic Test Structure
import { test, expect } from '@playwright/test';
test.describe('User Authentication', () => {
test('should login with valid credentials', async ({ page }) => {
await page.goto('/login');
await page.getByRole('textbox', { name: 'Email' }).fill('[email protected]');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
test('should show error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.getByRole('textbox', { name: 'Email' }).fill('[email protected]');
await page.getByLabel('Password').fill('wrongpassword');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('alert')).toContainText('Invalid');
});
});
Page Object Model
// pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByRole('textbox', { name: 'Email' });
this.passwordInput = page.getByLabel('Password');
this.loginButton = page.getByRole('button', { name: 'Sign in' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async expectError(message: string) {
await expect(this.errorMessage).toBeVisible();
await expect(this.errorMessage).toContainText(message);
}
}
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('login with valid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('[email protected]', 'password123');
await expect(page).toHaveURL('/dashboard');
});
Authentication Fixture
// fixtures/auth.fixture.ts
import { test as base, expect } from '@playwright/test';
type AuthFixtures = {
authenticatedPage: Page;
};
export const test = base.extend<AuthFixtures>({
authenticatedPage: async ({ page }, use) => {
// Login before test
await page.goto('/login');
await page.getByRole('textbox', { name: 'Email' }).fill('[email protected]');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL('/dashboard');
// Use authenticated page in test
await use(page);
},
});
// tests/dashboard.spec.ts
import { test } from '../fixtures/auth.fixture';
import { expect } from '@playwright/test';
test('authenticated user can access dashboard', async ({ authenticatedPage }) => {
await authenticatedPage.goto('/dashboard');
await expect(authenticatedPage.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
Common Scenarios
Form Testing
test('should validate form fields', async ({ page }) => {
await page.goto('/signup');
// Submit empty form
await page.click('[data-testid="submit"]');
// Check validation messages
await expect(page.locator('[data-testid="email-error"]')).toHaveText('Email is required');
await expect(page.locator('[data-testid="password-error"]')).toHaveText('Password is required');
// Fill with invalid email
await page.fill('[data-testid="email"]', 'invalid-email');
await page.click('[data-testid="submit"]');
await expect(page.locator('[data-testid="email-error"]')).toHaveText('Invalid email format');
});
API Mocking
test('should handle API errors gracefully', async ({ page }) => {
// Mock API to return error
await page.route('**/api/users', route => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Server error' }),
});
});
await page.goto('/users');
await expect(page.locator('[data-testid="error-banner"]')).toContainText('Failed to load');
});
test('should display user data from API', async ({ page }) => {
// Mock successful response
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
body: JSON.stringify([
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' },
]),
});
});
await page.goto('/users');
await expect(page.locator('[data-testid="user-list"] li')).toHaveCount(2);
});
Responsive Testing
import { devices } from '@playwright/test';
test.describe('Responsive Design', () => {
test('mobile navigation', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 }); // iPhone SE
await page.goto('/');
// Mobile menu should be hidden
await expect(page.locator('[data-testid="desktop-nav"]')).toBeHidden();
// Hamburger menu should be visible
await expect(page.locator('[data-testid="mobile-menu-button"]')).toBeVisible();
// Open mobile menu
await page.click('[data-testid="mobile-menu-button"]');
await expect(page.locator('[data-testid="mobile-nav"]')).toBeVisible();
});
});
// playwright.config.ts - Multiple devices
export default defineConfig({
projects: [
{ name: 'Desktop Chrome', use: { ...devices['Desktop Chrome'] } },
{ name: 'Mobile Safari', use: { ...devices['iPhone 12'] } },
{ name: 'Tablet', use: { ...devices['iPad Pro'] } },
],
});
Visual Regression
test('homepage visual regression', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 100,
});
});
test('button states', async ({ page }) => {
await page.goto('/components');
const button = page.locator('[data-testid="primary-button"]');
// Default state
await expect(button).toHaveScreenshot('button-default.png');
// Hover state
await button.hover();
await expect(button).toHaveScreenshot('button-hover.png');
// Focus state
await button.focus();
await expect(button).toHaveScreenshot('button-focus.png');
});
Configuration
playwright.config.ts
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'],
['junit', { outputFile: 'results.xml' }],
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
CI/CD Integration
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Debugging
# Run with UI mode
npx playwright test --ui
# Run in headed mode
npx playwright test --headed
# Run in debug mode (step through)
npx playwright test --debug
# Generate test code
npx playwright codegen http://localhost:3000
# Show trace viewer
npx playwright show-trace trace.zip
Optional: AI / Automation
Do:
- Use AI to scaffold tests and page objects, then enforce selector rules, remove sleeps, and add explicit oracles.
- Use AI to summarize failing traces/logs, but base fixes on evidence and stable assertions.
Avoid:
- Auto-healing by weakening assertions or switching to brittle selectors.
- Generating tests that lock onto CSS structure instead of user-facing roles/labels.
Navigation
Resources
- resources/playwright-patterns.md — Advanced testing patterns
- resources/playwright-ci.md — CI/CD configurations
- data/sources.json — Playwright documentation links
Templates
- templates/template-playwright-e2e-review-checklist.md — E2E review checklist (selectors, parallelization, flake rules)
Related Skills
- ../qa-testing-strategy/SKILL.md — General testing strategies
- ../software-frontend/SKILL.md — Frontend development
- ../ops-devops-platform/SKILL.md — CI/CD integration
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?