Playwright for E2E Testing: A Practical Guide for Frontend Developers
Learn end-to-end testing with Playwright — writing reliable tests, handling authentication, testing across browsers, and integrating with CI/CD pipelines.
A practical guide to the testing pyramid — what unit, integration, and end-to-end tests actually do, when to write each, and how to automate them in CI without slowing your team down.
By codefunc
Manual QA does not scale. Neither does a test suite that takes forty minutes and flakes every Tuesday. Effective quality assurance in 2026 is layered automation: fast tests that run on every save, broader tests that run on every pull request, and a thin slice of end-to-end tests that prove critical user journeys still work in a real browser.
Bottom line: Write many unit tests for pure logic, some integration tests for modules working together, and few E2E tests for paths that earn revenue or prevent disasters. Automate all three in CI — but keep the pyramid shape or feedback gets slow and teams start skipping runs.
Quality assurance asks: does the product meet requirements and work for users?
Test automation asks: can a machine verify that repeatedly, cheaply, and on every change?
Automation does not replace exploratory testing, design review, or accessibility audits. It prevents regressions — the bug you fixed in March that returned in October because nobody re-checked manually.
| Activity | Human | Automated |
|---|---|---|
| Exploratory testing | ✓ | ✗ |
| Regression on every PR | impractical | ✓ |
| Edge case matrix (100 inputs) | tedious | ✓ |
| "Does this feel right?" UX | ✓ | ✗ |
| Build/deploy integrity | ✗ | ✓ |
Mike Cohn's testing pyramid still holds: many small fast tests at the base, fewer slow tests at the top.
flowchart TB
subgraph pyramid [Test pyramid]
E2E["E2E — few, slow, high confidence"]
INT["Integration — some, medium speed"]
UNIT["Unit — many, fast, cheap"]
end
UNIT --> INT --> E2E
| Layer | Speed | Cost | Confidence scope |
|---|---|---|---|
| Unit | milliseconds | low | One function or class |
| Integration | seconds | medium | Several modules + I/O boundaries |
| E2E | minutes | high | Full app in browser |
Anti-pyramid (inverted cone) — mostly E2E, few unit tests — is the main reason teams abandon CI. Fix the base first.
Unit tests exercise a single unit of code in isolation — usually one function or method — with dependencies mocked or stubbed.
| Property | Meaning |
|---|---|
| Fast | Entire suite under a few seconds |
| Deterministic | Same result every run — no network roulette |
| Isolated | No real database, filesystem, or HTTP |
| Readable | Name describes behavior: rejects empty format input |
Developer tool sites benefit enormously from unit tests because core value lives in pure functions — formatters, parsers, encoders — not in exotic infrastructure.
import { describe, it, expect } from "vitest";
import { formatJson } from "../json";
import { expectErr, expectOk } from "./helpers";
describe("formatJson", () => {
it("formats json with indentation", () => {
expectOk(formatJson('{"a":1}'), "\n");
});
it("rejects empty input", () => {
expectErr(formatJson(""), "Paste JSON");
});
});
On codefunc, logic lives in lib/tools/logic/ and runs with Vitest + happy-dom when DOM APIs are needed. npm test runs the full unit suite in seconds — suitable for pre-commit and every PR.
Skip unit testing presentational components with no logic on day one. Test the behavior users depend on.
| Anti-pattern | Problem |
|---|---|
| Testing implementation details | Refactors break tests without user-visible bugs |
| Mocking everything | Test proves mocks work, not production code |
| One assertion per file obsession | Verbose without clarity |
| Snapshot of 4,000 lines of HTML | Unreviewable diffs; mute failures |
| No tests for error paths | Happy path only — prod fails on empty input |
Use the JSON Validator and Regex Tester locally to explore edge cases before encoding them as test fixtures.
Integration tests verify that multiple parts work together — modules, database layer, file system, HTTP client against a test server, or React component with real child providers.
| Boundary | Example |
|---|---|
| Logic + file I/O | Blog post parser reading markdown frontmatter |
| API route + validation | POST handler rejects malformed body |
| Component + state | Search dialog opens and filters tools list |
| Package + dependency | Cron parser library with your wrapper |
| Build pipeline | next build generates all static pages |
If removing a mock would make the test fail because real collaboration is under test — it is integration.
If every dependency is fake and only one function's return value matters — it is unit.
An integration test for a static blog might read a real fixture file and assert parsed metadata:
import { describe, it, expect } from "vitest";
import { getPostBySlug } from "../posts";
describe("blog posts", () => {
it("parses frontmatter and computes reading time", () => {
const post = getPostBySlug("how-to-format-json-safely");
expect(post?.title).toBeTruthy();
expect(post?.readingTimeMinutes).toBeGreaterThan(0);
expect(post?.category).toBe("guides");
});
});
This tests gray-matter + file read + slug resolution together — not any single function in isolation.
For Next.js SSG sites, npm run build is one of the highest-value integration tests:
generateStaticParams slug resolvesIf build fails in CI, you caught a class of bugs no unit test touched — missing registry entry, typo in slug, edge runtime on static export.
| Anti-pattern | Problem |
|---|---|
| Shared mutable DB state | Order-dependent flakes |
| Hitting production APIs | Rate limits, data corruption, flakes |
| Testing entire app without boundaries | Slow; duplicates E2E |
| No cleanup between tests | Polluted state |
Use test containers, in-memory SQLite, or fixture files. Reset state per test.
End-to-end (E2E) tests drive the application like a user — typically in a real browser via Playwright or Cypress. They click, type, navigate, and assert on visible outcomes.
| Cost | Detail |
|---|---|
| Time | 30s–5min per spec |
| Flakiness | Timing, animations, network, test data |
| Maintenance | UI selectors break when design changes |
| CI resources | Browser binaries, parallelization complexity |
That is why the pyramid says few E2E tests — only for journeys that justify the price.
Pick 5–10 flows, not 500:
| Priority | Flow |
|---|---|
| P0 | Homepage loads, core navigation visible |
| P0 | Primary conversion path (signup, checkout, tool use) |
| P1 | Search or command palette finds a result |
| P1 | One representative tool: input → output → copy |
| P2 | Blog post renders, internal links work |
| P2 | Legal pages, 404 page |
For a developer tools catalog, a strong minimal E2E suite:
/tools/json-formatterYou do not need E2E for all 72 tools if unit tests cover each formatter's logic.
import { test, expect } from "@playwright/test";
test("json formatter transforms input", async ({ page }) => {
await page.goto("/tools/json-formatter");
await page.getByRole("textbox", { name: /input/i }).fill('{"a":1}');
await expect(page.getByRole("textbox", { name: /output/i })).toContainText('"a"');
});
test("command search opens with keyboard", async ({ page }) => {
await page.goto("/");
await page.keyboard.press("Control+K");
await expect(page.getByRole("dialog")).toBeVisible();
});
Prefer accessible selectors (getByRole, getByLabel) over CSS classes — they survive refactors and align with accessibility best practices.
| Anti-pattern | Problem |
|---|---|
| 200 E2E tests, 10 unit tests | 45-minute CI, random failures |
sleep(3000) everywhere |
Flaky; use auto-wait assertions |
| Testing third-party widgets | You do not control Google Analytics |
| Same flow in 4 browsers on every commit | Save cross-browser for nightly runs |
| Hard-coded prod URLs | Never mutate production from tests |
Run E2E against preview deploys or local npm run start after build — not live production.
| Question | Unit | Integration | E2E |
|---|---|---|---|
| Scope | One function | Several modules | Whole application |
| Dependencies | Mocked | Real (or test doubles at boundary) | Real stack |
| Speed | ⚡ ms | 🔄 seconds | 🐢 minutes |
| Debug ease | Pinpoints line | Narrow subsystem | "Something broke" |
| Best for | Algorithms, validation | APIs, build, wiring | Critical user paths |
| Typical tools | Vitest, Jest | Vitest, Supertest | Playwright, Cypress |
Automate regression; keep humans for:
Exploratory findings become new automated tests so bugs stay dead.
flowchart LR
A[Push / PR] --> B[Lint]
B --> C[Unit tests]
C --> D[Integration tests]
D --> E[Build]
E --> F{Main branch?}
F -->|PR only| G[Done]
F -->|Main / nightly| H[E2E smoke]
H --> I[Deploy]
| Stage | Runs on | Fail merge? |
|---|---|---|
| Lint + typecheck | Every PR | Yes |
| Unit tests | Every PR | Yes |
| Integration (+ build) | Every PR | Yes |
| E2E smoke | Main, nightly, or pre-release | Yes for release |
| Full E2E cross-browser | Nightly | Alert, not block |
Standard release command for many frontend repos:
npm run lint && npm test && npm run build
Add playwright test when E2E exists — not before the base is green.
node_modules and Playwright browsers| Layer | Data strategy |
|---|---|
| Unit | Inline strings, smallest possible input |
| Integration | Fixture files in __fixtures__/ |
| E2E | Seed script or API factory; reset DB per run |
Generate realistic JSON payloads for fixtures with the Fake Data Generator. Compare expected vs actual output with the Text Diff tool when debugging failing assertions.
Store golden files for complex output only when humans review changes intentionally — not for volatile timestamps or IDs.
npm run test:coverage helps find untested modules — not prove quality.
| Coverage % | Reality |
|---|---|
| 0% on logic folder | Dangerous for tool sites |
80% on lib/tools/logic/ |
Strong for formatters |
| 100% line coverage mandate | Encourages useless tests |
Cover branches that handle errors and public APIs. Ignore generated files, config, and thin re-exports.
| Layer | Focus |
|---|---|
| Unit | All lib/tools/logic/ |
| Integration | Blog parser, metadata, build |
| E2E | 3–5 smoke journeys |
| Manual | New tool UX, a11y |
| Layer | Focus |
|---|---|
| Unit | Business rules, pricing, permissions |
| Integration | API routes, DB, webhooks |
| E2E | Login, checkout, core feature |
| Manual | Edge cases, email delivery |
| Layer | Focus |
|---|---|
| Unit | Utility functions |
| Integration | Components + providers (Vitest + Testing Library) |
| E2E | Visual regression (Storybook, Chromatic) or key docs pages |
npm test scriptnpm run build)| Symptom | Fix |
|---|---|
| "CI is red, merge anyway" | Block merges; fix trust |
| Tests nobody reads when they fail | Delete or fix — noise kills culture |
| QA team only, devs never write tests | Quality is everyone's job |
| Testing after release crunch | Shift left — test with feature |
| E2E only because "it's real" | Add unit tests for debuggability |
Unit tests protect logic. Integration tests protect connections. E2E tests protect journeys. Automate all three, but respect the pyramid — fast feedback on every commit, deep confidence before release.
Start where bugs hurt most: validators and parsers for tool sites, auth and payments for SaaS. Let build be your integration gate. Add browser smoke tests when the cost of a broken homepage exceeds an hour of Playwright maintenance.
Good QA is not more manual checklists. It is machines repeating what you already learned — so you never learn the same bug twice.
Learn end-to-end testing with Playwright — writing reliable tests, handling authentication, testing across browsers, and integrating with CI/CD pipelines.
When to use XPath vs CSS selectors, how to write stable queries, and a browser-side workflow for testing against real HTML without shipping brittle scrapers.
How to count words and characters the way products and platforms do — limits, diffs, Markdown pitfalls, and a practical editing workflow for developers who ship copy.
Find a developer tool