playwright-e2e-testing

Playwright E2E Testing Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "playwright-e2e-testing" with this command: npx skills add bobmatnyc/claude-mpm-skills/bobmatnyc-claude-mpm-skills-playwright-e2e-testing

Playwright E2E Testing Skill

progressive_disclosure: entry_point: summary: "Modern E2E testing framework with cross-browser automation and built-in test runner" when_to_use:

  • "When testing web applications end-to-end"
  • "When needing cross-browser testing"
  • "When testing user flows and interactions"
  • "When needing screenshot/video recording" quick_start:
  • "npm init playwright@latest"
  • "Choose TypeScript and test location"
  • "npx playwright test"
  • "npx playwright show-report" token_estimate: entry: 75-90 full: 4200-5200

Overview

Playwright is a modern end-to-end testing framework that provides cross-browser automation with a built-in test runner, auto-wait mechanisms, and excellent developer experience.

Key Features

  • Auto-wait: Automatically waits for elements to be ready

  • Cross-browser: Chromium, Firefox, WebKit support

  • Built-in runner: Parallel execution, retries, reporters

  • Network control: Mock and intercept network requests

  • Debugging: UI mode, trace viewer, inspector

Installation

Initialize new Playwright project

npm init playwright@latest

Or add to existing project

npm install -D @playwright/test

Install browsers

npx playwright install

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',

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'] }, }, { name: 'Mobile Chrome', use: { ...devices['Pixel 5'] }, }, ],

webServer: { command: 'npm run start', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, }, });

Fundamentals

Basic Test Structure

import { test, expect } from '@playwright/test';

test('basic test', async ({ page }) => { await page.goto('https://example.com');

// Wait for element and check visibility const title = page.locator('h1'); await expect(title).toBeVisible(); await expect(title).toHaveText('Example Domain');

// Get page title await expect(page).toHaveTitle(/Example/); });

test.describe('User authentication', () => { test('should login successfully', async ({ page }) => { await page.goto('/login'); await page.fill('[name="username"]', 'testuser'); await page.fill('[name="password"]', 'password123'); await page.click('button[type="submit"]');

await expect(page).toHaveURL('/dashboard');
await expect(page.locator('.welcome-message')).toContainText('Welcome');

});

test('should show error for invalid credentials', async ({ page }) => { await page.goto('/login'); await page.fill('[name="username"]', 'invalid'); await page.fill('[name="password"]', 'wrong'); await page.click('button[type="submit"]');

await expect(page.locator('.error-message')).toBeVisible();
await expect(page.locator('.error-message')).toHaveText('Invalid credentials');

}); });

Test Hooks

import { test, expect } from '@playwright/test';

test.describe('Dashboard tests', () => { test.beforeEach(async ({ page }) => { // Run before each test await page.goto('/dashboard'); await page.waitForLoadState('networkidle'); });

test.afterEach(async ({ page }) => { // Cleanup after each test await page.close(); });

test.beforeAll(async ({ browser }) => { // Run once before all tests in describe block console.log('Starting test suite'); });

test.afterAll(async ({ browser }) => { // Run once after all tests console.log('Test suite complete'); });

test('displays user data', async ({ page }) => { await expect(page.locator('.user-name')).toBeVisible(); }); });

Locator Strategies

Best Practice: Role-based Locators

import { test, expect } from '@playwright/test';

test('accessible locators', async ({ page }) => { await page.goto('/form');

// By role (BEST - accessible and stable) await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com'); await page.getByRole('checkbox', { name: 'Subscribe' }).check(); await page.getByRole('link', { name: 'Learn more' }).click();

// By label (good for forms) await page.getByLabel('Password').fill('secret123');

// By placeholder await page.getByPlaceholder('Search...').fill('query');

// By text await page.getByText('Welcome back').click(); await page.getByText(/hello/i).isVisible();

// By test ID (good for dynamic content) await page.getByTestId('user-profile').click();

// By title await page.getByTitle('Close dialog').click();

// By alt text (images) await page.getByAltText('User avatar').click(); });

CSS and XPath Locators

test('CSS and XPath locators', async ({ page }) => { // CSS selectors await page.locator('button.primary').click(); await page.locator('#user-menu').click(); await page.locator('[data-testid="submit-btn"]').click(); await page.locator('div.card:first-child').click();

// XPath (use sparingly) await page.locator('xpath=//button[contains(text(), "Submit")]').click();

// Chaining locators const form = page.locator('form#login-form'); await form.locator('input[name="email"]').fill('user@example.com'); await form.locator('button[type="submit"]').click();

// Filter locators await page.getByRole('listitem') .filter({ hasText: 'Product 1' }) .getByRole('button', { name: 'Add to cart' }) .click(); });

Page Object Model

Page Class Pattern

// pages/LoginPage.ts import { Page, Locator } from '@playwright/test';

export class LoginPage { readonly page: Page; readonly usernameInput: Locator; readonly passwordInput: Locator; readonly submitButton: Locator; readonly errorMessage: Locator;

constructor(page: Page) { this.page = page; this.usernameInput = page.getByLabel('Username'); this.passwordInput = page.getByLabel('Password'); this.submitButton = page.getByRole('button', { name: 'Log in' }); this.errorMessage = page.locator('.error-message'); }

async goto() { await this.page.goto('/login'); }

async login(username: string, password: string) { await this.usernameInput.fill(username); await this.passwordInput.fill(password); await this.submitButton.click(); }

async expectErrorMessage(message: string) { await this.errorMessage.waitFor({ state: 'visible' }); await expect(this.errorMessage).toHaveText(message); } }

// pages/DashboardPage.ts export class DashboardPage { readonly page: Page; readonly welcomeMessage: Locator; readonly logoutButton: Locator;

constructor(page: Page) { this.page = page; this.welcomeMessage = page.locator('.welcome-message'); this.logoutButton = page.getByRole('button', { name: 'Logout' }); }

async waitForLoad() { await this.welcomeMessage.waitFor({ state: 'visible' }); }

async logout() { await this.logoutButton.click(); } }

// tests/auth.spec.ts import { test, expect } from '@playwright/test'; import { LoginPage } from '../pages/LoginPage'; import { DashboardPage } from '../pages/DashboardPage';

test('successful login flow', async ({ page }) => { const loginPage = new LoginPage(page); const dashboard = new DashboardPage(page);

await loginPage.goto(); await loginPage.login('testuser', 'password123');

await dashboard.waitForLoad(); await expect(dashboard.welcomeMessage).toContainText('Welcome'); });

Component Pattern

// components/NavigationComponent.ts import { Page, Locator } from '@playwright/test';

export class NavigationComponent { readonly page: Page; readonly homeLink: Locator; readonly profileLink: Locator; readonly searchInput: Locator;

constructor(page: Page) { this.page = page; const nav = page.locator('nav'); this.homeLink = nav.getByRole('link', { name: 'Home' }); this.profileLink = nav.getByRole('link', { name: 'Profile' }); this.searchInput = nav.getByPlaceholder('Search...'); }

async navigateToProfile() { await this.profileLink.click(); }

async search(query: string) { await this.searchInput.fill(query); await this.searchInput.press('Enter'); } }

User Interactions

Form Interactions

test('form interactions', async ({ page }) => { await page.goto('/form');

// Text inputs await page.fill('input[name="email"]', 'user@example.com'); await page.type('textarea[name="message"]', 'Hello', { delay: 100 });

// Checkboxes await page.check('input[type="checkbox"][name="subscribe"]'); await page.uncheck('input[type="checkbox"][name="spam"]');

// Radio buttons await page.check('input[type="radio"][value="option1"]');

// Select dropdowns await page.selectOption('select[name="country"]', 'US'); await page.selectOption('select[name="color"]', { label: 'Blue' }); await page.selectOption('select[name="size"]', { value: 'large' });

// Multi-select await page.selectOption('select[multiple]', ['value1', 'value2']);

// File uploads await page.setInputFiles('input[type="file"]', 'path/to/file.pdf'); await page.setInputFiles('input[type="file"]', [ 'file1.jpg', 'file2.jpg' ]);

// Clear file input await page.setInputFiles('input[type="file"]', []); });

Mouse and Keyboard

test('mouse and keyboard interactions', async ({ page }) => { // Click variations await page.click('button'); await page.dblclick('button'); // Double click await page.click('button', { button: 'right' }); // Right click await page.click('button', { modifiers: ['Shift'] }); // Shift+click

// Hover await page.hover('.tooltip-trigger'); await expect(page.locator('.tooltip')).toBeVisible();

// Drag and drop await page.dragAndDrop('#draggable', '#droppable');

// Keyboard await page.keyboard.press('Enter'); await page.keyboard.press('Control+A'); await page.keyboard.type('Hello World'); await page.keyboard.down('Shift'); await page.keyboard.press('ArrowDown'); await page.keyboard.up('Shift');

// Focus await page.focus('input[name="email"]'); await page.fill('input[name="email"]', 'test@example.com'); });

Waiting Strategies

test('waiting strategies', async ({ page }) => { // Wait for element await page.waitForSelector('.dynamic-content'); await page.waitForSelector('.modal', { state: 'visible' }); await page.waitForSelector('.loading', { state: 'hidden' });

// Wait for load state await page.waitForLoadState('load'); await page.waitForLoadState('domcontentloaded'); await page.waitForLoadState('networkidle');

// Wait for URL await page.waitForURL('**/dashboard'); await page.waitForURL(//product/\d+/);

// Wait for function await page.waitForFunction(() => { return document.querySelectorAll('.item').length > 5; });

// Wait for timeout (avoid if possible) await page.waitForTimeout(1000);

// Wait for event await page.waitForEvent('load'); await page.waitForEvent('popup'); });

Assertions

Common Assertions

import { test, expect } from '@playwright/test';

test('assertions', async ({ page }) => { await page.goto('/dashboard');

// Visibility await expect(page.locator('.header')).toBeVisible(); await expect(page.locator('.loading')).toBeHidden(); await expect(page.locator('.optional')).not.toBeVisible();

// Text content await expect(page.locator('h1')).toHaveText('Dashboard'); await expect(page.locator('h1')).toContainText('Dash'); await expect(page.locator('.message')).toHaveText(/welcome/i);

// Attributes await expect(page.locator('button')).toBeEnabled(); await expect(page.locator('button')).toBeDisabled(); await expect(page.locator('input')).toHaveAttribute('type', 'email'); await expect(page.locator('input')).toHaveValue('test@example.com');

// CSS await expect(page.locator('.button')).toHaveClass('btn-primary'); await expect(page.locator('.button')).toHaveClass(/btn-/); await expect(page.locator('.element')).toHaveCSS('color', 'rgb(255, 0, 0)');

// Count await expect(page.locator('.item')).toHaveCount(5);

// URL and title await expect(page).toHaveURL('http://localhost:3000/dashboard'); await expect(page).toHaveURL(/dashboard$/); await expect(page).toHaveTitle('Dashboard - My App'); await expect(page).toHaveTitle(/Dashboard/);

// Screenshot comparison await expect(page).toHaveScreenshot('dashboard.png'); await expect(page.locator('.widget')).toHaveScreenshot('widget.png'); });

Custom Assertions

test('custom matchers', async ({ page }) => { // Soft assertions (continue test on failure) await expect.soft(page.locator('.title')).toHaveText('Welcome'); await expect.soft(page.locator('.subtitle')).toBeVisible();

// Multiple elements const items = page.locator('.item'); await expect(items).toHaveCount(3); await expect(items.nth(0)).toContainText('First'); await expect(items.nth(1)).toContainText('Second');

// Poll assertions await expect(async () => { const response = await page.request.get('/api/status'); expect(response.ok()).toBeTruthy(); }).toPass({ timeout: 10000, intervals: [1000, 2000, 5000], }); });

Authentication Patterns

Storage State Pattern

// auth.setup.ts - Run once to save auth state import { test as setup } from '@playwright/test';

const authFile = 'playwright/.auth/user.json';

setup('authenticate', async ({ page }) => { await page.goto('/login'); await page.fill('[name="username"]', 'testuser'); await page.fill('[name="password"]', 'password123'); await page.click('button[type="submit"]');

await page.waitForURL('/dashboard');

// Save authentication state await page.context().storageState({ path: authFile }); });

// playwright.config.ts export default defineConfig({ projects: [ { name: 'setup', testMatch: /.*.setup.ts/, }, { name: 'chromium', use: { ...devices['Desktop Chrome'], storageState: authFile, }, dependencies: ['setup'], }, ], });

// tests/dashboard.spec.ts - Already authenticated test('view dashboard', async ({ page }) => { await page.goto('/dashboard'); // Already logged in! await expect(page.locator('.user-menu')).toBeVisible(); });

Multiple User Roles

// fixtures/auth.ts import { test as base } from '@playwright/test';

type Fixtures = { adminPage: Page; userPage: Page; };

export const test = base.extend<Fixtures>({ adminPage: async ({ browser }, use) => { const context = await browser.newContext({ storageState: 'playwright/.auth/admin.json', }); const page = await context.newPage(); await use(page); await context.close(); },

userPage: async ({ browser }, use) => { const context = await browser.newContext({ storageState: 'playwright/.auth/user.json', }); const page = await context.newPage(); await use(page); await context.close(); }, });

// tests/permissions.spec.ts import { test } from '../fixtures/auth';

test('admin can access admin panel', async ({ adminPage }) => { await adminPage.goto('/admin'); await expect(adminPage.locator('.admin-panel')).toBeVisible(); });

test('regular user cannot access admin panel', async ({ userPage }) => { await userPage.goto('/admin'); await expect(userPage.locator('.access-denied')).toBeVisible(); });

Network Control

Request Mocking

test('mock API responses', async ({ page }) => { // Mock API response await page.route('**/api/users', route => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ users: [ { id: 1, name: 'John Doe' }, { id: 2, name: 'Jane Smith' }, ], }), }); });

await page.goto('/users'); await expect(page.locator('.user-list')).toContainText('John Doe'); });

test('mock with conditions', async ({ page }) => { await page.route('/api/', route => { const url = route.request().url();

if (url.includes('/users/1')) {
  route.fulfill({
    status: 200,
    body: JSON.stringify({ id: 1, name: 'Test User' }),
  });
} else if (url.includes('/users')) {
  route.fulfill({
    status: 200,
    body: JSON.stringify({ users: [] }),
  });
} else {
  route.continue();
}

}); });

test('simulate network errors', async ({ page }) => { await page.route('**/api/data', route => { route.abort('failed'); });

await page.goto('/data'); await expect(page.locator('.error-message')).toBeVisible(); });

Request Interception

test('intercept and modify requests', async ({ page }) => { // Modify request headers await page.route('/api/', route => { const headers = route.request().headers(); route.continue({ headers: { ...headers, 'X-Custom-Header': 'test-value', }, }); });

// Modify POST data await page.route('**/api/submit', route => { const postData = route.request().postDataJSON(); route.continue({ postData: JSON.stringify({ ...postData, timestamp: Date.now(), }), }); }); });

test('wait for API response', async ({ page }) => { // Wait for specific request const responsePromise = page.waitForResponse('**/api/users'); await page.click('button#load-users'); const response = await responsePromise;

expect(response.status()).toBe(200); const data = await response.json(); expect(data.users).toHaveLength(10); });

Test Organization

Custom Fixtures

// fixtures/todos.ts import { test as base } from '@playwright/test';

type TodoFixtures = { todoPage: TodoPage; createTodo: (title: string) => Promise<void>; };

export const test = base.extend<TodoFixtures>({ todoPage: async ({ page }, use) => { const todoPage = new TodoPage(page); await todoPage.goto(); await use(todoPage); },

createTodo: async ({ page }, use) => { const create = async (title: string) => { await page.fill('.new-todo', title); await page.press('.new-todo', 'Enter'); }; await use(create); }, });

// tests/todos.spec.ts import { test } from '../fixtures/todos';

test('can create new todo', async ({ todoPage, createTodo }) => { await createTodo('Buy groceries'); await expect(todoPage.todoItems).toHaveCount(1); await expect(todoPage.todoItems).toHaveText('Buy groceries'); });

Test Tags and Filtering

test('smoke test', { tag: '@smoke' }, async ({ page }) => { await page.goto('/'); await expect(page).toHaveTitle('Home'); });

test('regression test', { tag: ['@regression', '@critical'] }, async ({ page }) => { // Complex test });

// Run: npx playwright test --grep @smoke // Run: npx playwright test --grep-invert @slow

Visual Testing

Screenshot Comparison

test('visual regression', async ({ page }) => { await page.goto('/dashboard');

// Full page screenshot await expect(page).toHaveScreenshot('dashboard.png', { maxDiffPixels: 100, });

// Element screenshot await expect(page.locator('.widget')).toHaveScreenshot('widget.png');

// Full page with scroll await expect(page).toHaveScreenshot('full-page.png', { fullPage: true, });

// Mask dynamic elements await expect(page).toHaveScreenshot('masked.png', { mask: [page.locator('.timestamp'), page.locator('.avatar')], });

// Custom threshold await expect(page).toHaveScreenshot('comparison.png', { maxDiffPixelRatio: 0.05, // 5% difference allowed }); });

Video and Trace

// playwright.config.ts export default defineConfig({ use: { video: 'retain-on-failure', trace: 'on-first-retry', screenshot: 'only-on-failure', }, });

// Programmatic video test('record video', async ({ page }) => { await page.goto('/'); // Test actions...

// Video saved automatically to test-results/ });

// View trace: npx playwright show-trace trace.zip

Parallel Execution

Test Sharding

// playwright.config.ts export default defineConfig({ fullyParallel: true, workers: process.env.CI ? 4 : undefined, });

// Run shards in CI // npx playwright test --shard=1/4 // npx playwright test --shard=2/4 // npx playwright test --shard=3/4 // npx playwright test --shard=4/4

Serial Tests

test.describe.configure({ mode: 'serial' });

test.describe('order matters', () => { let orderId: string;

test('create order', async ({ page }) => { // Create order orderId = await createOrder(page); });

test('verify order', async ({ page }) => { // Use orderId from previous test await verifyOrder(page, orderId); }); });

CI/CD Integration

GitHub Actions

.github/workflows/playwright.yml

name: Playwright Tests on: push: branches: [main, master] pull_request: branches: [main, master]

jobs: test: timeout-minutes: 60 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4

  - uses: actions/setup-node@v4
    with:
      node-version: 20

  - 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

Docker

FROM mcr.microsoft.com/playwright:v1.40.0-jammy

WORKDIR /app

COPY package*.json ./ RUN npm ci

COPY . .

CMD ["npx", "playwright", "test"]

Debugging

UI Mode

Interactive debugging

npx playwright test --ui

Debug specific test

npx playwright test --debug login.spec.ts

Step through test

npx playwright test --headed --slow-mo=1000

Trace Viewer

// Generate trace test('with trace', async ({ page }) => { await page.context().tracing.start({ screenshots: true, snapshots: true });

// Test actions await page.goto('/');

await page.context().tracing.stop({ path: 'trace.zip' }); });

// View: npx playwright show-trace trace.zip

Console Logs

test('capture console', async ({ page }) => { page.on('console', msg => console.log(Browser: ${msg.text()})); page.on('pageerror', error => console.error(Error: ${error.message}));

await page.goto('/'); });

Best Practices

  1. Use Stable Locators

// ✅ Good - Role-based, stable await page.getByRole('button', { name: 'Submit' }).click(); await page.getByLabel('Email').fill('test@example.com');

// ❌ Bad - Fragile, implementation-dependent await page.click('button.btn-primary.submit-btn'); await page.fill('div > form > input:nth-child(3)');

  1. Leverage Auto-Waiting

// ✅ Good - Auto-waits await page.click('button'); await expect(page.locator('.result')).toBeVisible();

// ❌ Bad - Manual waits await page.waitForTimeout(2000); await page.click('button');

  1. Use Page Object Model

// ✅ Good - Reusable, maintainable const loginPage = new LoginPage(page); await loginPage.login('user', 'pass');

// ❌ Bad - Duplicated selectors await page.fill('[name="username"]', 'user'); await page.fill('[name="password"]', 'pass');

  1. Parallel-Safe Tests

// ✅ Good - Isolated test('user signup', async ({ page }) => { const uniqueEmail = user-${Date.now()}@test.com; await signUp(page, uniqueEmail); });

// ❌ Bad - Shared state test('user signup', async ({ page }) => { await signUp(page, 'test@test.com'); // Conflicts in parallel });

  1. Handle Flakiness

// ✅ Good - Wait for network idle await page.goto('/', { waitUntil: 'networkidle' }); await expect(page.locator('.data')).toBeVisible();

// Configure retries test.describe(() => { test.use({ retries: 2 });

test('flaky test', async ({ page }) => { // Test with auto-retry }); });

Common Patterns

Multi-Page Scenarios

test('popup handling', async ({ page, context }) => { // Listen for new page const popupPromise = context.waitForEvent('page'); await page.click('a[target="_blank"]'); const popup = await popupPromise;

await popup.waitForLoadState(); await expect(popup).toHaveTitle('New Window'); await popup.close(); });

Conditional Logic

test('handle optional elements', async ({ page }) => { await page.goto('/');

// Close modal if present const modal = page.locator('.modal'); if (await modal.isVisible()) { await page.click('.modal .close-button'); }

// Or use count const cookieBanner = page.locator('.cookie-banner'); if ((await cookieBanner.count()) > 0) { await page.click('.accept-cookies'); } });

Data-Driven Tests

const testCases = [ { input: 'hello', expected: 'HELLO' }, { input: 'World', expected: 'WORLD' }, { input: '123', expected: '123' }, ];

for (const { input, expected } of testCases) { test(transforms "${input}" to "${expected}", async ({ page }) => { await page.goto('/transform'); await page.fill('input', input); await page.click('button'); await expect(page.locator('.result')).toHaveText(expected); }); }

Resources

  • Playwright Documentation

  • Best Practices Guide

  • API Reference

  • GitHub Examples

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

drizzle-orm

No summary provided by upstream source.

Repository SourceNeeds Review
General

pydantic

No summary provided by upstream source.

Repository SourceNeeds Review
General

tailwind-css

No summary provided by upstream source.

Repository SourceNeeds Review
General

trpc-type-safety

No summary provided by upstream source.

Repository SourceNeeds Review