How to Set Up End-to-End Testing (2026)
E2E tests verify your app works from the user's perspective — clicking buttons, filling forms, navigating pages. Playwright is the standard in 2026.
Setup Playwright
npm init playwright@latest
This creates:
playwright.config.ts— configurationtests/— test directory.github/workflows/playwright.yml— CI pipeline
Your First Test
// tests/home.spec.ts
import { test, expect } from '@playwright/test'
test('homepage loads and shows title', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveTitle(/My App/)
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible()
})
Common Test Patterns
Form Submission
test('user can sign up', async ({ page }) => {
await page.goto('/signup')
await page.getByLabel('Email').fill('test@example.com')
await page.getByLabel('Password').fill('SecurePass123!')
await page.getByRole('button', { name: 'Sign Up' }).click()
await expect(page).toHaveURL('/dashboard')
await expect(page.getByText('Welcome, test@example.com')).toBeVisible()
})
Navigation
test('navigation works', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: 'Blog' }).click()
await expect(page).toHaveURL('/blog')
await expect(page.getByRole('heading', { name: 'Blog' })).toBeVisible()
})
Authentication Flow
test('login and access dashboard', async ({ page }) => {
// Login
await page.goto('/login')
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('password')
await page.getByRole('button', { name: 'Log In' }).click()
// Verify dashboard
await expect(page).toHaveURL('/dashboard')
await expect(page.getByText('Dashboard')).toBeVisible()
// Verify auth persists
await page.goto('/settings')
await expect(page.getByText('Account Settings')).toBeVisible()
})
Reusable Auth State
// tests/auth.setup.ts
import { test as setup } from '@playwright/test'
setup('authenticate', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('user@example.com')
await page.getByLabel('Password').fill('password')
await page.getByRole('button', { name: 'Log In' }).click()
await page.waitForURL('/dashboard')
// Save auth state
await page.context().storageState({ path: '.auth/user.json' })
})
// playwright.config.ts
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { storageState: '.auth/user.json' },
dependencies: ['setup'],
},
],
})
Configuration
// playwright.config.ts
import { defineConfig } from '@playwright/test'
export default defineConfig({
testDir: './tests',
fullyParallel: true,
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',
},
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
})
CI Integration (GitHub Actions)
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
Best Practices
- Test user flows, not implementation. Test "user can create a post," not "button has class X."
- Use role-based selectors.
getByRole('button')overpage.locator('.btn'). - Don't test everything E2E. Unit tests for logic, E2E for critical user flows.
- Keep tests independent. Each test should work in isolation.
- Use test fixtures. Seed data for each test, clean up after.
FAQ
How many E2E tests should I write?
Cover critical paths: signup, login, core feature, payment. 10-30 E2E tests cover most apps. Don't test every edge case with E2E.
Playwright vs Cypress?
Playwright: faster, multi-browser, better for modern apps. Cypress: easier learning curve, better debugging UI. Choose Playwright for new projects.
How do I handle flaky tests?
Add retries in CI (retries: 2). Use await expect() instead of fixed waits. Check for race conditions.
Bottom Line
Install Playwright, write tests for critical user flows, run in CI. Start with 5-10 tests covering signup, login, and core features. Use role-based selectors and auth state reuse. The Playwright HTML reporter makes debugging failures painless.