Skip to content
C Codeloom
Testing

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.

·4 min read · By Codeloom
Intermediate 7 min read

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
Playwright test execution flow

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, or data-testid.
  • Manual waitForTimeout(2000): arbitrary sleeps cause flaky tests. Use expect(...).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.