Testing

The three test layers, common commands, and when to add an E2E

Last updated October 29, 2026


E2E only guards "can a user complete a task"; detailed interactions belong in component tests. This page covers the split and what to ask before adding a case.

Prerequisites

pnpm test runs locally, see Getting Started.

Setup

The three layers

LayerScopeLocation
UnitPure logic: module gates, theme parsing, locale normalization, env validation, email templatestests/unit/
ComponentSingle page or component interactions: form validation, menu toggles, breakpoint layout, anchors, copytests/components/
E2ECross-page, cross-service journeys: sign-up and sign-in, account center reads and writes, module gates, key pages reachablee2e/

What belongs in E2E

Yes: paths that need a real browser, real server, and real database — session cookies, redirects, SSR and hydration, route guards, API and page working together.

No: element positions, overflow at breakpoints, animation toggles, hover offsets, anchor scrolling, TOC structure, single-field validation messages, static file contents (sitemap / robots / manifest). Component tests or build-time assertions cover these at an order of magnitude lower cost.

Before adding an E2E, ask: "which core flow does this guard?" If you cannot answer, push it down to a component test. Keep each spec under 10 cases and the whole suite under five minutes. Cross-locale behavior is covered by unit tests; E2E runs the main flow in one locale only.

Current E2E baseline

SpecWhat it guards
auth.spec.tsSign-up without a session → unverified sign-in denied → email verification and auto sign-in → sign-out; validation blocks submission
settings.spec.tsRoute guard, default landing, nickname written to D1, session invalid after sign-out
docs.spec.ts/docs redirect, ⌘K search → navigation, sidebar preference read back from the server
marketing.spec.tsHome renders, locale switch uses the real /en prefix
hydration.spec.tszh / en home hydrate without React or CSP errors
site-config.spec.tsDisabled module APIs return 404 rather than a fake success
payment.spec.tsCannot buy twice; billing page shows plan, renewal date, and amount
payment.spec.ts needs VITE_PAYMENT_PROVIDER and STRIPE_SECRET_KEY in .env. Without them the cases skip rather than fail — shipping without credentials is the normal state. It never calls Stripe: it writes an active subscription into local D1 via scripts/e2e-seed-subscription.ts and exercises the real API and page. The price anchors (lookup_key) live in src/config/products.ts, not in env.

Local email capture

pnpm test:e2e starts a dedicated test server. A Vite transport layer directs Resend requests to an in-memory local capture service with synthetic mail configuration. Sign-up, template rendering, verification links, and sessions use the real product implementation without sending real email. Account and payment tests seed verified users in local D1 and then sign in with real passwords.

Tests do not reuse a normal pnpm dev server or accept .dev.vars overrides. If port 3000 is occupied, use a separate port:

bash
E2E_BASE_URL=http://localhost:3107 pnpm test:e2e

For an externally started server in CI, run pnpm exec tsx e2e/global-setup.ts, then run tests with E2E_NO_SERVER=1. Global setup checks the local mail capture endpoint and rejects servers without the test transport.

Two traps

  • Wait for hydration before clicking. A click before hydration finishes fails silently

— no error, no navigation, indistinguishable from a product bug.

  • Add randomness to concurrent sign-up emails. Date.now() alone collides under

fullyParallel.

Commands

CommandPurpose
pnpm testAll unit and component tests
pnpm test:affectedOnly affected tests
pnpm test:watchWatch mode
pnpm test:e2ePlaywright E2E
pnpm test:e2e:uiPlaywright UI mode
pnpm exec vitest run --project unitUnit tests only
pnpm exec vitest run --project componentsComponent tests only (jsdom)
pnpm test runs locale:compile first and can stall on an interactive prompt in non-interactive environments. In CI, run the projects separately.

References

What's next