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#
- Install Playwright:
npm init playwright@latest - Configure browsers: Set up projects in
playwright.config.ts - Create page objects: Organize selectors and actions
- Set up auth: Save and reuse authentication state
- Write tests: Cover critical user journeys
- Run in CI: Add to your pipeline
Best Practices#
- Use data-testid - Add stable selectors for test elements
- Create page objects - Organize selectors and reuse across tests
- Test critical paths - Focus on important user journeys
- Keep tests independent - Each test should work in isolation
- Use visual regression - Catch unintended UI changes
- Test across browsers - Verify cross-browser compatibility
- Run in CI - Automate E2E tests in your pipeline
Related Patterns#
- Vitest - Unit and component testing
- Component Testing - React component testing
- Integration Testing - API and service testing
- Fixtures - Test data management