| name | e2e-testing |
| description | End-to-end testing patterns with Playwright. Use when writing browser automation tests, integration tests, testing user flows, or setting up E2E test suites. |
End-to-End Testing Patterns (Playwright)
Setup
npm init playwright@latest
npx playwright install
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
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: 'Mobile Safari', use: { ...devices['iPhone 13'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})
Page Object Model
import { Page, Locator } from '@playwright/test'
export class LoginPage {
readonly emailInput: Locator
readonly passwordInput: Locator
readonly submitButton: Locator
readonly errorMessage: Locator
constructor(private page: Page) {
this.emailInput = page.getByLabel('Email')
this.passwordInput = page.getByLabel('Password')
this.submitButton = 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.submitButton.click()
}
}
Writing Tests
import { test, expect } from '@playwright/test'
import { LoginPage } from './pages/LoginPage'
test.describe('Authentication', () => {
test('login with valid credentials', async ({ page }) => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.login('user@example.com', 'password123')
await expect(page).toHaveURL('/dashboard')
})
test('show error for invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page)
await loginPage.goto()
await loginPage.login('wrong@email.com', 'wrong')
await expect(loginPage.errorMessage).toContainText('Invalid credentials')
})
})
Reuse Auth State
import { test as setup } from '@playwright/test'
setup('authenticate', async ({ page }) => {
await page.goto('/login')
await page.fill('[name=email]', 'test@example.com')
await page.fill('[name=password]', 'password')
await page.click('[type=submit]')
await page.waitForURL('/dashboard')
await page.context().storageState({ path: 'tests/e2e/.auth/user.json' })
})
API Mocking
test('displays mocked products', async ({ page }) => {
await page.route('**/api/products', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: '1', name: 'Test Product', price: 99.99 }]),
})
})
await page.goto('/products')
await expect(page.getByText('Test Product')).toBeVisible()
})
Common Assertions
await expect(element).toBeVisible()
await expect(element).toContainText('partial text')
await expect(page).toHaveURL('/dashboard')
await expect(page.getByRole('listitem')).toHaveCount(5)
await expect(input).toHaveValue('hello')
await expect(button).toBeDisabled()
const response = await page.waitForResponse('**/api/submit')
expect(response.status()).toBe(200)
Running Tests
npx playwright test
npx playwright test auth.spec.ts
npx playwright test --headed
npx playwright test --ui
npx playwright show-report
Rules
- Use Page Object Model — never write raw selectors in test bodies
- Prefer role/label selectors over CSS selectors (more resilient)
- Save auth state with storageState — don't login in every test
- Mock external APIs (payment, email) — test in isolation
- Never use waitForTimeout() — use waitForResponse or waitForSelector
- Run E2E against production build in CI
- E2E for critical paths only — unit test business logic separately