Blueprint for Frontend Testing Strategy
The created suite must:
- Run in the minimum possible time: reuse rendering context, choose the proper layer for every case, avoid test duplication between layers.
- Provide early feedback: apply the testing pyramid — always test at the lowest viable layer, and prefer build-time over post-deployment.
Testing Pyramid and Layer Optimization
/\
/ \ 7. E2E Tests — post-deployment, real env, fewest & slowest
/----\
/ \ 6. Contract Tests (FE↔BE) — Pact, compatibility only
/--------\
/ \ 5. Mock-E2E Tests — several components / full page, SSR, mocked BE
/------------\
/ \ 4. Component Tests — single business component in a real browser
/----------------\
/ \ 3. Unit Visual Tests — presentational components in Storybook
/--------------------\
/ \ 2. Unit Tests — logic, JSDOM, sociable-first
/------------------------\ 1. Static Analysis — most, fastest, no execution
Test at the Lowest Viable Layer
Every scenario must live at the lowest layer that can meaningfully verify it. Move up only when the lower layer cannot exercise the scenario without unreasonable complexity or loss of fidelity. A real browser is the expensive resource on the frontend — only climb to a browser-backed layer when JSDOM genuinely cannot cover the case.
| Scenario | Layer |
|---|---|
| Type errors, code smells, style violations | Static Analysis (ESLint + TypeScript) |
| Logic in a presentational or business component; user-perspective flow over several components’ logic (no real browser) | Unit (Jest + RTL + MSW, JSDOM) |
Component-level React hydration / render-to-string (react-dom/server + hydrateRoot in JSDOM) | Unit (Jest + RTL, JSDOM) |
Next.js SSR pipeline (getServerSideProps/getStaticProps, Server Components, streaming, framework hydration) | Mock-E2E / E2E (app running) |
| Visual appearance of a catalog presentational component | Unit Visual (Storybook + Puppeteer + RegCli) |
Browser-dependent behaviour of one business component (sliders, scroll, useEffect, children updates) | Component (real browser, Playwright) |
| Visual appearance of one business component | Component Visual (Playwright) |
| Several components / a full page wired together, mocking the backend, covering the Next.js SSR pipeline | Mock-E2E |
| Compatibility of the FE↔BE API contract | Contract (Pact) |
| Critical flow against a deployed environment with the real backend | E2E (post-deployment) |
Layer Boundary Rules
- If a bug can be caught with ESLint or the TypeScript compiler, it MUST be caught there — never write a runtime test for what static analysis covers.
- If a bug can be caught with a JSDOM unit test, it MUST be caught there. Do not open a real browser for something a unit test can cover.
- A real-browser layer (Component / Mock-E2E / E2E) exists to verify what JSDOM cannot: real rendering, browser-dependent interactions, SSR/hydration timing. Do not use it to re-assert logic already covered by unit tests.
- “Component Contract” is not a test layer. Parent/child component compatibility is enforced by TypeScript static type checking (or
propTypes, which React discourages) — not by a written test. - Contract Tests verify FE↔BE compatibility, not business correctness. Do not re-validate logic already covered at lower layers.
- E2E Tests must not duplicate contract or mock-e2e assertions, and should be a small set of critical flows.
Anti-Patterns
- Pyramid inversion: more browser-backed (component/mock-e2e/E2E) tests than static + unit tests.
- Cross-layer duplication: same rule asserted at multiple layers. Pick the lowest; trust the others.
- Snapshot testing as default: not recommended — use only in justified exceptions.
- Storybook for business components: Storybook is for the presentational-component catalog. Do not use it to mount business components — you lose interaction, children updates and
useEffectfidelity. - Jest “component tests”: a Jest/JSDOM test of a business component is a sociable unit test, not a component test. A component test renders the component isolated in a real browser.
- Over-testing in upper layers: do not re-test in mock-e2e/E2E what is already covered by component or unit tests.
Layer Decision Checklist
Before generating any test, answer in order:
- Can the compiler or a lint rule catch it? → Static Analysis
- Only component/page logic, react SSR or hydration, no real browser needed? → Unit Test (sociable preferred)
- Visual regression of a catalog presentational component? → Unit Visual Test (Storybook)
- Needs a real browser for one isolated business component (interaction, browser API, children updates)? → Component Test (+ Component Visual if appearance is the target)
- Several components / a full page together, mocking the backend, covering NextJS SSR? → Mock-E2E Test
- Verifies the agreed FE↔BE contract? → Contract Test (Pact)
- Verifies the deployed system end-to-end? → E2E Test (post-deployment)
Versions — use technology-radar-blip skill for: TypeScript, Jest, React Testing Library, Mock Service Worker (MSW), Storybook, Puppeteer, Playwright, Pact-JS.
BUILD TIME
1. Static Analysis
Scope: code quality, problematic patterns and potential errors detected without executing the code.
Tooling:
- ESLint — code-quality rules (issues, problematic patterns, potential errors).
- TypeScript — static type checking to catch type-related errors before execution.
- Husky — run static analysis on git hooks (pre-commit / pre-push).
Constraints:
- Apply both ESLint and TypeScript — they are complementary, not alternatives.
- Where SonarQube is in use (e.g. Zeus, UI & Modules), align ESLint rules with the Sonar TypeScript profile so neither tool silently misses errors.
2. Unit Tests
Scope: a piece of logic in presentational components, business components, modules (services) or any other file with JS/TS logic; user-perspective flows that span several components’ logic — when the component is not rendered in a real browser.
Dependencies: Jest + React Testing Library + Mock Service Worker (MSW). Jest renders components with JSDOM.
Naming: [Name].test.ts(x).
Constraints:
- Sociable unit tests are preferred — they are more user-perspective oriented; use solitary unit tests where isolation is genuinely needed.
- React hydration at component level is covered here: we can render a React component server-side in-process with
react-dom/server, inject the resulting HTML into JSDOM, callhydrateRoot, and assert that there are no hydration mismatch warnings and that event handlers attach. This validates the component’s React-level SSR/hydration behavior in isolation, without needing a running Next.js app. For traditional React DOM server rendering at component level, SSR can be exercised in-process as a render-to-HTML operation. - The Next.js SSR pipeline is NOT covered here: Jest/RTL do not execute the Next.js runtime. Therefore framework-level behavior such as routing,
getServerSideProps/getStaticProps, React Server Components, streaming, Flight payloads, layouts, and Next’s hydration orchestration must be tested with the app running, typically via Mock-E2E or E2E tests. - Mock the network at the boundary with MSW, not by stubbing individual fetch calls.
3. Unit Visual Tests
Scope: visual regression of the presentational components in the Storybook catalog.
Dependencies: Storybook + Puppeteer + RegCli. Puppeteer opens a browser and navigates to the launched Storybook; RegCli performs image comparison.
Constraints:
- Test only the components affected by the change (controlled via the Vite configuration for Storybook).
- Adding a new component to the catalog should be the only action required from the developer — keep the runner transparent and automatic.
- Puppeteer drives Chrome/Firefox only; if a wider browser matrix is required for this catalog layer, evaluate Playwright as the driver.
4. Component Tests
Scope: functional test of a whole business component, rendered isolated in a real browser. A component may nest other components; the backend and parent-component props are mocked. SSR is not tested here. Covers browser-dependent behaviour (sliders, scroll, children updates, useEffect).
Dependencies: Playwright Component Tests.
Naming: [ComponentName].ct.ts(x) (component test).
Constraints:
- The defining property of this layer is rendering the component isolated in a real browser — Playwright is the chosen tool for this strategy.
- A Jest/JSDOM test of a business component is not a component test — it is a sociable unit test. Keep it at layer 2.
- Do not use Storybook for business components (loss of interaction / children-update /
useEffectfidelity). - Snapshot testing is not recommended — exceptions only.
- Do not over-test here what lower layers already cover.
5. Component Visual Tests
Scope: visual regression of business components, mounted isolated, with backend and parent props mocked. SSR is not tested here.
Dependencies: Playwright (+ a visual-diff library such as RegCli).
Constraints:
- Do not use Storybook for business-component visual tests — Playwright is the chosen tool for this layer.
- Do not test every component in every style across every browser/platform combination. Choose the most representative browser/platform per case.
6. Mock-E2E Tests
Scope: functional end-to-end test of several business and presentational components working together with the backend mocked — may mount a whole web page, with the target of covering the real Next.js SSR pipeline (getServerSideProps/getStaticProps, Server Components, streaming and framework hydration) that the Unit layer cannot reach.
Dependencies: Playwright for real-browser mock-e2e tests against the running app; Jest + RTL (JSDOM) only when a real browser is not required (treat that variant as a sociable unit test, and note it does NOT exercise the Next runtime).
Naming: [Flow].me2e.ts.
Constraints:
- The Next.js SSR pipeline requires the app to be running (e.g. a dev/docker instance) — verify it here, not at the Unit layer.
- Prefer a real browser when SSR or browser-dependent behaviour is the target.
- Mock the backend; contracts may be reused as mocks where available.
- Do not duplicate assertions already covered at the component or unit layers.
7. Contract Tests (Frontend ↔ Backend)
Scope: API contract compatibility between frontend and backend — compatibility only, not business correctness.
Dependencies: Pact (Pact.io).
Naming: split each contract into three files in a pacts/ folder:
[endpoint].pact.spec.ts— the test (setup, interactions wiring, assertions).[endpoint].pact.interactions.ts— the interaction builders (request/response definitions per scenario).[endpoint].pact.mock.ts— the request/response matchers and example payloads.
Constraints:
- Verify compatibility of the agreed contract; do not re-validate logic owned by lower layers.
- Contracts produced here can be reused as mocks in mock-e2e tests.
Best Practices:
A well-made contract combines two things: a complete description of the client’s behaviour (structure, typing and error coverage) and a faithful description of the payload shape (rich matchers). Both halves matter — a contract with perfect matchers that only tests the happy path is incomplete, and a contract that covers errors with hardcoded payloads doesn’t really test the contract.
- Type everything against the real client types. Type the request/response against the actual types exported by the client, never
any. This is what makes the test break when the client’s shape drifts from the contract. - Cover errors, not just the happy path. Add an interaction per meaningful outcome — success, validation failure (4xx), business errors (e.g. a conflict mapped to a domain error code) — and assert that the client surfaces them correctly (
await expect(call()).rejects.toThrow(...)/rejects.toEqual(...)). A single 200 interaction is rarely a sufficient contract. - Describe payloads with matchers, never literals. The whole point of contract testing is matching shape, not values. Use the strongest matcher per field —
regex/termfor enums and formatted strings,timestampfor dates,integer/decimal/number/booleanfor primitives,eachLike({ min })for collections,arrayContaining/eachValueMatchesfor heterogeneous structures. Hardcoded response bodies are an anti-pattern. - Assert on shape, not on the mock’s literals. When asserting in the spec, verify the contract the client must satisfy (e.g. that a currency field matches
/^[A-Z]{3}$/), not the placeholder example value — avoid assertions that just echo the literal value used in the mock. - Keep the three files DRY. Factor a shared interaction builder parameterised by state/description/matchers so each scenario is a few lines; keep matchers and example payloads in the
.mock.tsso interactions and specs stay declarative. - Provider states must be meaningful. Each
given(...)should describe a real backend precondition (e.g. “a record with the requested id already exists”), not a generic label. - No dead or disabled contracts. Don’t leave interaction builders defined-but-never-invoked, and avoid
.skip-ed contract tests — a skipped or unwired contract gives false confidence. Use the project’s configured test runner consistently andawaitthe Pact setup before running interactions.
POST DEPLOYMENT
8. E2E Tests
Scope: end-to-end checks after deployment, validating the connection with the real backend and availability in a real environment. The app is not started by the test suite.
Dependencies: Playwright.
Naming: [Flow].e2e.ts.
Constraints:
- Make tests resilient to client-side change: styles must not affect E2E outcomes; submit forms without depending on buttons; verify whether form fields are displayed before filling them.
- Focus on critical smoke/regression flows against the real backend — not exhaustive coverage.
- Reference: Front End Post Deployment Tests Decision Log.
Done When
- Unit Tests run with Jest + RTL + MSW (JSDOM), sociable-first, covering component-level React SSR/hydration (the Next.js SSR pipeline is left to Mock-E2E / E2E)
- Unit Visual Tests run only for components affected by the change (Storybook + Puppeteer + RegCli)
- Component / Mock-E2E tests that require a real browser use Playwright, not Jest/JSDOM or Storybook
- Visual layers test representative browser/platform combinations, not the full matrix
- FE↔BE Contract Tests run with Pact
- E2E tests are resilient to client-side change and run post-deployment against the real backend