HT
How Things Work
System Online

E2E Testing with Playwright

Driving a real browser through full user journeys — auto-waiting that kills flakiness, role-based locators that survive refactors, the Page Object Model, and CI integration.

How It Works

End-to-end tests verify complete user journeys through a real browser against the fully running system — the highest-confidence (and slowest) tests. Playwright makes them reliable with auto-waiting web-first assertions that eliminate timing flakiness, and resilient role-based locators that survive UI refactors. The Page Object Model keeps them maintainable, and they sit at the small tip of the test pyramid.

1
Test Like a Real User

End-to-end tests drive a real browser through complete user journeys — navigate, fill forms, click, and verify what's on screen — against the fully running application (frontend + backend + database). They give the highest confidence that the system actually works, because they exercise everything together.

2
Auto-Waiting Kills Flakiness

Modern web apps are asynchronous, so the classic E2E failure is timing: the test clicks before an element is ready. Playwright's web-first assertions auto-wait — they poll for the element to be visible/actionable up to a timeout — so you never sprinkle fragile, fixed waitForTimeout sleeps that either waste time or fail intermittently.

3
Resilient Locators

Tests should find elements the way a user (or screen reader) does: by role, label, text, or test-id — not by brittle CSS/XPath tied to implementation. getByRole('button', { name: 'Sign in' }) survives restyling and refactors that break .btn-primary selectors, and it nudges you toward accessible markup.

4
Page Objects & CI

The Page Object Model wraps each page's locators and actions in a class, so tests read like user stories and selectors live in one place — change the UI, fix one object. In CI, E2E suites run headless across browsers, capture traces/videos/screenshots on failure, and run in parallel — but stay small (the pyramid's tip) because they're the slowest tests.

Key Concepts

🧭End-to-End (E2E)

Tests that drive a real browser through full user journeys against the whole running system.

âģAuto-Waiting

Assertions/actions that poll until an element is ready, eliminating fixed-sleep flakiness.

✅Web-First Assertions

expect(locator).toBeVisible() etc. — they retry until the condition holds or time out.

🏷ïļRole-Based Locators

Finding elements by accessible role/label/text rather than brittle CSS selectors.

📄Page Object Model

Encapsulating a page's locators and actions in a class so tests are readable and maintainable.

🎎Trace / Video

Playwright artifacts captured on failure (DOM snapshots, network, video) for fast debugging in CI.

Playwright test + Page Object
tsx
1import { test, expect } from "@playwright/test";
2
3test("user can sign in and see the dashboard", async ({ page }) => {
4 await page.goto("/login");
5
6 // Query by ACCESSIBLE role/label — robust to markup changes,
7 // not brittle CSS selectors like ".btn-primary > span:nth-child(2)"
8 await page.getByLabel("Email").fill("a@b.com");
9 await page.getByLabel("Password").fill("secret");
10 await page.getByRole("button", { name: "Sign in" }).click();
11
12 // Web-first assertion AUTO-WAITS: polls until visible (or times out).
13 // No page.waitForTimeout(800) guessing → no flakiness.
14 await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
15});
16
17// Page Object Model keeps tests readable and selectors in one place:
18class LoginPage {
19 constructor(private page: Page) {}
20 async signIn(email: string, pw: string) {
21 await this.page.getByLabel("Email").fill(email);
22 await this.page.getByLabel("Password").fill(pw);
23 await this.page.getByRole("button", { name: "Sign in" }).click();
24 }
25}
ðŸ’Ą
Why This Matters

E2E tests are the ultimate proof that critical flows (signup, checkout, login) actually work for users, and Playwright has become the modern standard. Knowing how auto-waiting eliminates flakiness, why role-based locators matter, and how to keep E2E suites small and maintainable is increasingly expected of full-stack and QA engineers.

Common Pitfalls

⚠Fixed sleeps (waitForTimeout) instead of auto-waiting assertions — the #1 source of flaky E2E tests.
⚠Brittle CSS/XPath locators tied to markup, so restyles cause mass false failures.
⚠Too many E2E tests (an inverted pyramid) — slow CI, hard to debug; push logic down to unit/integration.
⚠Tests that depend on each other or on shared/production data, causing order-dependent flakiness.
⚠Not capturing traces/videos on failure, making intermittent CI failures painful to diagnose.
Real-World Use Cases

1The Flaky Suite Nobody Trusted

Scenario

An older Selenium E2E suite failed randomly ~15% of CI runs. Engineers learned to just hit 'retry', real failures hid among the noise, and eventually the suite was disabled — leaving the critical paths untested.

Problem

The tests relied on fixed sleeps (Thread.Sleep / waitForTimeout) to wait for async UI. Too short caused failures on slow runs; too long made the suite crawl. The flakiness destroyed trust in the entire safety net.

Solution

Rewrite the critical journeys in Playwright using auto-waiting web-first assertions and role-based locators, with traces captured on failure. Tests wait exactly as long as needed and no more, eliminating timing flakiness, and the suite becomes fast and reliable enough to gate deploys again.

ðŸ’Ą

Takeaway: Flaky E2E tests are worse than none — they erode trust. Auto-waiting (not fixed sleeps) and resilient locators are what make a browser suite reliable enough to actually depend on.

2Brittle Selectors Broke on Every Restyle

Scenario

E2E tests located elements by CSS like div.card > button.btn-primary. A routine design refactor that changed class names broke 40 tests overnight, even though the app worked perfectly for users.

Problem

Tests were coupled to implementation details (CSS structure), so cosmetic changes caused mass false failures. Maintenance cost ballooned and discouraged UI improvements.

Solution

Switch to user-facing locators — getByRole, getByLabel, getByText, or stable data-testid — and centralize them in Page Objects. Now restyling doesn't touch behavior-based locators, and when markup truly changes, fixing one Page Object updates every test that uses it.

ðŸ’Ą

Takeaway: Locate elements the way users and screen readers do (role/label/text), not by CSS structure. Resilient locators plus Page Objects make E2E tests survive UI refactors and slash maintenance.

Join the discussion

Comments

?

Loading comments...

Cookie preferences

We use essential cookies for sign-in and preferences. With your permission, we also use basic analytics to improve lessons.