Home/Learn/JavaScript & TypeScript/End-to-End Testing with Playwright

End-to-End Testing with Playwright

Advanced
Quality

E2E tests run the real app in a real browser. They catch what unit tests structurally cannot, and they cost more — so pick the few flows worth that cost.

Overview

A unit test cannot tell you that the login redirect broke, that a Content Security Policy blocked your script, or that the payment flow fails on the third step. Only a real browser hitting a real build can. The trade-off is speed and flakiness, which is why the sensible shape is many unit tests, some component tests, and a handful of E2E tests covering the flows that lose money when broken. Playwright has largely settled this space: auto-waiting removes most flakiness, and it can reuse a saved login session so every test does not sign in from scratch.

A Test, and Auto-Waiting

Locators retry until the element is ready, which is why explicit sleeps are unnecessary and wrong.

Locators retry; sleeps do not
import { test, expect } from '@playwright/test'

test('a signed-in user can run a solution', async ({ page }) => {
  await page.goto('/dsa/problems/two-sum')

  await page.getByRole('button', { name: 'Run' }).click()
  await expect(page.getByText('3 of 3 tests passed')).toBeVisible()
})

// A locator is lazy and auto-retries: it waits for the element to
// exist, be visible, be stable and be enabled before acting.
// So this is never needed and hides real timing bugs:
await page.waitForTimeout(2000)      // avoid

// Assertions retry too, up to the timeout
await expect(page.getByRole('alert')).toHaveText(/saved/i)

// Locator priority mirrors Testing Library
page.getByRole('button', { name: 'Submit' })
page.getByLabel('Email')
page.getByTestId('editor')           // last resort

Auth and Isolation

Sign in once, reuse the state, and keep tests independent of each other.

Saved storage state, and independent tests
// Global setup: log in once, save the storage state
// auth.setup.ts
await page.goto('/login')
await page.getByLabel('Email').fill(process.env.TEST_EMAIL)
await page.getByLabel('Password').fill(process.env.TEST_PASSWORD)
await page.getByRole('button', { name: 'Sign in' }).click()
await page.context().storageState({ path: 'playwright/.auth/user.json' })

// playwright.config.ts
use: { storageState: 'playwright/.auth/user.json' }

// Every test then starts signed in, with no repeated login.

// Isolation rules that keep a suite maintainable:
//   - no test depends on another test having run
//   - no test depends on data another test created
//   - each test creates and cleans up what it needs
//   - use a seeded test database, not production

// Stub third parties you do not control
await page.route('**/api.razorpay.com/**', route =>
  route.fulfill({ json: { status: 'captured' } }))

Choosing What to Cover

The pyramid, and the flows that justify the cost.

Few tests, on the flows that matter
//        /\        E2E — few, slow, high confidence
//       /  \       Component — more, fast, realistic rendering
//      /____\      Unit — many, instant, precise

// Worth an E2E test — a break here costs money or trust:
//   sign up -> verify -> first lesson
//   upgrade to Pro -> payment -> feature unlocks
//   solve a problem -> submit -> progress recorded
//   admin publishes content -> it appears

// Not worth it: every form field validation, every empty state.
// Those belong in component tests, which run in milliseconds.

// Debugging tools that make E2E bearable
npx playwright test --ui          // time-travel through the run
npx playwright codegen localhost:3000
// trace: 'on-first-retry' in config — a full recording of failures,
// which is how you diagnose a test that only fails in CI.

Key Points to Remember

  • 1E2E tests catch integration, routing, auth and CSP failures that unit tests structurally cannot
  • 2Playwright locators and assertions auto-retry — an explicit waitForTimeout hides real timing bugs
  • 3Log in once and reuse the saved storage state instead of signing in in every test
  • 4Tests must not depend on each other or on data another test created; stub third-party services
  • 5Cover only the flows whose breakage costs money or trust — keep the rest in fast component tests

Interview Questions

Sign in to ask Aria
1

What does an E2E test catch that a component test cannot?

Medium
2

Why is waitForTimeout considered an anti-pattern in Playwright?

Medium
3

How do you keep an E2E suite from becoming slow and flaky as it grows?

Hard

Ask Aria about End-to-End Testing with Playwright

Your personal AI tutor — ask anything about this concept

Revision Status

Personal Notes

Sign in to save personal notes for this topic.

Discussion

Sign in to join the discussion.

Loading discussion…