Skip to content

Grow test migration: Jest → Vitest (staged plan)

Goal: bring ~1000 Grow test files back into CI. Today they are fully excluded (the scope:grow tag is filtered out in .github/scripts/nx-run-test.ts) because Jest OOMs in CI. Instead of tuning Jest, we migrate to Vitest in stages — the standard for the rest of the monorepo.

Why Vitest (measured on our code, not theoretical): • Jest: a worker starts at 2.2 GB heap and grows linearly to 4.8 GB (module registry + jsdom leak); 4 workers ≈ 19 GB → OOM in CI. • Cold start of a trivial test under Jest: 49 s (babel-jest transforms the whole import graph through barrels). The same test under Vitest: 2.7 s cold / 1.4 s warm (PoC done — a real component test with render, user-event, jest-dom and an i18next mock passes too). • The dev loop (watch mode, single file) drops from ~1 min to seconds.

Scale: 1013 test files (~101k lines, 1962 tests): 432 in the app + 621 across 19 libs. 3572 jest.* usages in 683 files — ~95% handled by a codemod. The manual tail: ~285 files with mockX variables inside jest.mock factories (Vitest requires vi.hoisted()), 103 × jest.requireActualawait vi.importActual.


Stage 0 — foundation (1 person, sequential, ~2–3 days)

V0.1 Base infrastructure Shared vitest.lib.config (counterpart of jest.lib.config.ts) + a shared setup that ports setupTests.js: • jest-dom via expect.extend(matchers) (note: the @testing-library/jest-dom/vitest entry does not work with Vitest 4 — verified), • global vi.mock for react-i18next and nanoid, • jest-fetch-mockvitest-fetch-mock, jest-whenvitest-when, • the SVG mock as .jsx (esbuild does not parse JSX in .js — verified), • verify vitest-styled-components (244 toHaveStyleRule assertions in 32 files); fallback: port the matcher (~200 lines). A seed config (aliases translated from the jest moduleNameMapper, setup, SVG mock) came out of the PoC — get it from Ryszard. DoD: a chosen app test file passes locally under Vitest.

V0.2 CI wiring Change .github/scripts/nx-run-test.ts: instead of excluding all of scope:grow, exclude only grow projects whose test target still uses @nx/jest:jest. A migrated project automatically joins the "Test affected projects – Vitest" job. DoD: the V0.3 pilot is visible and green in CI on a PR.

V0.3 Pilot: data-controls (9 tests) + hooks (8 tests) The smallest libs with no risky mocks. End-to-end proof: codemod → green locally → green in CI. DoD: both libs on Vitest in CI; a short "how to migrate a lib" note in docs/.

Stage 1 — simple libs (parallelizable, 1 task = 1 lib = 1 PR, ~0.5–1 day each)

Ordered by size (tests / files needing manual hoisting fixes): • V1.1 reviews (4 / 1) • V1.2 file-export (4 / 3) • V1.3 widgets (7 / 2) • V1.4 bravos (10 / 2) • V1.5 filter-builder (10 / 0) • V1.6 auth (16 / 2) • V1.7 ui (41 / 0) • V1.8 reports (46 / 2) • V1.9 ui-legacy (61 / 6, note: theme + styled-components) • V1.10 shared (63 / 14, note: factories/models used by everyone else's tests) (permissions, routes, types, testing — zero test files: just switch the target, fold into any PR.)

Stage 2 — large libs (1 task = 1 lib, ~2–4 days each)

• V2.1 surveys — 115 tests, 44 files with hoisting, 15 × requireActual • V2.2 skills-framework — 98 tests, 66 files with hoisting • V2.3 flexible-reviews — 129 tests, 76 files with hoisting, 6 × requireActual

Stage 3 — the app (432 files; split by directory, 1 task = 1 directory = 1 PR)

Coexistence mechanics: the app's test target stays on Jest (locally, outside CI) while a Vitest target grows an include list extended per PR; Jest's testPathIgnorePatterns grows mirror-wise. At the end Jest goes away. • V3.1 src/app/utils + src/app/routing + src/app/hooks • V3.2 src/app/components • V3.3 src/app/pages • V3.4 src/modules • V3.5 src/app/oldApp + storybook/ • V3.6 Fix the 4 red suites (2 fail deterministically already under Jest: ReviewTypeButton — TypeError in Radix dismissable-layer, TableOfContents — "Target container is not a DOM element"; 2 skipped in testPathIgnorePatterns: useGetGoBackLink, FormTemplateActionsDropdown).

Stage 4 — cleanup and closure

• V4.1 Remove Jest: jest.config.ts, jest.env.cjs, jest.lib.config.ts, babel-jest + jest dependencies from package.json; delete the grow exception in nx-run-test.ts (everything runs in the standard job). • V4.2 Measure full-suite time and memory in CI; tune poolOptions if needed. • V4.3 Fix the 3 extremely slow tests (real timers/waitFor): ReviewsActionDropdown 462 s, AuthenticatedAppRouter 376 s, GoalFormUpdate 341 s.


Definition of Done for every migration task:

  1. npx nx test <project> green locally and in CI; no new skips.
  2. Only the runner API changes (jest.* → vi.*), never test behavior; any deviation described in the PR.
  3. Snapshots pass without regeneration (6 files in the repo) — regeneration only as a conscious decision in review.

Risks:vi.hoisted() in ~285 files is the main manual cost — concentrated in Stages 2 and 3 (flexible-reviews 76, skills-framework 66, surveys 44). • vitest-styled-components unverified — gate in V0.1, fallback known. • Grow ships an old Vitest 1.6.1 (for playwright-ct) — use the root version (4.0.17), do not mix. • ESM/CJS mock interop (__esModule — 15 places) may need spot fixes.

Overall estimate: ~3–4 engineering weeks, fully sliceable: Stage 1 is 10 independent "spare time" tasks, Stages 2–3 parallelize across people. Tests return to CI incrementally — first libs right after Stage 0.