Why E2E tests and where they fit

An end-to-end (E2E) test starts the whole application in a real browser and does what a user does: opens a page, types, clicks, reads the result. It catches problems that unit tests cannot see: a broken script path, a form that is not wired to its handler, a button covered by another element.

The test pyramid: many unit tests, some integration tests, few E2E tests
The test pyramid: many unit tests, some integration tests, few E2E tests
KindWhat it checksSpeedTypical tool
UnitOne function or classmillisecondsvitest
IntegrationSeveral modules or a database togetherhundreds of millisecondsvitest, supertest
E2EThe whole app in a browserseconds@playwright/test

The higher the level, the more realistic the test and the more expensive it is: slower, harder to debug and more likely to fail for reasons unrelated to your code. That is why the shape is a pyramid. Test every branch of the logic with unit tests (see Unit tests with Vitest), and keep E2E tests for a handful of critical paths: sign up, add to cart, pay.

💡 A good E2E candidate is a flow that makes or loses money or that would be embarrassing to break. A tooltip colour is not.


Installing Playwright and configuring it

Playwright is a test runner (@playwright/test) that drives Chromium, Firefox and WebKit. Install it as a dev dependency and download a browser:

bash
npm init -y
npm i -D @playwright/test http-server
npx playwright install chromium

The configuration lives in playwright.config.js:

javascript
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  retries: process.env.CI ? 2 : 0,
  forbidOnly: !!process.env.CI,
  use: {
    baseURL: 'http://localhost:4173',
    trace: 'on-first-retry',
  },
  webServer: {
    command: 'npx http-server ./public -p 4173 -s',
    url: 'http://localhost:4173',
    reuseExistingServer: !process.env.CI,
  },
});
  • testDir is where the *.spec.js files live.
  • webServer starts the app before the tests and waits until url answers; reuseExistingServer lets you keep a dev server running locally.
  • baseURL lets tests write page.goto('/').
  • retries re-runs a failed test, but only on CI. forbidOnly fails the build if someone leaves test.only in the code.