Adds .planning/codebase/ with parallel-mapper outputs covering stack, integrations, architecture, structure, conventions, testing, and concerns.
15 KiB
Testing Patterns
Analysis Date: 2026-05-11
Test Framework
Runner: Vitest 4.x.
- Root devDependency in
package.json:"vitest": "^4.1.5","@vitest/coverage-v8": "^4.1.5". - Every workspace package and
apps/webhas its ownvitest.config.tsthat delegates to the shared factory atvitest.shared.ts.
DOM environment: happy-dom 20.x (migrated from jsdom in commit 7a60a42a0). Default test environment per vitest.shared.ts:19 is "node"; per-package configs opt into browser-like envs.
Assertion / testing libraries (root package.json devDependencies):
@testing-library/react^16.3.2@testing-library/dom^10.4.1@testing-library/jest-dom^6.9.1 — registered globally invitest.setup.ts:1@testing-library/user-event^14.6.1
Run commands (root package.json):
pnpm test # turbo run test → vitest run --passWithNoTests in every package
pnpm test:coverage # turbo run test:coverage → adds --coverage flag
pnpm test:ci # adds GitHub Actions, JSON, and JUnit reporters
pnpm test:agent # agent-friendly reporter + JSON output for agentic runs
Per-package commands (uniform across all packages, see packages/api/package.json:13-19):
pnpm --filter @reactive-resume/utils test
pnpm --filter @reactive-resume/api test
pnpm --filter web test
Vitest test paths are package-relative when filtering: pnpm --filter @reactive-resume/utils test -- src/string.test.ts.
Shared Vitest Configuration
vitest.shared.ts exports createVitestProjectConfig({ name, dirname, environment, plugins }). Highlights:
root: dirname— each package runs in isolation.envDir: workspaceRoot—.envat repo root is loaded for every package.resolve: { tsconfigPaths: true }— TS path aliases resolve from the package's owntsconfig.json.setupFiles: [./vitest.setup.ts]— global hooks applied to all projects.include: ["src/**/*.{test,spec}.?(c|m)[jt]s?(x)"]— both.test.*and.spec.*are picked up.exclude: ["node_modules", "dist", ".output", "coverage", "reports"].pool: "threads",isolate: false— fast threaded execution with shared module state inside a worker.passWithNoTests: true— packages without tests don't fail CI.environmentOptions.happyDOMdisables JS/CSS file loading and navigation for safety.
Coverage is configured directly in vitest.shared.ts:49-56:
- Provider:
v8 - Output:
./coverageper package - Reporters:
text,text-summary,json-summary,json,lcov,html - Include:
src/**/*.{ts,tsx} - Exclude:
src/**/*.{test,spec}.*,src/**/*.d.ts,src/routeTree.gen.ts reportOnFailure: true
No global coverage thresholds are enforced (no thresholds: {...} block). Per-package coverage HTML lives under each package's coverage/ directory after pnpm test:coverage.
Global Setup (vitest.setup.ts)
Applied to every project:
import "@testing-library/jest-dom/vitest"— registerstoBeInTheDocument,toHaveAttribute, etc.afterEach(() => cleanup())— explicit RTL cleanup (Vitest doesn't exposeafterEachglobally withouttest.globals: true).- Polyfills for jsdom/happy-dom gaps:
ResizeObserver,IntersectionObserver,Element.prototype.scrollIntoView,window.matchMedia(used bycmdk, Base UI,next-themes).
Per-Package Vitest Configs
All vitest.config.ts files reuse the shared factory. Notable variants:
- Node default (
packages/utils/vitest.config.ts,packages/api/vitest.config.ts,packages/db/vitest.config.ts,packages/schema/vitest.config.ts,packages/ai/vitest.config.ts,packages/email/vitest.config.ts,packages/fonts/vitest.config.ts,packages/env/vitest.config.ts,packages/auth/vitest.config.ts,packages/import/vitest.config.ts,packages/pdf/vitest.config.ts,packages/config/vitest.config.ts) —environment: "node". - DOM (
packages/ui/vitest.config.ts:7) —environment: "happy-dom"for component tests. - Web app (
apps/web/vitest.config.ts) —environment: "node"plus Vite plugins to mirror dev:@tailwindcss/vite,@lingui/vite-plugin(withlinguiTransformerBabelPreset), and@rolldown/plugin-babel. Individual web tests opt into the DOM via the@vitest-environment happy-domfile-level comment.
Per-test environment overrides (declared at the top of the file as // @vitest-environment happy-dom or in a /** @vitest-environment happy-dom */ block):
packages/utils/src/sanitize.test.tspackages/utils/src/file.test.tsapps/web/src/components/resume/preview.browser.test.tsxapps/web/src/components/resume/preview.shared.test.tsxapps/web/src/components/typography/combobox.test.tsx
Test File Organization
Location: Co-located with implementation. foo.ts lives next to foo.test.ts (or foo.test.tsx for React).
Naming:
<name>.test.ts— Node/pure logic (e.g.packages/utils/src/string.test.ts).<name>.test.tsx— JSX/component tests (e.g.packages/ui/src/components/button.test.tsx).<name>.node.test.ts— Node-only modules whose implementation is also.node.ts(e.g.packages/utils/src/url-security.node.test.ts,packages/utils/src/monorepo.node.test.ts).- No
.spec.*files in the repo today, but the include pattern supports them.
Test counts (current): 127 *.test.ts files + 98 *.test.tsx files across packages/ and apps/web/src/.
Hot spots (where coverage is densest):
packages/utils/src/*.test.ts— string, color, html, date, level, locale, sanitize, rate-limit, field, file, network-icons, style, url, url-security.node, monorepo.node, plusresume/patch.test.ts.packages/ui/src/components/*.test.tsx— ~30+ component tests (button, dialog, alert, badge, card, combobox, command, popover, scroll-area, sidebar, switch, tabs, textarea, toggle, tooltip, etc.).packages/pdf/src/templates/shared/*.test.ts— columns, section-links, rich-text, metrics, picture, filtering.packages/pdf/src/section-title.test.tsandpackages/pdf/src/hooks/use-register-fonts.test.ts.packages/api/src/{dto,helpers,services}/*.test.ts—dto/resume,helpers/resume-access-policy,services/ai.packages/schema/src/{templates,page}.test.tsandpackages/schema/src/resume/{data,default}.test.ts.packages/ai/src/{tools,resume}/*.test.ts— patch-proposal, sanitize, extraction-template.packages/import/src/reactive-resume-v4-json.test.ts,packages/fonts/src/index.test.ts.apps/web/src/libs/{pwa,locale,theme,error-message}.test.ts,apps/web/src/dialogs/store.test.ts,apps/web/src/components/resume/preview.{browser,shared}.test.tsx,apps/web/src/components/typography/combobox.test.tsx.
Test Structure Patterns
Idiomatic skeleton (from packages/utils/src/string.test.ts:1-21 and packages/api/src/helpers/resume-access-policy.test.ts:1-17):
import { describe, expect, it } from "vitest";
import { thingUnderTest } from "./thing";
describe("thingUnderTest", () => {
it("returns X for Y", () => {
expect(thingUnderTest(input)).toBe(expected);
});
it("returns Z for empty input", () => {
expect(thingUnderTest("")).toBe("");
});
});
Patterns observed:
- Top-level
describeper exported function; nesteddescribeblocks group behaviors. - One assertion focus per
it— short, declarative names ("returns X", "throws Y when Z", "does not mutate the input"). - Negative cases are first-class — every helper has tests for
null, empty string, unknown shapes, etc. it.each([...] as const)("variant=%s renders without throwing", (variant) => {...})for matrix tests over discriminated union variants (seepackages/ui/src/components/button.test.tsx:55-79).- Setup with
beforeEach/afterEachis used only when needed (timers, temp dirs, store reset). Example:apps/web/src/dialogs/store.test.ts:4-13resets a Zustand store between tests withuseDialogStore.setState(...). - Temp directories use
fs.mkdtempSync+realpathSyncand are torn down inafterEach(packages/utils/src/monorepo.node.test.ts:7-17). - Fake timers via
vi.useFakeTimers()/vi.advanceTimersByTime(300)for animation/transition assertions (apps/web/src/dialogs/store.test.ts:54-65).
Mocking
Library: Vitest's built-in vi (no Jest). Used sparingly — most tests cover pure functions.
Patterns:
vi.fn()for callback assertions:expect(onClick).toHaveBeenCalledOnce()(packages/ui/src/components/button.test.tsx:32-37).vi.fn().mockResolvedValue(true)for async handlers (apps/web/src/dialogs/store.test.ts:86-93).vi.mock("module-path", () => ({ ... }))for replacing modules. Heaviest example:apps/web/src/components/resume/preview.browser.test.tsx:25-81mocks@react-pdf/renderer,@/libs/resume/pdf-document,./builder-resume-draft, and./pdf-canvasso the preview component can be exercised without React PDF.vi.hoisted(() => ({ ... }))to share mutable state with hoistedvi.mockfactories (apps/web/src/components/resume/preview.browser.test.tsx:8-12). This is required becausevi.mockcalls are hoisted above imports.- Spies via
vi.spyOnare rare; mock modules are preferred so the real implementation stays out of scope.
Test data / fixtures:
- No central
fixtures/directory. Tests build minimal objects inline or extend canonical defaults from the schema package:import { defaultResumeData } from "@reactive-resume/schema/resume/default"(used bypackages/api/src/helpers/resume-access-policy.test.ts:2).import { sampleResumeData } from "@reactive-resume/schema/resume/sample"(used byapps/web/src/components/resume/preview.browser.test.tsx:5).
- Local helper builders are inlined per test file (e.g. the
resumeDataWithPageCounthelper atapps/web/src/components/resume/preview.browser.test.tsx:14-23).
React Component Testing
Render + query via Testing Library (packages/ui/src/components/button.test.tsx):
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
render(<Button onClick={onClick}>Click</Button>);
await userEvent.click(screen.getByRole("button"));
Accessibility-first queries: getByRole("button", { name: "..." }) is the default; aria-label is asserted explicitly (packages/ui/src/components/button.test.tsx:81-84).
Slot / data-attr conventions: Components expose data-slot for shadcn/Base UI slotting and tests assert it (button.test.tsx:22-25).
Async UI: Use waitFor from @testing-library/react and await userEvent.*. The cleanup() afterEach in vitest.setup.ts ensures DOM doesn't leak between tests despite isolate: false.
Reporters
Per-package package.json scripts (uniform pattern, e.g. packages/api/package.json:15-18):
test→vitest run --passWithNoTeststest:coverage→vitest run --coverage --passWithNoTeststest:ci→vitest run --coverage --reporter=default --reporter=github-actions --reporter=json --reporter=junit --outputFile.json=reports/vitest-results.json --outputFile.junit=reports/vitest-junit.xml --passWithNoTeststest:agent→vitest run --reporter=agent --reporter=json --outputFile.json=reports/vitest-results.json --passWithNoTests
The agent reporter is a Vitest 4 feature optimized for LLM-driven runs; pnpm test:agent is the canonical script to use when an agent needs structured pass/fail data.
JUnit + JSON outputs land in reports/ inside each package (Biome-ignored).
CI
.github/workflows/autofix.yml— runs on every PR and push tomain. It runspnpm knip --fixandpnpm check. It does NOT runpnpm test. Tests are not currently gating CI..github/workflows/docker-build.yml—workflow_dispatchonly; builds multi-arch images. No test step..github/workflows/crowdin-sync.yml— translation sync only.
Tests are run locally (pnpm test) or by agents via pnpm test:agent / pnpm test:ci. No PR is currently blocked by a failing test on GitHub Actions.
Test Types
- Unit tests — Dominant. Pure functions in
packages/utils,packages/schema,packages/pdf/src/templates/shared,packages/api/src/{helpers,dto}are exercised in isolation. - Component tests —
packages/ui/src/components/*.test.tsxand a handful inapps/web/src/components/. Driven by@testing-library/react+happy-dom(file-level override) orpackages/ui's package-levelenvironment: "happy-dom". - Integration tests — None against the live Drizzle client or the running TanStack Start server.
apps/web/src/components/resume/preview.browser.test.tsxis the closest, mocking React PDF and exercising the preview pipeline end-to-end in happy-dom. - E2E / browser tests — Not present. No Playwright, Cypress, or
vitest --browserconfig.
Common Patterns
Async error testing:
expect(() => assertCanView({ userId: "u1", isPublic: false }, null)).toThrow();
try {
assertCanView({ userId: "u1", isPublic: false }, null);
expect.unreachable();
} catch (error: unknown) {
expect((error as { code?: string }).code).toBe("NOT_FOUND");
}
(See packages/api/src/helpers/resume-access-policy.test.ts:29-44.)
Immutability assertions:
const before = JSON.stringify(resume);
redactResumeForViewer(resume, false);
expect(JSON.stringify(resume)).toBe(before);
(See packages/api/src/helpers/resume-access-policy.test.ts:86-93.)
Time-sensitive logic: UUIDv7 ordering is verified with a setTimeout and a string compare instead of mocking time (packages/utils/src/string.test.ts:15-20).
Coverage Gaps Worth Flagging
These areas have implementation but no *.test.* files alongside them today — agents adding features here should consider adding tests.
packages/email/src/transport.tsand templates — no tests. SMTP transport is untested.packages/env/src/server.ts— no tests for env-var schema validation.packages/auth/— no tests; Better Auth config and helpers are uncovered.packages/api/src/services/{resume,storage,statistics,auth,flags,resume-events}.ts— onlyai.tshas a service-level test (ai.test.ts). Most resume mutation flow logic is exercised only indirectly throughhelpers/resume-access-policy.test.tsanddto/resume.test.ts.packages/api/src/routers/*— no router-level tests. End-to-end oRPC procedure behavior (auth + rate limit + service composition) is not asserted.packages/api/src/middleware/rate-limit/*— no tests.packages/db/src/schema/*— no tests (schema correctness is implicit, but no migration roundtrip tests).packages/scripts/— no tests.packages/runtime-externals/— no tests.apps/web/src/routes/**— route handlers (api/rpc.$.ts,api/auth.$.ts,api/health.ts,uploads/...,mcp/...,auth/oauth.ts) and most builder UI underroutes/builder/$resumeIdare uncovered.apps/web/src/dialogs/**— onlystore.test.tscovers the dialog store. Individual dialog components (resume create/update/import, two-factor, api-key) are uncovered.packages/pdf/src/templates/{azurill,bronzor,...}— individual template renderers have no tests; only the shared primitives intemplates/shared/are covered.
When adding tests in any of these areas, follow the colocation rule (foo.ts ↔ foo.test.ts) and reuse defaultResumeData / sampleResumeData from @reactive-resume/schema/resume/* instead of inventing new fixtures.
Testing analysis: 2026-05-11