E2E Testing Pattern

Build comprehensive end-to-end tests with Playwright, including page objects, authentication, visual regression, and cross-browser testing.

Overview#

End-to-end (E2E) tests verify your application works correctly from a user's perspective, testing complete flows through the browser. Playwright provides fast, reliable cross-browser testing.

When to use:

  • Testing critical user flows (login, checkout, signup)
  • Verifying UI interactions work correctly
  • Cross-browser compatibility testing
  • Visual regression testing

Key features:

  • Cross-browser testing (Chromium, Firefox, WebKit)
  • Mobile device emulation
  • Visual regression with screenshots
  • Network request mocking
  • Authentication state persistence

Code Example#

Basic Test#

1// tests/e2e/login.spec.ts 2import { test, expect } from '@playwright/test' 3 4test('user can log in', async ({ page }) => { 5 await page.goto('/login') 6 7 await page.fill('[name="email"]', 'test@example.com') 8 await page.fill('[name="password"]', 'password123') 9 await page.click('button[type="submit"]') 10 11 await expect(page).toHaveURL('/dashboard') 12 await expect(page.locator('h1')).toContainText('Welcome') 13}) 14 15test('shows error for invalid credentials', async ({ page }) => { 16 await page.goto('/login') 17 18 await page.fill('[name="email"]', 'wrong@example.com') 19 await page.fill('[name="password"]', 'wrongpassword') 20 await page.click('button[type="submit"]') 21 22 await expect(page.locator('[role="alert"]')).toContainText('Invalid credentials') 23 await expect(page).toHaveURL('/login') 24})

Page Object Model#

1// tests/e2e/pages/login.page.ts 2import { Page, Locator, expect } from '@playwright/test' 3 4export class LoginPage { 5 readonly page: Page 6 readonly emailInput: Locator 7 readonly passwordInput: Locator 8 readonly submitButton: Locator 9 readonly errorMessage: Locator 10 11 constructor(page: Page) { 12 this.page = page 13 this.emailInput = page.locator('[name="email"]') 14 this.passwordInput = page.locator('[name="password"]') 15 this.submitButton = page.locator('button[type="submit"]') 16 this.errorMessage = page.locator('[role="alert"]') 17 } 18 19 async goto() { 20 await this.page.goto('/login') 21 } 22 23 async login(email: string, password: string) { 24 await this.emailInput.fill(email) 25 await this.passwordInput.fill(password) 26 await this.submitButton.click() 27 } 28 29 async expectError(message: string) { 30 await expect(this.errorMessage).toContainText(message) 31 } 32} 33 34// tests/e2e/pages/dashboard.page.ts 35export class DashboardPage { 36 readonly page: Page 37 readonly heading: Locator 38 readonly userMenu: Locator 39 40 constructor(page: Page) { 41 this.page = page 42 this.heading = page.locator('h1') 43 this.userMenu = page.locator('[data-testid="user-menu"]') 44 } 45 46 async expectWelcome(name: string) { 47 await expect(this.heading).toContainText(`Welcome, ${name}`) 48 } 49 50 async logout() { 51 await this.userMenu.click() 52 await this.page.click('text=Logout') 53 } 54} 55 56// Usage in test 57import { test, expect } from '@playwright/test' 58import { LoginPage } from './pages/login.page' 59import { DashboardPage } from './pages/dashboard.page' 60 61test('login with page object', async ({ page }) => { 62 const loginPage = new LoginPage(page) 63 const dashboardPage = new DashboardPage(page) 64 65 await loginPage.goto() 66 await loginPage.login('test@example.com', 'password123') 67 68 await expect(page).toHaveURL('/dashboard') 69 await dashboardPage.expectWelcome('Test User') 70})

Authentication State#

1// tests/e2e/auth.setup.ts 2import { test as setup, expect } from '@playwright/test' 3 4const authFile = 'playwright/.auth/user.json' 5 6setup('authenticate', async ({ page }) => { 7 await page.goto('/login') 8 await page.fill('[name="email"]', process.env.TEST_EMAIL!) 9 await page.fill('[name="password"]', process.env.TEST_PASSWORD!) 10 await page.click('button[type="submit"]') 11 12 await expect(page).toHaveURL('/dashboard') 13 14 // Save auth state 15 await page.context().storageState({ path: authFile }) 16}) 17 18// playwright.config.ts 19import { defineConfig, devices } from '@playwright/test' 20 21export default defineConfig({ 22 projects: [ 23 { name: 'setup', testMatch: /.*\.setup\.ts/ }, 24 { 25 name: 'chromium', 26 use: { 27 ...devices['Desktop Chrome'], 28 storageState: 'playwright/.auth/user.json' 29 }, 30 dependencies: ['setup'] 31 }, 32 { 33 name: 'firefox', 34 use: { 35 ...devices['Desktop Firefox'], 36 storageState: 'playwright/.auth/user.json' 37 }, 38 dependencies: ['setup'] 39 } 40 ] 41}) 42 43// Usage - tests start authenticated 44test('authenticated user can access settings', async ({ page }) => { 45 await page.goto('/settings') 46 await expect(page).toHaveURL('/settings') 47 await expect(page.locator('h1')).toContainText('Settings') 48})

API Mocking#

1// tests/e2e/dashboard.spec.ts 2import { test, expect } from '@playwright/test' 3 4test('displays mocked data', async ({ page }) => { 5 // Mock API response 6 await page.route('/api/users', async (route) => { 7 await route.fulfill({ 8 status: 200, 9 contentType: 'application/json', 10 body: JSON.stringify([ 11 { id: '1', name: 'Test User' } 12 ]) 13 }) 14 }) 15 16 await page.goto('/dashboard') 17 await expect(page.locator('text=Test User')).toBeVisible() 18}) 19 20test('handles API errors gracefully', async ({ page }) => { 21 await page.route('/api/users', async (route) => { 22 await route.fulfill({ 23 status: 500, 24 contentType: 'application/json', 25 body: JSON.stringify({ error: 'Server error' }) 26 }) 27 }) 28 29 await page.goto('/dashboard') 30 await expect(page.locator('text=Failed to load')).toBeVisible() 31})

Visual Regression#

1// tests/e2e/visual.spec.ts 2import { test, expect } from '@playwright/test' 3 4test('homepage visual regression', async ({ page }) => { 5 await page.goto('/') 6 7 // Full page screenshot 8 await expect(page).toHaveScreenshot('homepage.png', { 9 fullPage: true 10 }) 11}) 12 13test('component visual regression', async ({ page }) => { 14 await page.goto('/components') 15 16 // Element screenshot 17 await expect(page.locator('[data-testid="card"]')).toHaveScreenshot('card.png') 18}) 19 20test('visual regression with threshold', async ({ page }) => { 21 await page.goto('/') 22 23 await expect(page).toHaveScreenshot('homepage-threshold.png', { 24 maxDiffPixelRatio: 0.1 // Allow 10% difference 25 }) 26})

Mobile Testing#

1// tests/e2e/mobile.spec.ts 2import { test, expect, devices } from '@playwright/test' 3 4test.use({ ...devices['iPhone 13'] }) 5 6test('mobile navigation', async ({ page }) => { 7 await page.goto('/') 8 9 // Desktop nav should be hidden 10 await expect(page.locator('nav.desktop')).toBeHidden() 11 12 // Open mobile menu 13 await page.click('[aria-label="Open menu"]') 14 await expect(page.locator('nav.mobile')).toBeVisible() 15 16 // Navigate 17 await page.click('text=Products') 18 await expect(page).toHaveURL('/products') 19}) 20 21test('responsive layout', async ({ page }) => { 22 await page.goto('/') 23 24 // Check mobile-specific elements 25 await expect(page.locator('.mobile-banner')).toBeVisible() 26 await expect(page.locator('.desktop-sidebar')).toBeHidden() 27})

Form Testing#

1// tests/e2e/forms.spec.ts 2import { test, expect } from '@playwright/test' 3 4test('contact form submission', async ({ page }) => { 5 await page.goto('/contact') 6 7 // Fill form 8 await page.fill('[name="name"]', 'John Doe') 9 await page.fill('[name="email"]', 'john@example.com') 10 await page.selectOption('[name="subject"]', 'support') 11 await page.fill('[name="message"]', 'This is a test message') 12 13 // Submit 14 await page.click('button[type="submit"]') 15 16 // Verify success 17 await expect(page.locator('.success-message')).toContainText('Thank you') 18}) 19 20test('form validation', async ({ page }) => { 21 await page.goto('/contact') 22 23 // Submit empty form 24 await page.click('button[type="submit"]') 25 26 // Check validation errors 27 await expect(page.locator('[data-error="name"]')).toContainText('Name is required') 28 await expect(page.locator('[data-error="email"]')).toContainText('Email is required') 29 30 // Fill invalid email 31 await page.fill('[name="email"]', 'invalid-email') 32 await page.click('button[type="submit"]') 33 await expect(page.locator('[data-error="email"]')).toContainText('Invalid email') 34})

Complete User Journey#

1// tests/e2e/checkout.spec.ts 2import { test, expect } from '@playwright/test' 3 4test('complete checkout flow', async ({ page }) => { 5 // Browse products 6 await page.goto('/products') 7 await page.click('[data-testid="product-1"]') 8 9 // Add to cart 10 await page.selectOption('[name="size"]', 'M') 11 await page.fill('[name="quantity"]', '2') 12 await page.click('text=Add to Cart') 13 14 // Verify cart 15 await page.click('[data-testid="cart-icon"]') 16 await expect(page.locator('.cart-item')).toHaveCount(1) 17 await expect(page.locator('.cart-total')).toContainText('$49.98') 18 19 // Proceed to checkout 20 await page.click('text=Checkout') 21 await expect(page).toHaveURL('/checkout') 22 23 // Fill shipping 24 await page.fill('[name="address"]', '123 Main St') 25 await page.fill('[name="city"]', 'New York') 26 await page.fill('[name="zip"]', '10001') 27 await page.click('text=Continue to Payment') 28 29 // Fill payment (test mode) 30 const stripeFrame = page.frameLocator('iframe[name*="stripe"]') 31 await stripeFrame.locator('[name="cardnumber"]').fill('4242424242424242') 32 await stripeFrame.locator('[name="exp-date"]').fill('12/30') 33 await stripeFrame.locator('[name="cvc"]').fill('123') 34 35 // Place order 36 await page.click('text=Place Order') 37 38 // Verify confirmation 39 await expect(page).toHaveURL(/\/orders\/\w+/) 40 await expect(page.locator('h1')).toContainText('Order Confirmed') 41})

Usage Instructions#

  1. Install Playwright: npm init playwright@latest
  2. Configure browsers: Set up projects in playwright.config.ts
  3. Create page objects: Organize selectors and actions
  4. Set up auth: Save and reuse authentication state
  5. Write tests: Cover critical user journeys
  6. Run in CI: Add to your pipeline

Best Practices#

  1. Use data-testid - Add stable selectors for test elements
  2. Create page objects - Organize selectors and reuse across tests
  3. Test critical paths - Focus on important user journeys
  4. Keep tests independent - Each test should work in isolation
  5. Use visual regression - Catch unintended UI changes
  6. Test across browsers - Verify cross-browser compatibility
  7. Run in CI - Automate E2E tests in your pipeline