█
LastWrite
  • > Curriculum
  • > Pricing
  • > For Educators
  • > About
  • > Contact
Log InGet Started

Questions, concerns, bug reports, or suggestions? We read every message, write to us at [email protected].

More ways to reach us →
LastWrite

Structured computer science lessons for aspiring developers and security professionals.

[email protected]

(201) 785-7951

Mon–Fri, 9 AM–5 PM EST

Learn

  • Curriculum
  • Pricing

Company

  • About
  • For Educators & Schools
  • Contact Us

Legal

  • Terms of Service
  • Privacy Policy
© 2026 LastWrite. All rights reserved.
Curriculum/Software Engineering/Testing/End-to-End Testing with Playwright
50 minIntermediate

End-to-End Testing with Playwright

After this lesson, you will be able to: Automate a browser with Playwright to test a real user flow end to end, and run those tests headless in CI.

End-to-end tests sit at the top of the pyramid: they drive the actual app the way a user does, clicking through a browser. Playwright is the modern standard. This lesson writes a real e2e test for a login-to-dashboard flow and covers why these tests are powerful but kept few.

Prerequisites:Integration Testing

What e2e tests are for (and why you keep them few)

An e2e test launches a real browser, navigates your app, fills forms, clicks buttons, and asserts on what the user sees. It is the only layer that proves the whole stack works together: frontend, backend, database, auth. That power costs speed and stability: e2e tests are slow (tens of seconds) and the most prone to flakiness (timing, animations, network). So you write a handful for the critical journeys (sign up, log in, checkout) and rely on unit and integration tests for everything else.

A Playwright test for the login journey

Playwright auto-waits for elements, which removes most of the flaky sleep() calls older tools needed.

python
// e2e/login.spec.ts
import { test, expect } from '@playwright/test';
test('user can log in and reach the dashboard', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('correct-horse-battery');
await page.getByRole('button', { name: 'Sign in' }).click();
// Auto-waits for navigation + the heading to appear.
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
await expect(page).toHaveURL(/\/dashboard/);
});
test('wrong password shows an error', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('wrong');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Invalid email or password')).toBeVisible();
});
// Setup: npm init playwright@latest Run: npx playwright test

Select like a user, not like a developer

Prefer role- and label-based selectors (getByRole, getByLabel, getByText) over CSS classes or test ids tied to structure. They survive refactors and assert that the app is actually accessible, which is what a real user (and a screen reader) relies on. Reserve a data-testid only when no accessible handle exists. Selecting by brittle CSS paths is the number-one source of e2e flakiness.

💡 Run headless in CI, headed when debugging

In CI, e2e tests run headless (no visible browser) for speed. When a test fails mysteriously, run it headed (npx playwright test --headed) or open the Playwright trace viewer, which records a step-by-step timeline with DOM snapshots so you can see exactly where it diverged.

Quick Check

Why prefer getByRole('button', { name: 'Sign in' }) over a CSS selector like .btn-primary?

Pick the best answer.

Sign in and purchase access to unlock this lesson.

Sign in to purchase
←Integration Testing
Back to Testing
Test-Driven Development→