End-to-End Testing with Playwright: A Practical Tutorial
Learn how to write reliable end-to-end tests with Playwright, including selectors, fixtures, auto-waiting, and patterns that avoid flakiness.
What you'll learn
- ✓What end-to-end testing actually verifies
- ✓How Playwright drives real browsers
- ✓Writing your first resilient test
- ✓Using fixtures and auto-waiting
- ✓Avoiding flakiness with stable selectors
Prerequisites
- •Familiar with testing concepts
- •Basic JavaScript or TypeScript
What and Why
End-to-end (E2E) tests exercise your application the way a user does: through a real browser, clicking buttons, filling forms, and checking what appears on the screen. Unit tests verify functions in isolation; integration tests verify modules talking to each other; E2E tests verify the whole stack — frontend, backend, network, and database — works together.
Playwright is a modern E2E framework from Microsoft that drives Chromium, Firefox, and WebKit through a single API. It is fast, has built-in auto-waiting, and gives you tools like tracing and codegen that dramatically reduce flakiness.
Mental Model
Think of Playwright as a remote-controlled browser. Your test script sends commands (“click this”, “type that”), and Playwright waits for the page to be in a sensible state before each action. There are three core objects you will use constantly:
- Browser: a long-lived process (Chromium, Firefox, WebKit).
- Context: an isolated session inside the browser, like a fresh incognito window.
- Page: a single tab inside a context.
Each test typically gets its own context, which means cookies, localStorage, and cache are clean. This isolation is why Playwright tests can run in parallel without stepping on each other.
Hands-on Example
Install Playwright with npm init playwright@latest. Then write a test that logs into a demo app:
import { test, expect } from '@playwright/test';
test('user can log in and see dashboard', async ({ page }) => {
await page.goto('https://demo.example.com/login');
await page.getByLabel('Email').fill('jane@example.com');
await page.getByLabel('Password').fill('hunter2');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
Run it with npx playwright test. Playwright opens a real browser, runs the steps, and reports pass or fail.
Test Runner
|
v
Browser
/ \
Context Context <- isolated sessions
| |
Page Page <- tabs / actions
| |
Assert Assert Notice the selectors: getByLabel and getByRole. These are accessibility-first locators. They match what a screen reader sees, not what a CSS class is called. That makes tests survive refactors.
Common Pitfalls
- CSS selectors that break on refactor: avoid
.btn-primary-large-v2. Prefer role, label, ordata-testid. - Manual
waitForTimeout(2000): arbitrary sleeps cause flaky tests. Useexpect(...).toBeVisible()instead — it polls automatically. - Shared state between tests: log in once, share session via storage state; do not let test A leave the cart full for test B.
- Testing third-party UIs you do not control: mock or stub them at the network layer with
page.route. - Running E2E on every commit: they are slower. Run a smoke subset on commit, the full suite nightly or pre-release.
Practical Tips
Use npx playwright codegen to record interactions and produce a starter script — then clean it up. Enable tracing with --trace on-first-retry so failed runs give you a time-travel view of the page. Tag tests (@smoke, @regression) so CI can target subsets. Keep each test under fifteen seconds; longer tests are usually doing too much. And run tests against a deployed preview environment, not localhost, so you catch real-world configuration issues.
Wrap-up
Playwright takes most of the historical pain out of E2E testing: auto-waiting prevents timing flakes, accessibility selectors survive refactors, and traces make debugging tractable. Start with one critical user journey — sign up, checkout, or whatever drives revenue — and grow from there. A small, reliable E2E suite beats a huge, flaky one every day.
Related articles
- Testing Testing Pyramid: Unit, Integration, E2E
What the testing pyramid actually means in modern apps, when to deviate, and how to keep each layer giving you the value it is supposed to.
- Testing Vitest Tutorial
A practical guide to Vitest, the testing framework built for modern JavaScript projects. Setup, syntax, mocking, coverage, and how it differs from Jest in ways that matter.
- Testing End-to-End Testing with Playwright
A practical guide to end-to-end testing with Playwright, covering installation, writing your first test, selectors, auto-wait, and running the suite in CI.
- Testing Property-Based Testing: An Introduction
Stop writing one example per test. Property-based testing generates inputs for you and finds the edge cases you would never think to write.