Skip to content

test: raise coverage 49.67% → 98.84%, fix the config that hid the gap - #28

Merged
chriskehayias merged 1 commit into
devfrom
test/coverage-push-to-99-percent
Sep 13, 2026
Merged

test: raise coverage 49.67% → 98.84%, fix the config that hid the gap#28
chriskehayias merged 1 commit into
devfrom
test/coverage-push-to-99-percent

Conversation

@chriskehayias

Copy link
Copy Markdown
Contributor

The reported coverage number was wrong

vitest.config.ts set no coverage.include, so the v8 provider only instrumented files that some test had already imported. 131 of 176 source files were absent from the report entirely rather than counted as 0%. It read 83.14% while true coverage was 48.41% — and this is the report CI uploads to Codecov.

That's how src/services/familyService.ts (756 lines) and src/app/(web)/tools/layout.tsx (the /tools authorization gate) sat at zero tests without anyone seeing it.

Results

Measured on the same denominator before and after:

Metric Before After
Statements 49.67% (1,814/3,652) 98.84% (3,610/3,652)
Lines 50.20% 99.70% (3,363/3,373)
Branches 37.51% 92.21%
Functions 38.35% 98.81%
Test files 50 116
Test cases 812 1,535

Scope excludes vendored shadcn/ui primitives, generated MP models, build-time scripts, loading.tsx skeletons, barrels, and type-only modules.

Config changes

  • coverage.include: ['src/**/*.{ts,tsx}'] — the actual fix. Worth knowing: coverage.all was removed in Vitest 5, so setting it is a type error that does nothing. include is the only lever.
  • Directory exclusions now end in **. A bare 'src/components/ui/' matches nothing, which had quietly kept those files in the denominator.
  • thresholds: { statements: 97, lines: 98 }, enforced by the existing test:coverage CI job. Branches and functions are deliberately unenforced — a single defensive guard can trip them, and a threshold people learn to override is worse than none. I verified the gate actually fires by setting it to an impossible value and confirming exit 1.
  • vitest.config.ts.mts. As .ts it loaded as CommonJS and Vite warned it breaks when configLoader: 'native' becomes the default. The rename exposed a second instance of the same problem: __dirname only worked because of the CJS load. As real ESM it would have thrown at config load and stopped every test from starting. Now import.meta.dirname.

npm run build was already broken on dev

tsconfig.json includes **/*.ts, next.config.ts sets no ignoreBuildErrors, and CI never runs next build — so two committed test files failed type checking while CI stayed green.

Their fixtures built CommunicationInfo / MessageInfo / FileUploadParams values from raw MP column names (Author_User_ID, From_Contact) instead of the DTO fields the helper requires. The tests passed because the mocks never inspected the missing fields. Fixtures now carry explicit type annotations so the compiler enforces the shape. Build succeeds.

Runner output: 1,507 lines → 17, zero warnings

  • Barcode tests mocked react-pdf's View as the string 'View', which React read as a capitalised unknown tag — two warnings per element, 797 lines for one 65-bar barcode. Now rpdf-view; the dash matters, since any undashed unknown tag still draws "unrecognized in this browser".
  • Added console spies to the 15 test files that deliberately drive error paths. The MP logger is unconditional by design, so those were expected logs plus stack traces. The spies use mockImplementation, so they still record and assertions on logged content keep working.

One source fix (14 lines)

Select fields in the group wizard and Add/Edit Family passed value={... : undefined}, leaving them uncontrolled until first selection and logging a React controlled/uncontrolled warning.

Silencing that in tests would have hidden a real bug: while a field is uncontrolled, React state is not the source of truth for it, so form.reset() between wizard runs isn't guaranteed to clear the trigger. Changed to "", Radix's documented "no selection" value. Diff is exactly 14 lines, nothing else.

Review pointers

113 files, but only a handful need real attention:

  • vitest.config.mts — the config contract and thresholds
  • the 5 .tsx source files — 14 identical one-line changes
  • helper.test.ts / provider.test.ts — the corrected fixtures
  • everything else is new *.test.* files and docs

TODOs: 8 opened, 4 resolved

9 open — 3 high, all found by reading code while writing tests for it:

  • Filter injection shapeFamilyService interpolates client-supplied envelopeNo/donorId into MP $filter strings with no validation; the only filters in that file lacking a validatePositiveInt guard.
  • Silent field lossremoveGroup guards groupedFields but not groupOrder, so a non-empty group can be stripped from the order while its fields remain. buildSavePayload then drops them. Real MP page-config data loss, no error surfaced.
  • Address PII into logsmergeTemplate logs the raw docxtemplater error, which attaches the live merge scope (names, addresses).

Plus 4 medium (incl. no type-check gate in CI) and 2 low. Docs updated: CLAUDE.md, DECISIONS.md, testing references, new facts snapshot.

Verification

Gate Result
npm run test:run exit 0 — 116 files, 1,535 tests, 17 lines, no warnings
npm run test:coverage exit 0 — thresholds pass
npx tsc --noEmit 0 errors
npx eslint . 0 problems
npm run build exit 0

🤖 Generated with Claude Code

The reported coverage figure was wrong. `vitest.config.ts` set no
`coverage.include`, so the v8 provider only instrumented files that some
test had already imported — 131 of 176 source files were absent from the
report entirely rather than counted as 0%. That reported 83.14% while
true coverage was 48.41%, and it is what CI uploads to Codecov.

It is why `src/services/familyService.ts` (756 lines) and
`src/app/(web)/tools/layout.tsx` (the /tools authorization gate) could sit
at zero tests without anyone seeing it.

Coverage over authored code, measured on the same denominator throughout:

  Statements   49.67% -> 98.84%  (3,610/3,652)
  Lines        50.20% -> 99.70%  (3,363/3,373)
  Branches     37.51% -> 92.21%
  Functions    38.35% -> 98.81%
  Test files   50 -> 116   Test cases  812 -> 1,535

Scope excludes vendored shadcn/ui primitives, generated MP models,
build-time scripts, loading.tsx skeletons, barrels and type-only modules.

Config
- `coverage.include: ['src/**/*.{ts,tsx}']` — the actual fix. Note
  `coverage.all` was REMOVED in Vitest 5: setting it is a type error and
  does nothing, so `include` is the only lever.
- Directory exclusions now end in `**`. A bare `'src/components/ui/'`
  matches nothing, which had quietly kept those files in the denominator.
- `thresholds: { statements: 97, lines: 98 }`, enforced by the existing
  test:coverage CI job. Branches and functions are left unenforced: a
  single defensive guard can trip them, and a threshold people learn to
  override is worse than none. Verified the gate fires by setting it to
  an impossible value and confirming exit 1.
- Renamed `vitest.config.ts` -> `.mts`. As `.ts` it was loaded as
  CommonJS and Vite warned it breaks when `configLoader: 'native'`
  becomes the default. The rename exposed a second instance of the same
  problem: `__dirname` only worked because of the CJS load, and as real
  ESM it would have thrown at config load and stopped every test from
  starting. Now uses `import.meta.dirname`.

Build was already broken on dev
`tsconfig.json` includes `**/*.ts`, `next.config.ts` sets no
`ignoreBuildErrors`, and CI never runs `next build` — so two committed
test files failed type checking while CI stayed green. Their fixtures
built `CommunicationInfo`/`MessageInfo`/`FileUploadParams` values out of
raw MP column names (`Author_User_ID`, `From_Contact`) instead of the DTO
fields the helper requires. The tests passed because the mocks never
inspected the missing fields. Fixtures now carry explicit type
annotations so the compiler enforces the shape; `npm run build` succeeds.

Runner output: 1,507 lines -> 17, zero warnings
- Barcode tests mocked react-pdf's `View` as the string 'View', which
  React read as a capitalised unknown tag: two warnings per element, 797
  lines for one 65-bar barcode. Mocked as `rpdf-view` (the dash matters —
  any undashed unknown tag still draws "unrecognized in this browser").
- Added console spies to the 15 test files that deliberately drive error
  paths. The MP logger is unconditional by design, so these were expected
  logs plus stack traces. The spies use `mockImplementation`, so they
  still record and assertions on what was logged keep working.

Source fix (14 lines)
`Select` fields in the group wizard and Add/Edit Family passed
`value={... : undefined}`, leaving them uncontrolled until first
selection and logging a React controlled/uncontrolled warning. Silencing
that in tests would have hidden a real bug: while a field is
uncontrolled, React state is not the source of truth for it, so
`form.reset()` between wizard runs is not guaranteed to clear the
trigger. Changed to `""`, Radix's documented "no selection" value.

Also adds @testing-library/user-event (the standard RTL companion, not
previously installed).

TODOs: 8 opened, 4 resolved. 9 open — 3 high (filter injection in
FamilyService, field loss in removeGroup, address PII into logs from
mergeTemplate), 4 medium, 2 low. Docs updated: CLAUDE.md, DECISIONS.md,
testing references, and a new facts snapshot.

Verified: test:run exit 0 (1,535 passing) · test:coverage exit 0 ·
tsc --noEmit 0 errors · eslint 0 problems · next build exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@chriskehayias
chriskehayias merged commit decb1d1 into dev Sep 13, 2026
1 check passed
@chriskehayias
chriskehayias deleted the test/coverage-push-to-99-percent branch September 13, 2026 13:28
@codecov

codecov Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant