# Frameworks and conventions

Which technology proves work on each stack, and where the suite lives. Read at the plan gate and in placement mode.

## Frameworks by stack

Framework choice is **never** ad hoc and never the user's problem mid-task: detect the stack, use its row; if the repo already has a runner, that runner wins — **never** introduce a second framework beside it.

| Stack | Unit / integration | Component | Rendered (7b) | Judged drive | E2E | HTTP mocking | Don't use |
| --- | --- | --- | --- | --- | --- | --- | --- |
| Node + Vite / React (web) | `vitest` | `@testing-library/react` (+ `@testing-library/user-event`, `happy-dom`) | `vitest` browser mode, Playwright provider | Chrome CDP, else `@playwright/test` headed | `@playwright/test` | `msw` | `jest`, `mocha`, `jasmine`, `cypress`, `enzyme` |
| Express APIs | `vitest` | — | — (nothing rendered) | the real wire — `curl`/HTTP against the running app | `supertest` against the app instance | `msw` (outbound calls) | `jest`, `chai-http` |
| React Native / Expo | `jest` via the `jest-expo` preset | `@testing-library/react-native` | `maestro` on a device/simulator | the simulator, watched | `maestro` | `msw` | `detox`, `enzyme` |
| Cloudflare Workers | `vitest` + `@cloudflare/vitest-pool-workers` | — | — (nothing rendered) | the real wire against `wrangler dev` | `@playwright/test` against `wrangler dev` | `msw` | `jest` |
| Java / Spring Boot | JUnit 5 + AssertJ (`spring-boot-starter-test`); Testcontainers for `*IntegrationTest` | — | — (nothing rendered) | the real wire — `curl`/HTTP against the running jar | `*IntegrationTest` over HTTP against the booted app on Testcontainers | WireMock | JUnit 4, PowerMock, H2 standing in for MySQL |
| Rust | `cargo test` (built-in) | — | the app window, driven by the OS layer where one exists | the running binary, watched | — | — | — |

**The two new columns are the inversion made concrete.** *Rendered (7b)* is the unattended real-engine runner — where geometry pins live, and `—` where a stack renders nothing. *Judged drive* is what a person or an agent looks at when a change is finished, and it leads the gate on any stack that renders: Chrome CDP first because it drives a real browser with no script written in advance, Playwright headed where CDP is unavailable. A stack whose judged drive is `—` has no interface to look at, not permission to skip looking.

Test packages install at `latest` — outside the approved-packages pin set. Fake data comes from `@ngneat/falso` — **never** `@faker-js/faker` / `chance`.

## Conventions

- **Suite, not scripts.** Tests live in the canonical test tree — `tests/` mirroring `src/`, or the convention the repo already uses — named `<subject>.test.ts`; shared fixtures, factories, and helpers in `tests/helpers/` (or the suite's existing shared home).
- **Wire the canonical command.** `"test"` in `package.json` runs the whole unit suite (`vitest run`); e2e under `"test:e2e"`. A repo where `pnpm test` / `npm test` doesn't run the suite is misconfigured — fix the wiring, **never** route around it.
- **Unit and E2E trees stay separate.** Vitest/Jest suites in `tests/`; Playwright/Maestro suites in `e2e/` with their own config — **never** mixed into the unit runner's glob. The e2e config **owns its world** — it builds, seeds, and boots the app itself (a `webServer` block or equivalent), so a journey passes from a cold shell like any other test and "the world was not running" is never why one did not run.
- **The real-renderer stratum is a third tree** — `tests/rendered/` under the unit runner's *browser mode*, its own config, kept to geometry regressions that shipped and the composition assertions a component library cannot make alone. It is not a second component suite; anything a simulated DOM can prove stays in `tests/component/`.
- **No test doubles for what you own.** Prefer real instances — in-memory SQLite for drizzle, the actual Express app via `supertest` — over mocks; mock **only** true externals (network via `msw`, time via the runner's fake timers).
- **Determinism.** No real network, no real time, no shared mutable state between tests; every test **must** pass alone, in any order, **and from a cold shell** — never against a hand-started dev server (Workers: `@cloudflare/vitest-pool-workers`; Express: `supertest` against the app instance).
