TestingGENERALFRAMEWORK-SPECIFICSIMPLIFIED

Choosing the Test Level

Pure logic, component behaviour, critical user flow, visual appearance and accessibility are five different observations. The level is chosen by what the failure would look like, not by a pyramid.

The intent, the obvious build, and why it breaks

Every lesson starts where the work starts: someone wanted an outcome, and the first implementation that comes to mind has a problem.

The question

Which kind of test would actually have caught this bug, and what is that kind of test structurally unable to see?

The user intent

Someone wants to change a product without breaking the parts they were not thinking about, and wants to know before shipping whether the thing they changed still works for a person actually using it.

The obvious build

Write tests. Chase a coverage number, put most of them at the bottom of the pyramid because those are fast, and add a handful of end-to-end tests over the whole app because those are the realistic ones.

Why it breaks

Coverage counts lines executed, not behaviours proven. A test that mounts every component and asserts nothing produces a high number and catches nothing; the two are not related in the direction people assume.

How it breaks in a real browser
  • Coverage counts lines executed, not behaviours proven. A test that mounts every component and asserts nothing produces a high number and catches nothing; the two are not related in the direction people assume.
  • The pyramid is a cost heuristic. It ranks tests by how cheap they are to run, and says nothing about which failure each level can observe — which is the only question that matters when a bug reaches a user.
  • The frontend bugs that reach users are disproportionately the ones a unit test cannot structurally see: a submit control that is unreachable by keyboard, a stylesheet change that pushed it below the fold, a cached response from the previous user still on screen after a login.
  • "Add end-to-end tests for realism" produces a suite that takes tens of minutes and fails weekly for reasons unrelated to any change. Engineers learn to press re-run, and the level with the most signal becomes the level nobody trusts.
  • Choosing the level by habit produces two failures at once: the same rule tested three times at three levels, and whole categories — appearance, keyboard operability, announcement — tested nowhere at all.
IntentEventStateUI LogicDOM WorkNetworkLayout / PaintPixelsFeedback

What is actually happening

In the browser, not in the framework.

  • Each level observes a different slice of the chain from intent to pixels (The Frontend Reasoning Loop). That slice, not the tool, is what determines what the test can prove.
  • Pure logic runs a function in a JavaScript runtime with no document at all. It observes return values and thrown errors. It cannot observe anything about rendering, because nothing renders.
  • Component mounts real DOM — in a simulated document or a real browser — dispatches events, and asserts on what is visible and reachable. It observes structure, semantics and behaviour (The Accessibility Tree).
  • End-to-end drives a real browser across routes, storage, auth and network. It observes integration: the parts that only exist when the pieces are wired together (Client-Side Routing).
  • Visual regression renders and compares pixels. It is the only level that observes the output of paint (The Rendering Pipeline), which is exactly the output no assertion describes.
  • Automated accessibility checks read the accessibility tree and apply rules to it. They observe the tree, not the experience of operating the interface.
  • The trade is always the same: the more of the real system a test contains, the more real failures it can observe, and the more unrelated ways it has to fail.

What this makes the browser do

And which of it is avoidable.

  • A pure-logic test makes the browser do nothing — there is usually no browser. No parse, no style, no layout, no paint.
  • A component test in a simulated document builds a DOM tree and computes nothing else. Simulated DOM implementations generally do not run layout, so geometry queries return zeros and anything that depends on measured size is untestable there (Layout Thrashing).
  • A component test in a real browser runs the whole pipeline per test — style, layout, paint — which is why the same assertions cost orders of magnitude more machine time in that mode.
  • An end-to-end test runs a full browser plus whatever stack it points at, then does it again for each retry. Suite cost scales with flake, not just with test count.
  • A visual regression test must force the pipeline to be deterministic: pinned fonts, disabled animation, fixed device pixel ratio, frozen clocks. Every one of those is browser work you are deliberately suppressing (The Viewport and Device Pixels).

What each level can actually prove

The useful way to hold this is not "unit, integration, e2e" but "what does this test have in the room with it". A pure function test has the function. A component test has a document. An end-to-end test has a browser and a server. Each addition buys a class of observable failure and costs determinism.

The column that people skip is the third one. Knowing what a level *cannot* see is what stops a team from concluding, after a green suite, that a flow works — when in fact nothing in the suite has ever operated it by keyboard or looked at it.

LevelHas in the roomCan observeCannot observeFails by
Pure logicA function and its inputsReturn values, thrown errors, invariants, boundariesAnything rendered, any event, any style, any network callBeing written against code that was never extracted, so it does not exist
ComponentA document, real events, the accessibility treeRendered output, roles and names, interaction behaviour, escapingLayout geometry in a simulated DOM, real navigation, appearance, cross-page stateAsserting on internals, so a refactor turns it red without a behaviour change
End-to-endA real browser, routing, storage, network, a backendIntegration, auth, navigation, real request and response handlingReal devices, real networks, assistive technology, visual detailFlake — timing, animation, shared state — until the red is ignored
Visual regressionRendered pixels and a stored baselineAppearance: spacing, colour, overflow, truncation, z-order, regressions no assertion describesWhether anything works; a broken button that looks identical passesFalse positives from fonts, animation and dynamic content, then baseline churn
Accessibility (automated)The accessibility tree and a rule setMissing names, invalid ARIA, contrast, structural rule violationsFocus order, whether names are meaningful, announcement quality, operabilityBeing mistaken for the whole of accessibility testing

Choosing from the failure, not from the pyramid

Write the failure down in a sentence a non-engineer would understand — "the total is wrong for orders with a coupon", "the dialog opens but you cannot close it with the keyboard", "the price is hidden behind the image on a narrow screen" — and the level is usually already implied. Three different sentences, three different instruments.

When two levels can both observe a failure, take the cheaper one, and take it at the level where the fix is closest. A contrast failure caught in a component test points at one component; the same failure caught in an end-to-end run points at a page.

A change is about to ship. What proves it still works?

What would the failure look like to the person using this?

Pure logic test

when The failure is a wrong value: a total, a date, a sort order, a page boundary, a validation verdict.

cost Requires the logic to be reachable without a component, which sometimes means an extraction you would not otherwise do.

Component test

when The failure is behavioural: an interaction does not produce the change a person can see, an error is not shown, a control is not reachable by its name.

cost Slower than pure logic, and in a simulated document it can tell you nothing about geometry or appearance.

End-to-end test

when The failure only exists when the pieces are joined: auth, routing, real requests, storage that survives a navigation — and the flow is one you cannot ship broken.

cost The most expensive test you own, in machine time and in maintenance, and the one that will be flaky.

Visual regression

when The failure is appearance itself, and appearance is the deliverable: a design system component, a chart, an invoice, an email.

cost Baseline management forever, plus a false-positive stream from fonts, animation and dynamic data.

Automated rules plus a manual pass

when The failure is that someone cannot operate the interface — with a keyboard, with a screen reader, at high zoom.

cost The automated half is cheap and partial; the manual half is real, recurring human time that has to be scheduled (Accessibility Testing).

What a badly levelled suite feels like

These are the symptoms, and each one is a level mismatch rather than a lack of discipline. The team is usually already trying hard; they are trying hard at the wrong altitude.

Notice that each response is a *move*, not an addition. A suite improves as much from deleting a test at the wrong level as from writing one at the right level.

Symptom, cause, move
TriggerSymptomCauseResponse
Pull request feedback takes half an hourEngineers push and switch tasks; failures are read hours laterLogic-shaped assertions are being run through a browser and a loginMove the assertions down to the level that can see them and keep the browser for flows that need one.
The suite goes red on a refactor with no behaviour changeHours spent updating tests that found nothingTests assert internal state, render counts or DOM structure rather than what a person can perceiveRewrite them as given / when / then against visible behaviour (Component Testing).
Someone says "just re-run it"Red is no longer information; genuine failures shipFlake was treated as a property of the tool rather than as a race the test surfacedQuarantine and diagnose the flakiest tests as bugs, and fix the waiting strategy rather than the retry count (End-to-End Testing).
A visual bug ships with a green suiteOverlapping text, truncation, or a control off screenNo level in the suite has ever looked at a pixel; a simulated DOM does not lay outAdd visual coverage narrowly where appearance is the contract (Visual Regression Testing).
A keyboard user reports a flow is impossibleFocus is lost, or a control cannot be reached at allEvery test drove the UI with synthetic clicks, which never exercise focus orderDrive one critical flow by keyboard in the component layer and in the end-to-end layer (Keyboard Operability).
Coverage rose, escaped defects did not fallA metric improving while the product does notThe gate rewarded executing lines, and tests were written to satisfy itReplace the gate with escaped-defect attribution: which level should have caught last month's bugs.

How to build it

Most important first.

  • Start from the failure, not the level. Write one sentence describing what would go wrong for a person, then ask which level can observe that sentence being false. The level falls out of the sentence.
  • Push logic out of components so the cheapest level can reach it. Most of what people write component tests for is arithmetic, formatting and branching that never needed a DOM (Testing Pure Logic).
  • Make component tests the default for anything a person interacts with, and query the way a person perceives it — by role and accessible name (Component Testing).
  • Reserve end-to-end tests for a short, named list of flows whose failure is unacceptable, and defend the shortness of that list against everyone who wants to add to it (End-to-End Testing).
  • Add visual regression only where appearance is the contract: a design system, a chart, a printable document, an email template (Visual Regression Testing, Design Systems).
  • Run automated accessibility rules at the component level where they are cheapest to fix, and schedule the manual keyboard and screen-reader pass that those rules cannot replace (Accessibility Testing).
  • Treat the suite as a product with a maintenance budget. A test nobody trusts is worse than no test, because it costs time and produces no signal.

Keyboard, focus, semantics, announcement

A required field on every lesson in this domain, not a section added when there is room.

  • The level choice is an accessibility decision. Querying by role and accessible name means a component test fails the moment the accessible name is lost — a free regression test for the thing most likely to silently break (Semantics Before ARIA).
  • Automated rule checks catch a real but limited slice: contrast, missing names, invalid ARIA, duplicate ids. Keyboard operability and focus order are largely outside what a static rule can decide (Keyboard Operability, Focus Management).
  • An end-to-end test that drives the browser by clicking coordinates proves nothing about keyboard operation. Driving at least one critical flow entirely by keyboard is the cheapest high-value test in the whole suite.
  • Announcements — live regions, status messages, error summaries — are almost never verified automatically. They belong on the manual checklist by name, not as a general intention (Live Regions and Announcement).

What can go wrong

Failure modes
  • The inverted suite: end-to-end tests for logic that is a pure function, so every branch costs a browser launch and a login.
  • Over-mocking at the component level until the test asserts that the mock returns what it was configured to return.
  • Tests written against framework internals — hook call counts, internal state, render counts — so a refactor that changes nothing a user sees turns the suite red (Component Testing).
  • The mitigation failing: a strict flake policy that auto-deletes failing tests removes the tests that were finding real races, because those are the ones that fail intermittently (End-to-End Testing).
  • A coverage gate, which reliably produces tests written to execute lines rather than to assert behaviour.
  • Every level present, well maintained, and none of them covering the flow that makes the money.
What can arrive out of order
  • Tests that share a backend, a database or a user account observe each other's writes. The suite passes in the order it was written and fails when the runner parallelises it.
  • Parallel workers logging in as the same account race over session state, so a test fails because a sibling test signed it out.
  • A test that asserts on the result of two concurrent requests is asserting on an arrival order nobody guaranteed (Out-of-Order Responses).
Security
  • No frontend test proves authorization. A test showing the admin button is hidden proves the button is hidden; whether the endpoint refuses the request is a server-side test's job (What the Frontend Is Responsible For in Auth).
  • Test fixtures leak. Seeded credentials, recorded network fixtures containing real tokens, and session recordings committed to the repository are all credential disclosures with a test-shaped excuse.
  • Escaping is testable at the component level, and is one of the few genuine security properties a frontend test can prove: render a payload containing markup and assert it appears as text (Cross-Site Scripting).
  • End-to-end suites pointed at production with real accounts create write traffic, real emails and real charges. Point them at an environment you are willing to corrupt.
Misreads
  • "The pyramid tells us the right ratio." It tells you the relative cost. A product whose risk is concentrated in one checkout flow has a different correct shape from a product that is mostly data transformation.
  • "End-to-end tests are the realistic ones." They are realistic about integration and completely unrealistic about devices, networks, assistive technology and the state a real account accumulates.
  • "We hit full coverage, so it is tested." Coverage proves a line ran. It says nothing about whether anything was asserted, or whether the assertion was about behaviour a user cares about.
  • "It type-checks, so it works." Types prove shapes at a boundary. They do not prove that the discount is computed correctly or that focus goes anywhere sensible (TypeScript in the Build).
  • "We do accessibility testing — the rule checker runs in CI." That is the automated fraction, and the fraction it covers is not the fraction users get stuck on (Accessibility Testing).

Measuring it, and what changes in the field

How you would see this
  • For each user-visible bug that escaped, record which level should have caught it. That single habit reshapes a suite faster than any coverage target.
  • Track flake rate per test, not per suite. Suite-level flake hides the fact that three tests produce almost all of it.
  • Track wall-clock time to signal — how long after pushing does an engineer learn something — and where each level runs: editor, pre-commit, pull request, or nightly.
  • Production error tracking and release health tell you what the suite missed, which is the only honest measure of its coverage (Frontend Error Tracking, Release Health).
Slow device, slow network, large data, old tab
  • On a loaded CI machine everything runs slower, and timing-sensitive end-to-end tests flake at a rate they never do locally. Fixed waits calibrated on a laptop are the usual cause.
  • With a large design system, visual regression pays for itself because one token change touches hundreds of components. On a small bespoke tool it is mostly maintenance (Design Tokens).
  • In a server-rendered application, hydration mismatches are invisible to pure logic and often to component tests, because neither renders on a server and then hydrates (Hydration Mismatch).
  • Web clients do not update atomically. No level tests an hour-old tab running last week's bundle against today's API unless you deliberately construct that case (Long-Lived Clients and Version Skew).
What this costs
  • Choosing by failure costs a short design conversation per change, which is slower than reflexively adding a test at the level you always use.
  • A deliberately short end-to-end list means some flows are covered only by manual checks. You are trading breadth for a suite whose red is believed.
  • Extracting logic to make it cheaply testable adds indirection, and indirection has its own cost when it is applied to code that was never complicated (Over-Componentization).
  • Every level you add is a tool, a config, a CI job and a category of maintenance. Five levels is five things that break for reasons unrelated to your product.

Where this applies

Frontend advice ages badly and fragments across engines. These labels say what each claim is specific to, and where a different browser, device or framework would differ.

  • GENERALWhat each level can observe follows from how much of the system it instantiates, so it holds across every runner and browser. Which tool you use changes the ergonomics and the failure messages, not the blind spots.
  • FRAMEWORK-SPECIFICRunner and library names differ and move: Vitest and Jest overlap heavily but differ in module mocking and ESM handling; Testing Library exposes the same role-and-name query model across React, Vue, Svelte and Angular adapters; Playwright, Cypress and WebdriverIO differ most in how they wait and how they isolate browser state between tests.
  • SIMPLIFIEDPresenting five discrete levels is a teaching model. Real tools blur the boundaries — component tests can run in a real browser, end-to-end tools can mount a single component, and visual snapshots can be taken inside a component test — so treat the levels as observations rather than as products.

Where the depth lives

This domain teaches the browser-side mechanism and hands the rest off.

API Designapi-testing
Domains that do not exist yet
  • Testing & Reliability Engineering — the general theory this module only applies: what a test is evidence of, mutation and property-based testing, the economics of flake, fault injection, and how escaped-defect data should feed back into a suite. That domain owns the theory; this one owns what the browser makes observable.
  • Software Design — testability as a design property. Most "hard to test" verdicts in a frontend are really statements about coupling, and the fix lives in the design rather than in the test.