Increase browser component test coverage to ≥ 80% on all metrics (statements, lines, branches, functions) #496
Reference in New Issue
Block a user
Delete Branch "%!s()"
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?
Context
The
vitest.client-coverage.config.tscurrently enforces only abranches: 80threshold for the browser (client) Vitest project. All four Istanbul metrics must reach ≥ 80 %. The current numbers are:Branches are the hardest metric to move (26 pp gap); lines and functions each need ~10–12 pp. A test that exercises a branch almost always covers the corresponding lines, statements, and function entry — so the branch-coverage work plan below drives all four metrics simultaneously.
Quick Win — Delete the dev demo route
src/routes/demo/contains two scaffolding pages (+page.svelte,paraglide/+page.svelte) that exist only as a Paraglide i18n smoke-check from initial project setup. They are never navigated to in production or staging, carry no user-facing behaviour, and are already uncovered (0 %). Delete both files and thedemo/directory.Files to delete:
src/routes/demo/+page.sveltesrc/routes/demo/paraglide/+page.svelteMain Work — Component tests for 0 % branch files
38 source files have branch coverage of exactly 0 %. They are the fastest path to the threshold because every branch hit counts from a zero baseline.
Priority order by branch count (largest first — most leverage):
src/lib/document/DocumentTopBar.sveltesrc/routes/persons/[id]/edit/PersonEditForm.sveltesrc/routes/admin/invites/+page.sveltesrc/routes/persons/[id]/PersonCard.sveltesrc/lib/ocr/SegmentationTrainingCard.sveltesrc/routes/persons/[id]/PersonDocumentList.sveltesrc/routes/geschichten/[id]/+page.sveltesrc/routes/persons/new/+page.sveltesrc/routes/aktivitaeten/+page.sveltesrc/routes/persons/[id]/CoCorrespondentsList.sveltesrc/lib/ocr/OcrTrigger.sveltesrc/routes/documents/new/FileSectionNew.sveltesrc/routes/enrich/+page.sveltesrc/routes/enrich/[id]/+page.sveltesrc/routes/forgot-password/+page.sveltesrc/routes/reset-password/+page.sveltesrc/routes/stammbaum/+page.sveltesrc/routes/profile/+page.sveltesrc/routes/profile/PasswordChangeForm.sveltesrc/routes/profile/PersonalInfoForm.sveltesrc/routes/users/[id]/+page.sveltesrc/routes/documents/bulk-edit/+page.sveltesrc/lib/document/DocumentStatusChip.sveltesrc/lib/document/DocumentViewer.sveltesrc/lib/person/PersonChip.sveltesrc/lib/person/PersonChipRow.sveltesrc/lib/person/PersonTypeBadge.sveltesrc/lib/shared/primitives/ExpandableText.sveltesrc/routes/+error.sveltesrc/routes/briefwechsel/CorrespondentSuggestionsDropdown.sveltesrc/routes/geschichten/[id]/edit/+page.sveltesrc/routes/geschichten/new/+page.sveltesrc/routes/admin/groups/+layout.sveltesrc/routes/admin/groups/new/+page.sveltesrc/routes/admin/ocr/global/+page.sveltesrc/routes/admin/tags/+layout.sveltesrc/routes/admin/users/+layout.svelteCovering the top ~20 files at ~70 % branch hit rate yields roughly 400–450 additional branches. Combined with improvements on partially-covered files already above 0 %, all four thresholds should be reachable. If coverage progress stalls, revisit the exclude list in
vitest.client-coverage.config.tsfor any remaining dev-only artefacts.Config change required
vitest.client-coverage.config.tsmust be updated from:to:
Testing approach
Use
vitest-browser-sveltewithrender()fromvitest-browser-svelte— not JSDOM — as established in the existing.svelte.test.tsfiles. Mock only what cannot run in a headless Chromium (server-sideloadfunctions, network calls viavi.mock). Test observable behaviour (rendered text, ARIA roles, slot/prop variants) rather than internal state.Each test file lives alongside its component:
DocumentTopBar.svelte.test.tsnext toDocumentTopBar.svelte.Acceptance Criteria
Out of scope
frontend/e2e/and are not counted hereIncrease browser component test coverage to meet 80% branch thresholdto Increase browser component test coverage to ≥ 80% on all metrics (statements, lines, branches, functions)👨💻 Felix Brandt — Senior Fullstack Developer
Observations
80ccc0f3on this branch already expandedvitest.client-coverage.config.tsto enforce all four metrics at 80. The "Config change required" section of the issue spec is complete — the remaining work is purely writing the test files.UploadZone.svelte.test.tsandTranscriptionPanelHeader.svelte.test.tsare the clearest models to follow. The patterns are sound:render()fromvitest-browser-svelte,page.getByRole()/page.getByText()for assertions,vi.fn()for callback props.afterEach(cleanup)is inconsistent.TranscriptionPanelHeader.svelte.test.tscallscleanupafter each test;UploadZone.svelte.test.tsdoes not. For complex components that modify global DOM state, missing cleanup causes test pollution. Pick one pattern and stick to it — I'd go with always including it.data-testidcrept intoTranscriptionPanelHeader.svelte.test.ts(document.querySelector('[data-testid="mode-read"]')). This directly conflicts with the "test observable behaviour via ARIA roles" principle stated in the issue. UsegetByRole('button', { name: /lesen/i })instead — it tests what a screen reader announces and doesn't require production code to carry test attributes.DocumentTopBar.svelte(303 lines, 83 branches) is the hardest file on the list. At 303 lines it already violates the 60-line splitting signal. Before writing tests, consider whether any branches can be moved into smaller child components — that would both simplify the test and improve the component design. If splitting is out of scope for this issue, at minimum write separatedescribeblocks per visual state: drawer open/closed, metadata filled/empty, mobile vs. desktop overflow.requireAssertions: trueis set in the config — good guard against accidentally empty tests. Make sure every test body has at least oneawait expect.element(...)call, not just arender().Recommendations
UploadZone.svelte.test.tspattern as the canonical template:describeper visual state, oneitper observable outcome,vi.fn()for callbacks,getByRole/getByTextfor assertions. AvoidquerySelectoranddata-testid.DocumentStatusChip,+error.svelte,PasswordChangeForm,FileSectionNew) to build momentum and confirm the test harness works before tacklingDocumentTopBar(83 branches).afterEach(() => cleanup())to every new test file, and backfill it intoUploadZone.svelte.test.ts.canWriteprop (e.g.PersonCard.svelte), write separate tests for the authorized and unauthorized render — both branches count.vi.mock('$app/navigation')andvi.mock('$lib/api.server')at the top of any test file that renders a component that imports those modules — they are the most common import-time failures in browser tests here.🏗️ Markus Keller — Application Architect
Observations
vitest.client-coverage.config.tspattern is the right call. The comment in the file itself explains why it exists: Vitest 4 ignores per-projectcoverageoverrides, so a root-level config block is the only reliable way to gate on all four Istanbul metrics for the browser project. This is a pragmatic workaround for a framework limitation, not an architectural smell.src/lib/paraglide/**,src/lib/generated/**,src/hooks/**, and**/__mocks__/**are all correctly excluded — generated code, i18n outputs, and test infrastructure should not count toward coverage metrics.src/routes/demo/contains scaffolding pages with no user-facing behaviour. Deleting them reduces the measured surface area (fewer uncovered lines in the denominator), but more importantly removes dead code from the production bundle. No routing change, no redirect needed — the pages are navigated to by nothing.$libcomponents). There is no server-side logic in scope here. The test pyramid for this issue is: browser component tests only. That's appropriate — the server load functions and API calls are either already covered by backend tests or excluded by thesrc/lib/server/**exclude rule.Recommendations
vitest.client-coverage.config.tsdoesn't need updating after the demo route deletion — it currently matches on file patterns, not specific paths, so deletion alone won't affect it.chore: delete dev-only demo route — reduces uncovered denominator in browser coverage) so the coverage number jump is traceable in git history and not mistaken for a test regression.🧪 Sara Holt — QA Engineer & Test Strategist
Observations
npm run test:coveragefails, and the CIunit-testsjob is red. This is the right pressure to have — but it means nothing can merge until all four thresholds are cleared.DocumentTopBar.sveltehas 83 branches in 303 lines. Writing tests for it first risks getting stuck on a complex component while easier wins exist. I'd invert the order: start with the smallest files (4–6 branches each) to validate the test setup, build commit momentum, and make the coverage numbers move visibly — then tackle the large ones when the approach is proven.requireAssertions: trueis a good safety net — it will fail any test that forgets to assert, preventing the empty-test anti-pattern. Make sure every new test makes at least oneawait expect.element(...)call (not justrender()).OcrTrigger.svelte,SegmentationTrainingCard.svelte, andsrc/routes/admin/ocr/global/+page.sveltelikely import server-only modules or call APIs directly. Before writing tests for these, confirm what needs to be mocked (vi.mock('$lib/api.server'),vi.mock('$app/navigation'), etc.) — otherwise the test file will compile but fail at import time.Recommendations
DocumentStatusChip,PersonTypeBadge,PersonChip,ExpandableText,+error.svelte), (2) medium-complexity route pages (forgot-password,reset-password,geschichten/new,geschichten/[id]/edit), (3) large complex components (DocumentTopBar,PersonEditForm,admin/invites). Each batch should visibly move the coverage numbers.canWrite, loading flags, error states). These three cases together will hit 60–70% of branches in most components.🚀 Tobias Wendt — DevOps & Platform Engineer
Observations
unit-testsjob inci.ymlrunsnpm run test:coverageand uploads coverage reports as artifacts usingactions/upload-artifact@v4(current version — good, not the deprecated v3). The Playwright container is pinned tomcr.microsoft.com/playwright:v1.58.2-noble— reproducible and specific. No new infrastructure is needed for this issue.npm run test:coveragecallsvitest run -c vitest.client-coverage.config.ts --coverage, and that config now enforces all four thresholds at 80%. When any threshold is unmet, Vitest exits non-zero, the step fails, and the job fails. CI is currently red on this branch for that reason. This is the right gate — the branch cannot be merged until all thresholds are cleared.if: always()— this is correct. Even when the coverage step fails, the reports are preserved and downloadable. Reviewers can inspect thelcovandtextreports in the Gitea artifacts to see exactly which files and branches are still uncovered. This is a useful debugging tool during the implementation phase.vitest.client-coverage.config.tsis invoked explicitly bytest:coverage— it does not conflict with the mainvitest.config.tsused bynpm test. The two configs run separate test phases with no overlap.Recommendations
npm run test:coveragelocally and confirm the exit code is 0 before opening the PR. The CI will confirm it, but catching it locally saves a round-trip.lcovreporter output incoverage/client/is the one most useful for inspecting uncovered lines. Download the artifact from the CI run, openlcov-report/index.htmlin a browser, and use it to identify which specific branches in each file are still uncovered — much faster than reading the text reporter output in CI logs.docker-compose.yml,Caddyfile, or production infrastructure are required. This is a pure dev/CI concern.🔐 Nora "NullX" Steiner — Application Security Engineer
Observations
src/routes/demo/pages are in the production build (SvelteKit includes all routes by default). They serve no user-facing purpose and represent dead code in the attack surface. Deleting them is the right call — less code, less exposure, smaller bundle.PersonCard.sveltetakes acanWriteprop;DocumentTopBar.sveltelikely does too. When writing tests for these components, test both the authorized and unauthorized rendering paths explicitly. This validates that write-action controls (edit buttons, delete links, etc.) are not rendered for users without the appropriate permission — a pattern that matters even at the component level, since a rendered button that POSTs to a@RequirePermission-protected endpoint is a potential CSRF surface if the permission check fails.vi.mock()and pass data as props. No real backend is touched. This is the correct pattern.Recommendations
{#if canWrite}is accidentally removed.📋 Elicit — Requirements Engineer
Observations
The issue spec is unusually well-written for an internal engineering ticket — quantified baseline numbers, a prioritised file list with branch estimates, working test commands, and Gherkin acceptance criteria. Most of what a developer needs is here. A few gaps are worth flagging before work starts.
Gap 1 — Statements baseline is missing. The issue explicitly says "Statements were not captured in the original snapshot; run
npm run test:coverageon the client project to fill in that row." This means the starting statements percentage is unknown. Since statements is now a gated threshold (≥ 80%), implementation could finish all branch work and still fail CI if statements happen to land below 80. The statements number should be captured once at the start of the issue and noted so there's no surprise at the end.Gap 2 — The file list is a point-in-time snapshot. The 38 zero-coverage files were identified at a specific moment. If new
.sveltefiles were added to the project after that snapshot (or are added during this issue's implementation), they won't be on the list — but they will count against the threshold. The acceptance criteria should read: "All four thresholds pass as measured bynpm run test:coverageat the time of the PR review", not "all 38 files have tests". (The issue's Gherkin AC already expresses this correctly — this is just a reminder not to treat the file list as exhaustive.)Gap 3 — "If coverage progress stalls, revisit the exclude list" is undefined. The fallback strategy is mentioned but not specified. What would a legitimate addition to the exclude list look like? (Admin/OCR pages with complex server dependencies? Generated component wrappers?) Without a criterion, this becomes an open door to padding the threshold by excluding legitimate source code. A concrete rule would be: "Files may be added to the exclude list only if they contain no user-observable behaviour independent of server data".
Gap 4 — No definition of "done" for the demo route deletion. The AC says the files are deleted and no import references them. A
grep -r "routes/demo"check would confirm this. Consider adding it as an explicit verification step.Recommendations
SegmentationTrainingCard,src/routes/admin/ocr/global/+page.svelte) are in scope for this issue or whether they should be excluded from coverage measurement due to dependency complexity. Document that decision in the issue or PR description.🎨 Leonie Voss — UX Designer & Accessibility Strategist
Observations
getByRole()andgetByText(), each test implicitly verifies that the component has the correct semantic structure to be found by role. A test that usesgetByRole('heading'),getByRole('button', { name: /save/i }), orgetByRole('status')is both a behavior test and an implicit accessibility check — the component must have the correct ARIA role for the assertion to pass. This is a free accessibility audit layered into coverage work.getByText()for non-translatable strings. Several components use Paraglide i18n (m.save(),m.edit(), etc.). In browser tests, Paraglide renders the actual German string at test time. Tests likegetByText('Speichern')work today but break if the German copy changes. PrefergetByRole('button', { name: /speichern/i })with a loose regex, or usegetByTestIdas a last resort for interactive elements with no unique accessible name.Recommendations
getByRoleassertions that verify meaningful ARIA structure overgetByTextfor UI strings. Example: