End-to-End Testing
Driving the whole application like a real user to verify complete flows
Overview
End-to-end (E2E) tests exercise your application the way a real user would: opening a page, clicking buttons, and checking what appears on screen. They cover the full stack — UI, server, and database — so they catch problems no isolated test can. E2E tests are the slowest and fewest in a suite, reserved for critical user journeys like sign-up and checkout.
Syntax / Usage
Tools like Playwright launch a real browser and interact with the page. You navigate, act, and assert on visible results.
const { test, expect } = require("@playwright/test");
test("user can log in and see the dashboard", async ({ page }) => {
await page.goto("https://localhost:3000/login");
await page.getByLabel("Email").fill("ada@example.com");
await page.getByLabel("Password").fill("correct-horse");
await page.getByRole("button", { name: "Sign in" }).click();
await expect(page).toHaveURL(/.*dashboard/);
await expect(page.getByRole("heading", { name: "Welcome, Ada" })).toBeVisible();
});
Run with npx playwright test. The browser can run headless in CI.
Examples
Asserting a validation error appears for bad input:
test("shows an error for wrong password", async ({ page }) => {
await page.goto("https://localhost:3000/login");
await page.getByLabel("Email").fill("ada@example.com");
await page.getByLabel("Password").fill("wrong");
await page.getByRole("button", { name: "Sign in" }).click();
await expect(page.getByText("Invalid credentials")).toBeVisible();
});
Checking that a new item persists after being created:
test("adding a todo shows it in the list", async ({ page }) => {
await page.goto("https://localhost:3000/todos");
await page.getByPlaceholder("New todo").fill("Write E2E tests");
await page.getByRole("button", { name: "Add" }).click();
await expect(page.getByText("Write E2E tests")).toBeVisible();
});
Common Mistakes
- Writing E2E tests for every case instead of only the critical flows
- Using fixed
sleepdelays instead of waiting for elements, causing flakiness - Selecting elements by fragile CSS classes rather than roles or labels
- Sharing state between tests so one run pollutes the next
- Running against a shared live environment instead of a clean test instance
See Also
testing-integration-tests testing-fundamentals testing-code-coverage