Imported from FutureAtoms/claude-skills-backup (
playwright-e2e-testing/SKILL.md). Install upstream withnpx skills add FutureAtoms/claude-skills-backup --skill playwright-e2e-testing. Copyright stays with the author.
Playwright E2E Testing
Set up end-to-end testing: $ARGUMENTS
Expert Knowledge
You are a Playwright E2E testing specialist with expertise in:
- Page Object Model (POM) architecture
- Custom test fixtures and hooks
- Parallel and sharded test execution
- Test data management and factories
- Cross-browser testing strategies
- Flaky test prevention and debugging
- CI/CD integration
Project Structure
tests/
├── e2e/
│ ├── auth/
│ │ ├── login.spec.ts
│ │ ├── register.spec.ts
│ │ └── password-reset.spec.ts
│ ├── checkout/
│ │ ├── cart.spec.ts
│ │ └── payment.spec.ts
│ └── user/
│ └── profile.spec.ts
├── fixtures/
│ ├── index.ts
│ ├── auth.fixture.ts
│ └── database.fixture.ts
├── pages/
│ ├── BasePage.ts
│ ├── LoginPage.ts
│ ├── HomePage.ts
│ └── CheckoutPage.ts
├── data/
│ ├── users.ts
│ └── products.ts
└── utils/
├── helpers.ts
└── api.ts
playwright.config.ts
Page Object Model
Base Page
// tests/pages/BasePage.ts
import { Page, Locator, expect } from '@playwright/test';
export abstract class BasePage {
constructor(protected page: Page) {}
// Common elements
get header() { return this.page.locator('header'); }
get footer() { return this.page.locator('footer'); }
get loadingSpinner() { return this.page.locator('[data-testid="loading"]'); }
get toastMessage() { return this.page.locator('[role="alert"]'); }
// Common actions
async waitForPageLoad() {
await this.page.waitForLoadState('networkidle');
await expect(this.loadingSpinner).toBeHidden();
}
async expectToastMessage(message: string) {
await expect(this.toastMessage).toContainText(message);
}
async navigate(path: string) {
await this.page.goto(path);
await this.waitForPageLoad();
}
// Screenshot helper
async takeScreenshot(name: string) {
await this.page.screenshot({ path: `screenshots/${name}.png` });
}
}
Login Page
// tests/pages/LoginPage.ts
import { Page, expect } from '@playwright/test';
import { BasePage } from './BasePage';
export class LoginPage extends BasePage {
readonly url = '/login';
// Locators
get emailInput() { return this.page.getByLabel('Email'); }
get passwordInput() { return this.page.getByLabel('Password'); }
get submitButton() { return this.page.getByRole('button', { name: 'Sign In' }); }
get forgotPasswordLink() { return this.page.getByRole('link', { name: 'Forgot password?' }); }
get errorMessage() { return this.page.getByRole('alert'); }
get rememberMeCheckbox() { return this.page.getByLabel('Remember me'); }
// Actions
async goto() {
await this.navigate(this.url);
}
async login(email: string, password: string, options?: { rememberMe?: boolean }) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
if (options?.rememberMe) {
await this.rememberMeCheckbox.check();
}
await this.submitButton.click();
}
async loginAndWait(email: string, password: string) {
await this.login(email, password);
await this.page.waitForURL('**/dashboard');
}
// Assertions
async expectLoginError(message: string) {
await expect(this.errorMessage).toContainText(message);
}
async expectToBeOnLoginPage() {
await expect(this.page).toHaveURL(/.*login/);
await expect(this.emailInput).toBeVisible();
}
}
Dashboard Page
// tests/pages/DashboardPage.ts
import { Page, expect } from '@playwright/test';
import { BasePage } from './BasePage';
export class DashboardPage extends BasePage {
readonly url = '/dashboard';
// Locators
get welcomeMessage() { return this.page.getByRole('heading', { level: 1 }); }
get userMenu() { return this.page.getByRole('button', { name: /user menu/i }); }
get logoutButton() { return this.page.getByRole('menuitem', { name: 'Logout' }); }
get statsCards() { return this.page.locator('[data-testid="stat-card"]'); }
get recentActivity() { return this.page.locator('[data-testid="activity-feed"]'); }
// Actions
async goto() {
await this.navigate(this.url);
}
async logout() {
await this.userMenu.click();
await this.logoutButton.click();
await this.page.waitForURL('**/login');
}
async getStatValue(statName: string): Promise<string> {
const card = this.statsCards.filter({ hasText: statName });
return await card.locator('.stat-value').textContent() || '';
}
// Assertions
async expectWelcomeMessage(userName: string) {
await expect(this.welcomeMessage).toContainText(`Welcome, ${userName}`);
}
async expectStatCount(statName: string, count: number) {
const value = await this.getStatValue(statName);
expect(parseInt(value)).toBe(count);
}
}
Custom Fixtures
Auth Fixture
// tests/fixtures/auth.fixture.ts
import { test as base, Page, BrowserContext } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
type AuthFixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
authenticatedPage: Page;
adminPage: Page;
};
export const test = base.extend<AuthFixtures>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
// Page with pre-authenticated user
authenticatedPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'tests/.auth/user.json',
});
const page = await context.newPage();
await use(page);
await context.close();
},
// Page with admin privileges
adminPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'tests/.auth/admin.json',
});
const page = await context.newPage();
await use(page);
await context.close();
},
});
export { expect } from '@playwright/test';
Database Fixture
// tests/fixtures/database.fixture.ts
import { test as base } from '@playwright/test';
import { PrismaClient } from '@prisma/client';
type DatabaseFixtures = {
db: PrismaClient;
seedUser: (data: { email: string; name: string }) => Promise<any>;
cleanupUsers: () => Promise<void>;
};
export const test = base.extend<DatabaseFixtures>({
db: async ({}, use) => {
const prisma = new PrismaClient();
await use(prisma);
await prisma.$disconnect();
},
seedUser: async ({ db }, use) => {
const createdUsers: string[] = [];
const seed = async (data: { email: string; name: string }) => {
const user = await db.user.create({
data: {
...data,
password: 'hashed_password',
},
});
createdUsers.push(user.id);
return user;
};
await use(seed);
// Cleanup after test
await db.user.deleteMany({
where: { id: { in: createdUsers } },
});
},
cleanupUsers: async ({ db }, use) => {
await use(async () => {
await db.user.deleteMany({
where: { email: { contains: '@test.com' } },
});
});
},
});
Combined Fixtures
// tests/fixtures/index.ts
import { mergeTests } from '@playwright/test';
import { test as authTest } from './auth.fixture';
import { test as dbTest } from './database.fixture';
export const test = mergeTests(authTest, dbTest);
export { expect } from '@playwright/test';
Test Data Management
Test Data Factory
// tests/data/users.ts
import { faker } from '@faker-js/faker';
export interface TestUser {
email: string;
password: string;
name: string;
role: 'admin' | 'user';
}
export const createTestUser = (overrides?: Partial<TestUser>): TestUser => ({
email: faker.internet.email(),
password: faker.internet.password({ length: 12 }),
name: faker.person.fullName(),
role: 'user',
...overrides,
});
export const TEST_USERS = {
admin: {
email: 'admin@test.com',
password: 'AdminPass123!',
name: 'Test Admin',
role: 'admin' as const,
},
user: {
email: 'user@test.com',
password: 'UserPass123!',
name: 'Test User',
role: 'user' as const,
},
};
Product Data
// tests/data/products.ts
import { faker } from '@faker-js/faker';
export interface TestProduct {
name: string;
price: number;
description: string;
category: string;
}
export const createTestProduct = (overrides?: Partial<TestProduct>): TestProduct => ({
name: faker.commerce.productName(),
price: parseFloat(faker.commerce.price()),
description: faker.commerce.productDescription(),
category: faker.commerce.department(),
...overrides,
});
E2E Test Examples
Authentication Flow
// tests/e2e/auth/login.spec.ts
import { test, expect } from '../../fixtures';
import { TEST_USERS } from '../../data/users';
test.describe('Login Flow', () => {
test.beforeEach(async ({ loginPage }) => {
await loginPage.goto();
});
test('successful login with valid credentials', async ({ loginPage, dashboardPage, page }) => {
await loginPage.login(TEST_USERS.user.email, TEST_USERS.user.password);
await expect(page).toHaveURL(/.*dashboard/);
await dashboardPage.expectWelcomeMessage(TEST_USERS.user.name);
});
test('shows error with invalid credentials', async ({ loginPage }) => {
await loginPage.login('wrong@email.com', 'wrongpassword');
await loginPage.expectLoginError('Invalid email or password');
await loginPage.expectToBeOnLoginPage();
});
test('validates required fields', async ({ loginPage }) => {
await loginPage.submitButton.click();
await expect(loginPage.emailInput).toHaveAttribute('aria-invalid', 'true');
await expect(loginPage.passwordInput).toHaveAttribute('aria-invalid', 'true');
});
test('remembers user with remember me checked', async ({ loginPage, context }) => {
await loginPage.login(TEST_USERS.user.email, TEST_USERS.user.password, { rememberMe: true });
// Check cookie is persistent
const cookies = await context.cookies();
const authCookie = cookies.find(c => c.name === 'auth_token');
expect(authCookie?.expires).toBeGreaterThan(Date.now() / 1000 + 86400); // > 1 day
});
});
Checkout Flow
// tests/e2e/checkout/cart.spec.ts
import { test, expect } from '../../fixtures';
test.describe('Shopping Cart', () => {
test.use({ storageState: 'tests/.auth/user.json' });
test.beforeEach(async ({ page }) => {
await page.goto('/products');
});
test('add product to cart', async ({ page }) => {
// Add first product
await page.getByTestId('add-to-cart').first().click();
// Verify cart badge
await expect(page.getByTestId('cart-count')).toHaveText('1');
// Open cart
await page.getByRole('link', { name: 'Cart' }).click();
// Verify product in cart
await expect(page.getByTestId('cart-item')).toHaveCount(1);
});
test('update quantity in cart', async ({ page }) => {
// Add product and go to cart
await page.getByTestId('add-to-cart').first().click();
await page.getByRole('link', { name: 'Cart' }).click();
// Increase quantity
await page.getByRole('button', { name: 'Increase quantity' }).click();
await expect(page.getByTestId('quantity-input')).toHaveValue('2');
// Verify total updated
const itemPrice = await page.getByTestId('item-price').textContent();
const total = await page.getByTestId('cart-total').textContent();
expect(parseFloat(total!.replace('$', ''))).toBe(parseFloat(itemPrice!.replace('$', '')) * 2);
});
test('complete checkout flow', async ({ page }) => {
// Add product
await page.getByTestId('add-to-cart').first().click();
await page.getByRole('link', { name: 'Cart' }).click();
// Proceed to checkout
await page.getByRole('button', { name: 'Checkout' }).click();
// Fill shipping
await page.getByLabel('Address').fill('123 Test St');
await page.getByLabel('City').fill('Test City');
await page.getByLabel('ZIP').fill('12345');
await page.getByRole('button', { name: 'Continue to payment' }).click();
// Fill payment (test card)
await page.getByLabel('Card number').fill('4242424242424242');
await page.getByLabel('Expiry').fill('12/30');
await page.getByLabel('CVC').fill('123');
await page.getByRole('button', { name: 'Place order' }).click();
// Verify success
await expect(page.getByText('Order confirmed')).toBeVisible();
await expect(page.getByTestId('order-number')).toBeVisible();
});
});
Parallel Execution
Configuration
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
// Run tests in parallel
fullyParallel: true,
// Number of parallel workers
workers: process.env.CI ? 4 : undefined,
// Retry failed tests
retries: process.env.CI ? 2 : 0,
// Sharding for CI
// Run with: npx playwright test --shard=1/4
});
Test Isolation
// Each test gets fresh context
test.describe.configure({ mode: 'parallel' });
test.describe('User Profile', () => {
// Tests run in parallel - no shared state
test('can update name', async ({ authenticatedPage }) => {
// ...
});
test('can update email', async ({ authenticatedPage }) => {
// ...
});
test('can change password', async ({ authenticatedPage }) => {
// ...
});
});
// Serial execution when needed
test.describe.configure({ mode: 'serial' });
test.describe('Onboarding Flow', () => {
// Tests run in order, share state
test('step 1: create account', async ({ page }) => {
// ...
});
test('step 2: verify email', async ({ page }) => {
// ...
});
test('step 3: complete profile', async ({ page }) => {
// ...
});
});
Authentication State Reuse
Setup Project
// playwright.config.ts
export default defineConfig({
projects: [
// Setup project - runs first
{
name: 'setup',
testMatch: /.*\.setup\.ts/,
},
// Tests that need auth
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'tests/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
Auth Setup
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import { TEST_USERS } from './data/users';
const authFile = 'tests/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(TEST_USERS.user.email);
await page.getByLabel('Password').fill(TEST_USERS.user.password);
await page.getByRole('button', { name: 'Sign In' }).click();
await page.waitForURL('**/dashboard');
// Save signed-in state
await page.context().storageState({ path: authFile });
});
Running E2E Tests
# Run all E2E tests
npx playwright test tests/e2e/
# Run specific test file
npx playwright test tests/e2e/auth/login.spec.ts
# Run tests with specific tag
npx playwright test --grep @smoke
# Run tests in headed mode
npx playwright test --headed
# Run with UI mode (debugging)
npx playwright test --ui
# Run with trace
npx playwright test --trace on
# Sharded execution (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
# Generate test code
npx playwright codegen https://example.com
Deliverables
For: $ARGUMENTS
Provide:
- Page Object Model classes for relevant pages
- Custom fixtures for authentication/data
- Test data factories if needed
- Comprehensive E2E test file
- Configuration for parallel execution
- Commands for running tests