Building Self-Contained Front-End Apps: A Testing-First Architecture

testingfrontendplaywrightarchitecturemswintegration-testing

How a Three-Tier Testing Strategy Changed the Way We Think About Frontend Independence

There’s a moment every frontend team eventually hits: you’ve written hundreds of unit tests, your component library is well-covered, but your CI is still bottlenecked on a slow backend-dependent test suite that takes twenty minutes and breaks on unrelated server changes.

We hit that moment building a multi-step wizard — a product with complex navigation state, multi-page flows, and a backend that was evolving in parallel. The combination broke our existing testing strategy in ways that were hard to paper over.

Fixing it forced us to rethink what “self-contained” really means for a frontend app.


The Problem With Two Extremes

Most frontend teams end up with a testing setup that looks like this:

jsdom doesn’t execute real CSS, doesn’t handle layout, and can’t test browser-native behaviors like:

A field accidentally set to display: none would be unusable in production but completely invisible to a jsdom-based test.

So teams reach for E2E tests:

But every E2E test requires:

For us, that meant 120-180 seconds per test — expensive enough that we could only afford a small number of critical-path tests.

Worse, our E2E tests lived in the backend repository, so a UI selector change could block unrelated backend merges.

The result:

And the middle ground is where things actually break:


The Insight: The Browser Is the Variable

When we diagnosed what was difficult to test, the common thread wasn’t the backend — it was the browser.

Most flows didn’t need real API responses. They just needed some API responses so the app could render and navigate.

That reframing led to a middle tier.

Integration Tests

The browser is real:

But no server exists.

Playwright wasn’t replacing E2E testing — it was filling a missing tier.


One Mock System for Both Tiers

A natural concern emerged.

If:

then every API contract exists in two places.

We solved this with an MSW adapter: a thin bridge converting MSW-style handlers into Playwright route handlers.

This allowed the same mock definitions to work in both environments.

Testing Layers

TierEnvironmentBackendSpeed
UnitNode.js / jsdomMSW mocksFast
IntegrationReal browserMSW mocksMedium
E2EReal browserReal APIsSlow

The practical effect:


What Self-Contained Actually Means

Self-contained doesn’t mean isolated from reality.

The frontend still understands:

What it doesn’t need is a running backend to function.

In practice this means:

VITE_ENABLE_MOCKING=true yarn dev
npx playwright test

A new engineer can clone the repo and run the app immediately — realistic states, realistic failures, no database provisioning.

That meaningfully changes onboarding velocity.


Feature Flags Without a Backend

In production, feature flags often arrive through backend session payloads.

In a self-contained environment, there is no session.

Playwright’s addInitScript() solved this by injecting flags into the browser before the app booted.

That enabled deterministic testing of:

without requiring:


A Pitfall: Over-Mocking in Unit Tests

This architecture also exposed a common anti-pattern: trying to unit test heavily connected features.

The result is often:

These tests pass because the mocks are correct — not because the feature works.

In many cases, one realistic Playwright integration test replaced five to ten fragile unit tests.


Placement Heuristic

Use Integration Tests (Playwright) For

Use Unit Tests For


The Payoff

After adding the integration tier:

But the deeper shift was conceptual.

The frontend became treated as a first-class system:

Self-contained doesn’t mean disconnected from reality.

It means the application knows how to stand on its own.