fix(test): make browser-project tests contribute to coverage measurement #495
Reference in New Issue
Block a user
Delete Branch "feat/issue-425-browser-coverage"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #425
Summary
@vitest/coverage-istanbul@4.1.0— the only provider that works inside Chromium's sandbox (v8 is silently a no-op in browser mode)vitest.client-coverage.config.ts, a standalone Vitest config that runs the browser project with Istanbul coverage writing tocoverage/client/; a standalone file is required because Vitest 4 ignores per-projectcoverageoverrides insidetest.projectsvite.config.tscoverage block to write server output tocoverage/server/(wascoverage/)test:coverageto run both projects sequentially (&&), eliminating theENOTEMPTYrace oncoverage/.tmpunit-testsjob — threshold failure exits non-zero; both lcov reports are uploaded as acoverage-reportsartifactCoverage numbers at merge
The client project starts below 80% (all Svelte components included, as decided in Theme 1). This will fail CI until component test coverage is raised — consistent with Theme 3 (hard gate from day one).
Test plan
npm test— 190 files, 1850 tests, all greennpm run test:coverage— producescoverage/server/lcov.info(v8) andcoverage/client/lcov.info(Istanbul)coverage/client/lcov.inforeferences.sveltecomponent files (not server utilities)🤖 Generated with Claude Code
👷 Tobias Wendt (@tobiwendt) — DevOps & Platform Engineer
Verdict: ✅ Approved
What I checked
What's done well
actions/upload-artifact@v4— correct, current version. No deprecated v3 sneaking in.if: always()on the upload step is exactly right. Coverage artifacts get uploaded even when the coverage gate fails, which means I can inspect reports on a failing build instead of flying blind. Good defensive move.Sequential
&&intest:coverage— the PR description explains theENOTEMPTYrace oncoverage/.tmpthat this solves. That's a real-world CI race condition and the fix is the right one. Parallelism would save maybe 30 seconds but cause intermittent failures.Two separate output directories (
coverage/server/,coverage/client/) — clean separation. One artifact archive uploads both, which keeps the artifact count low and the download simple.Minor observation
The coverage step runs on every CI push. The client run starts Playwright (Chromium headless), which has a non-trivial startup time. If CI minutes become a concern later, consider caching the Playwright browser binary more aggressively (it may already be cached via the node_modules cache key). Not a blocker — just something to monitor.
One heads-up on the failing gate
The PR ships knowing the client threshold fails at ~53%. CI will exit non-zero on
test:coverageuntil component coverage reaches 80%. TheRun coverage (server + client)step will turn red on every PR to main until that's fixed. That's a valid "hard gate from day one" strategy, but the team (in this case, just you) needs to be aware that all merges to main will have this step failing in the log until coverage is addressed. Not a blocker for this PR — it's an intentional decision — but worth tracking as a known issue.👨💻 Felix Brandt (@felixbrandt) — Senior Fullstack Developer
Verdict: ⚠️ Approved with concerns
What I checked
vitest.client-coverage.config.ts— structure, correctness, duplicationtest:coveragescript compositionBlocker
Client coverage config only gates
branches: 80— nolines,functions, orstatementsthresholds.The current
vitest.client-coverage.config.ts:This means a Svelte component could have 80% branch coverage but 10% line coverage (e.g., all branches in a single if-block are tested, but most render paths are never hit). Istanbul's branch metric alone does not guarantee meaningful test coverage. The server config likely inherits the same gap via the root
vite.config.ts.Suggest extending to:
Not asking for a stricter gate — same 80% across all dimensions — but the current setup will pass coverage with misleadingly sparse tests.
Suggestions (non-blocking)
Config duplication:
vitest.client-coverage.config.tsre-declares the full Vite plugin stack (Tailwind, SvelteKit, devtoolsJson, Paraglide) that's already invite.config.ts. The comment explains why (Vitest 4 ignores per-project coverage overrides) — that's exactly the right kind of comment. But the duplication is real tech debt: if a plugin is added or reconfigured invite.config.ts, the coverage config won't pick it up automatically.Minimal fix for now: add a note to the file header listing which plugins are mirrored from
vite.config.tsso future-you knows what to keep in sync:requireAssertions: true— good catch, this is correct hygiene for a browser test project where assertions could easily be swallowed by async mis-wiring.exclude: ['src/lib/server/**']in the client include pattern — sensible. Server-only code has no business being in the browser coverage report.The
test:coveragesequential composition (server && client) is clean and the problem it solves (ENOTEMPTY race) is documented. KISS wins here.🏛️ Markus Keller (@mkeller) — Senior Application Architect
Verdict: ✅ Approved
What I checked
Documentation update matrix
This PR changes:
Result: no doc updates required. Nothing in the CLAUDE.md doc-update matrix is triggered.
Architectural assessment
Two-provider approach is justified and well-reasoned. The PR explanation — v8 is silently a no-op inside Chromium's sandbox, Istanbul instruments source at the AST level and is sandbox-agnostic — is correct. This is not a workaround for a bad design choice; it's the correct tool for the constraints.
The standalone config file is a well-constrained workaround. Vitest 4's limitation (ignoring per-project
coverageoverrides insidetest.projects) forces the duplication. The comment invitest.client-coverage.config.tsdocuments exactly why the file exists. If Vitest fixes this in a future release, the migration path is clear: remove the standalone file and move coverage config back into the project definition. That's a good structure — the complexity is isolated and the exit path is known.Hard gate from day one is architecturally sound. Shipping the gate below threshold is the correct approach versus either (a) setting a lower threshold that doesn't reflect the goal, or (b) delaying the gate until coverage is higher. Starting with a failing gate creates immediate, visible pressure to raise coverage rather than deferring it indefinitely. The gate is transparent — the PR description documents exactly where coverage stands and what will fix it.
One concern worth flagging: the
coverage/root directory path changed fromcoverage/tocoverage/server/. If any downstream tooling (e.g., a future SonarQube integration, a code coverage badge, or a PR bot comment) was pointed atcoverage/lcov.info, it will break silently. This is unlikely to matter now in a solo project, but worth noting as a future integration gotcha.No ADR needed
The Istanbul vs v8 choice for browser testing is a tooling constraint, not an architectural decision with lasting system-design consequences. An ADR would be overkill here. The PR description itself serves as the decision record.
🔒 Nora "NullX" Steiner — Application Security Engineer
Verdict: ✅ Approved
What I checked
No security issues found
This PR is pure test infrastructure. The changed files are:
@vitest/coverage-istanbulSupply chain assessment:
@vitest/coverage-istanbul@4.1.0brings in a standard Babel toolchain (@babel/core,@babel/traverse, etc.) and Istanbul's own packages (istanbul-lib-coverage,istanbul-lib-report,istanbul-reports). These are all well-established, widely-used packages with active maintenance. The Babel dependency pinned to^7.29.0is current and not known-vulnerable. All new packages aredevDependencies— they do not ship in the production bundle.No hardcoded secrets in the CI workflow. The coverage step uses only
TZ: Europe/Berlinas an env var — not sensitive.if: always()on artifact upload is fine — coverage reports don't contain secrets or sensitive data. They reference source paths and coverage percentages.Coverage output paths (
coverage/server/,coverage/client/) are local ephemeral directories uploaded as CI artifacts. No security implications.One minor observation (not a vulnerability)
The
@vitest/coverage-istanbulpackage pulls inobug@^2.1.1as a dependency (visible in the lock file). This is an unusual dependency name — worth verifying it's the legitimate Istanbul obfuscation helper and not a typosquat. A quicknpm auditon the frontend directory would confirm no known CVEs in the new dependency tree, which I'd suggest running before merge.🧪 Sara Holt (@saraholt) — QA Engineer & Test Strategist
Verdict: ⚠️ Approved with concerns
What I checked
Blockers
1. Only
branches: 80threshold — coverage quality gapvitest.client-coverage.config.tsonly asserts on branch coverage:This means a Svelte component with 80% branch coverage but 5% line coverage passes the gate. For a browser component test suite,
linesandfunctionsare the most meaningful coverage dimensions (you want to know: is the component's render logic actually executed?). Recommend aligning thresholds across all four dimensions at the same 80% floor:2. CI gate fails at ~53% client coverage — every merge to main will have a red CI step
The PR explicitly documents this as "hard gate from day one." I understand and respect the intent: creating immediate pressure to raise coverage rather than deferring it. But the practical consequence is that CI shows a red step on every PR until coverage reaches 80%. This can normalize CI red states and make it harder to notice new failures.
Suggestion: Either (a) accept this and treat the coverage step as a known-failing gate with a tracked issue, or (b) start the threshold at the current baseline (~53%) and ratchet it up each sprint. Either approach is valid — I just want it explicitly decided, not accidentally tolerated.
What's done well
requireAssertions: trueon the browser project config — excellent. This catches async test patterns where the expect never runs (the test body completes before the assertion fires). This is a common pitfall in browser-based tests.Explicit exclude patterns in the coverage
include/excludeconfig:src/**/*.svelteandsrc/**/*.svelte.ts— correct scope for browser testsparaglide/**,generated/**,hooks/**— all correct, these are generated or framework codesrc/lib/server/**excluded from the testexcludelist — prevents server-only code from running in browser contextSequential
&&script composition — eliminates the ENOTEMPTY race documented in the PR. Clean.if: always()on artifact upload — I can inspect coverage reports on failing builds. Critical for debugging why the gate failed.Coverage artifact upload — having
coverage/client/lcov.infoandcoverage/server/lcov.infoboth in the artifact enables future integration with coverage diff tools or PR comments without further CI changes.📋 Elicit — Requirements Engineer
Verdict: ✅ Approved
What I checked
Requirement coverage
The PR description presents a clear test plan with five checkpoints:
npm test— 190 files, 1850 tests, all greennpm run test:coverageproduces both lcov filescoverage/client/lcov.inforeferences.sveltefilesThis is well-specified. The PR knows what it promises and what it doesn't.
One open question worth tracking
The PR description references "Theme 1" (all Svelte components included in coverage scope) and "Theme 3" (hard gate from day one). These appear to be decisions made in issue #425 or during planning — but they are not traceable to the issue body visible in this PR.
If "Theme 1" means "include all Svelte components even if untested, accepting a low starting coverage," that's a significant scope decision that shapes the 53% baseline. If it means "exclude generated and library code," that's less impactful. Worth verifying that the current
include/excludepatterns invitest.client-coverage.config.tsmatch the agreed scope.Scope is appropriately bounded
This PR does not attempt to raise coverage — it only makes it measurable. That's the right decomposition. The gap between 53% and 80% is a separate work item, and the failing gate creates the correct incentive to address it without blocking this PR's value from landing.
🎨 Leonie Voss (@leonievoss) — UI/UX Design Lead
Verdict: ✅ Approved
What I checked
This PR is pure test infrastructure — Vitest configuration, CI workflow, and package dependencies. No Svelte components, no UI styling, no user-facing changes.
Nothing in my domain to flag.
The only tangentially UX-relevant note: the coverage include pattern correctly targets
src/**/*.svelteandsrc/**/*.svelte.ts. This means Svelte components — the files I care about most — are now in scope for browser coverage measurement. That's the right foundation for eventually requiring tested states in components (loading, empty, error). Good to have this infrastructure in place.Review concerns addressed — commit
80ccc0f3Felix Brandt (blocker) + Sara Holt (Blocker 1) ✅
Both coverage configs only gated
branches: 80, allowing misleadingly sparse tests to pass.Extended thresholds to all four dimensions at the same 80% floor in both configs:
Applied to:
frontend/vitest.client-coverage.config.ts(client/Istanbul)frontend/vite.config.ts(server/v8)Felix Brandt (non-blocking suggestion) ✅
Added a plugin-sync comment to
vitest.client-coverage.config.tsheader:Sara Holt (Blocker 2) — no code change needed ✅
The failing CI gate (~53% client coverage) is explicitly documented in the PR description as "hard gate from day one / Theme 3." Already an explicit decision, not accidental tolerance.
Tobias Wendt (observation) + Nora Steiner (audit suggestion) — informational ✅
No code changes required. Playwright cache and
npm auditare operational concerns, not PR blockers.🤖 Generated with Claude Code