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
- Next.js E2E Testing Next.js Apps with Playwright
Set up Playwright for end-to-end testing of Next.js applications. Covers configuration, writing tests, handling auth, and CI integration.
- Testing Jest vs Vitest: JavaScript Test Runners Compared
Compare Jest and Vitest for JavaScript testing. Understand speed, configuration, compatibility, and migration tradeoffs to pick the right test runner.
- Testing Visual Regression Testing with Playwright
Catch unintended UI changes automatically by comparing screenshots with Playwright's built-in visual comparison tools.
- 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.