| name | qa-testing-playwright |
| description | 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. |
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-testid as a last resort for complex widgets or non-semantic UI.
- Avoid XPath and brittle CSS selectors.
Flake Control (Make CI Reliable)
CI Economics (Parallelize and Shard)
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
npm init playwright@latest
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:
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
await page.getByLabel('Email').fill('user@example.com');
await page.getByText('Sign in').click();
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
button role, select menuitem role 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('user@example.com');
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(, { : }).();
page.().();
page.(, { : }).();
(page.()).();
});
});
Page Object Model
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() {
..();
}
() {
..(email);
..(password);
..();
}
() {
(.).();
(.).(message);
}
}
{ test, expect } ;
{ } ;
(, ({ page }) => {
loginPage = (page);
loginPage.();
loginPage.(, );
(page).();
});
Authentication Fixture
import { test as base, expect } from '@playwright/test';
type AuthFixtures = {
authenticatedPage: Page;
};
export const test = base.extend<AuthFixtures>({
authenticatedPage: async ({ page }, use) => {
await page.goto('/login');
await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL('/dashboard');
await use(page);
},
});
import { test } from '../fixtures/auth.fixture';
import { expect } from ;
(, ({ authenticatedPage }) => {
authenticatedPage.();
(authenticatedPage.(, { : })).();
});
Common Scenarios
Form Testing
test('should validate form fields', async ({ page }) => {
await page.goto('/signup');
await page.click('[data-testid="submit"]');
await expect(page.locator('[data-testid="email-error"]')).toHaveText('Email is required');
await expect(page.locator('[data-testid="password-error"]')).toHaveText('Password is required');
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 }) => {
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 }) => {
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.();
(page.()).();
});
Responsive Testing
import { devices } from '@playwright/test';
test.describe('Responsive Design', () => {
test('mobile navigation', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto('/');
await expect(page.locator('[data-testid="desktop-nav"]')).toBeHidden();
await expect(page.locator('[data-testid="mobile-menu-button"]')).toBeVisible();
await page.click('[data-testid="mobile-menu-button"]');
await expect(page.locator('[data-testid="mobile-nav"]')).toBeVisible();
});
});
export default defineConfig({
projects: [
{ name: 'Desktop Chrome', use: { ...devices[] } },
{ : , : { ...devices[] } },
{ : , : { ...devices[] } },
],
});
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"]');
await expect(button).toHaveScreenshot('button-default.png');
await button.hover();
await expect(button).toHaveScreenshot('button-hover.png');
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: ,
: ,
: !process..,
},
});
CI/CD Integration
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()
Debugging
npx playwright test --ui
npx playwright test --headed
npx playwright test --debug
npx playwright codegen http://localhost:3000
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
Templates
Related Skills