audit report: factory vi.mock → prop-injection / __mocks__ migration (87 call sites, 12 modules) #560

Open
opened 2026-05-14 10:37:24 +02:00 by marcel · 0 comments
Owner

Closes #554.

Full audit of all factory-style vi.mock(module, factory) call sites in frontend/src/**/*.svelte.{spec,test}.ts, as scoped by #554. Methodology: exhaustive grep -rn "vi\.mock(" across *.spec.ts and *.test.ts, then filtered to .svelte.{spec,test}.ts files and classified per the three-bucket schema.

Reference: ADR-012 — Browser-mode test mocking strategy

Scope

  • In-scope: src/**/*.svelte.spec.ts and src/**/*.svelte.test.ts87 call sites across 12 mocked modules
  • Out-of-scope (noted below): *.server.spec.ts, __meta__/*.test.ts, and non-svelte *.spec.ts — run in Node mode, birpc race concern from ADR-012 does not apply

The issue estimated ~92; the actual count is 87 in-scope call sites across 72 distinct test files.


Classification Table

Files are relative to frontend/src/.

(b) __mocks__/ redirect — 72 call sites, 7 modules

Stable, never-changing fakes. One shared file per module eliminates duplicate factory boilerplate and removes the birpc race surface for these modules entirely.

$app/forms — 13 call sites — start here

Every factory is byte-for-byte identical: { enhance: () => () => {} }. One __mocks__/$app/forms.ts file and a single-pass removal across 13 files.

File Module Class Rationale
lib/person/genealogy/StammbaumCard.svelte.spec.ts $app/forms (b) Factory always { enhance: () => () => {} } — zero per-test variation
lib/person/genealogy/StammbaumSidePanel.svelte.spec.ts $app/forms (b) Same stub, no assertions on enhance
lib/person/relationship/AddRelationshipForm.svelte.spec.ts $app/forms (b) Same stub
lib/person/relationship/RelationshipChip.svelte.spec.ts $app/forms (b) Same stub
routes/admin/groups/[id]/page.svelte.spec.ts $app/forms (b) Same stub
routes/admin/tags/[id]/page.svelte.spec.ts $app/forms (b) Same stub
routes/admin/tags/[id]/TagDeleteGuard.svelte.spec.ts $app/forms (b) Same stub
routes/admin/tags/[id]/TagMergeZone.svelte.spec.ts $app/forms (b) Same stub
routes/admin/users/[id]/page.svelte.spec.ts $app/forms (b) Same stub
routes/admin/users/new/page.svelte.spec.ts $app/forms (b) Same stub
routes/persons/[id]/edit/NameHistoryEditCard.svelte.spec.ts $app/forms (b) Same stub
routes/persons/[id]/edit/NameHistoryEditCard.svelte.test.ts $app/forms (b) Same stub
routes/persons/[id]/PersonMergePanel.svelte.spec.ts $app/forms (b) Same stub

$env/static/public — 1 call site

File Module Class Rationale
routes/layout.svelte.spec.ts $env/static/public (b) Static build-time env — stable fake value, single consumer

$lib/paraglide/runtime — 1 call site

File Module Class Rationale
lib/shared/primitives/LanguageSwitcher.svelte.spec.ts $lib/paraglide/runtime (b) Fixed locale stub { getLocale: () => 'de', setLocale: vi.fn() } — single consumer, never changes

$lib/paraglide/messages.js — 1 in-scope call site

File Module Class Rationale
lib/shared/help/TranscribeCoachEmptyState.svelte.spec.ts $lib/paraglide/messages.js (b) i18n messages module; __mocks__ version returns key-as-value identity functions — same module also used in lib/ocr/translateOcrProgress.spec.ts (out of scope, but shared __mocks__ file covers it too)

$app/stores — 2 call sites

$app/stores is the legacy SvelteKit store API superseded by $app/state. Only two files still use it. Preferred approach: first migrate the host component (admin/tags/[id]/+page.svelte) from $app/stores to $app/state so the mock disappears automatically; add __mocks__/$app/stores.ts only as a safety net if stragglers remain.

File Module Class Rationale
routes/admin/tags/[id]/page.svelte.spec.ts $app/stores (b) SvelteKit legacy virtual module; writable store mock — __mocks__ exports a writable store, tests .set() before render
routes/admin/tags/[id]/page.svelte.test.ts $app/stores (b) Same — both mocks serve the same writable page-store interface

$app/state — 18 call sites

Factories vary by which properties each test needs (navigating, page, page.url), but all return plain object stubs. A __mocks__/$app/state.ts with getter-based proxies for navigating and page (backed by module-level variables tests can assign) covers every consumer.

File Module Class Rationale
lib/document/EnrichmentBlock.svelte.spec.ts $app/state (b) Stubs navigating.to
routes/admin/entity-nav.svelte.spec.ts $app/state (b) Stubs page.url.pathname
routes/admin/EntityNav.svelte.test.ts $app/state (b) Stubs page.url.pathname
routes/admin/groups/GroupsListPanel.svelte.test.ts $app/state (b) Stubs page.url
routes/admin/groups/layout.svelte.spec.ts $app/state (b) Stubs page.url.pathname
routes/admin/layout.svelte.spec.ts $app/state (b) Stubs page.url.pathname
routes/admin/tags/layout.svelte.spec.ts $app/state (b) Stubs page.url.pathname
routes/admin/tags/TagTreeNode.svelte.test.ts $app/state (b) Stubs page.url.pathname
routes/admin/users/layout.svelte.spec.ts $app/state (b) Stubs page.url.pathname
routes/admin/users/UsersListPanel.svelte.test.ts $app/state (b) Stubs page.url
routes/aktivitaeten/page.svelte.test.ts $app/state (b) Stubs both navigating and page.url
routes/AppNav.svelte.test.ts $app/state (b) Stubs page.url.pathname
routes/documents/[id]/page.svelte.test.ts $app/state (b) Stubs page.url
routes/documents/page.svelte.spec.ts $app/state (b) Stubs navigating.to
routes/documents/page.svelte.test.ts $app/state (b) Stubs both navigating and page.url
routes/error.svelte.test.ts $app/state (b) Stubs page.error and page.status
routes/geschichten/page.svelte.spec.ts $app/state (b) Stubs navigating.to
routes/stammbaum/page.svelte.test.ts $app/state (b) Stubs page.url

$app/navigation — 36 call sites

The largest group. Factories differ in which nav functions they stub (goto, invalidateAll, beforeNavigate, etc.), but all provide vi.fn() stubs. A __mocks__/$app/navigation.ts that exports every known function as vi.fn() lets each test import and spy on only what it needs, with vi.clearAllMocks() in afterEach.

File Module Class Rationale
lib/document/BulkDocumentEditLayout.svelte.spec.ts $app/navigation (b) goto stub only
lib/document/BulkSelectionBar.svelte.spec.ts $app/navigation (b) goto stub only
lib/document/DocumentRow.svelte.spec.ts $app/navigation (b) goto stub only
lib/document/DocumentRow.svelte.test.ts $app/navigation (b) goto + invalidateAll stubs
lib/notification/NotificationBell.svelte.spec.ts $app/navigation (b) goto + beforeNavigate stubs
lib/notification/NotificationDropdown.svelte.test.ts $app/navigation (b) goto stub only
lib/person/genealogy/StammbaumSidePanel.svelte.spec.ts $app/navigation (b) invalidateAll stub only
lib/person/genealogy/StammbaumSidePanel.svelte.test.ts $app/navigation (b) Full nav function set
lib/shared/hooks/useUnsavedWarning.svelte.test.ts $app/navigation (b) beforeNavigate stub
routes/admin/groups/[id]/page.svelte.spec.ts $app/navigation (b) beforeNavigate + goto stubs
routes/admin/groups/new/page.svelte.test.ts $app/navigation (b) Multiple nav stubs
routes/admin/page.svelte.spec.ts $app/navigation (b) goto stub only
routes/admin/page.svelte.test.ts $app/navigation (b) Multiple nav stubs
routes/admin/tags/[id]/page.svelte.spec.ts $app/navigation (b) beforeNavigate + goto + replaceState stubs
routes/admin/tags/[id]/page.svelte.test.ts $app/navigation (b) Full nav stub set
routes/admin/users/[id]/page.svelte.spec.ts $app/navigation (b) beforeNavigate + goto stubs
routes/admin/users/new/page.svelte.test.ts $app/navigation (b) Multiple nav stubs
routes/aktivitaeten/page.svelte.test.ts $app/navigation (b) Full nav function set
routes/briefwechsel/CorrespondenzHero.svelte.spec.ts $app/navigation (b) goto stub only
routes/briefwechsel/page.svelte.spec.ts $app/navigation (b) goto stub only
routes/briefwechsel/page.svelte.test.ts $app/navigation (b) Multiple nav stubs
routes/DocumentList.svelte.spec.ts $app/navigation (b) goto stub only
routes/DocumentList.svelte.test.ts $app/navigation (b) Multiple nav stubs
routes/documents/bulk-edit/page.svelte.test.ts $app/navigation (b) Multiple nav stubs
routes/documents/[id]/page.svelte.test.ts $app/navigation (b) Multiple nav stubs
routes/documents/page.svelte.spec.ts $app/navigation (b) goto stub only
routes/documents/page.svelte.test.ts $app/navigation (b) Multiple nav stubs
routes/DropZone.svelte.spec.ts $app/navigation (b) invalidateAll stub only
routes/DropZone.svelte.test.ts $app/navigation (b) Multiple nav stubs
routes/geschichten/[id]/edit/page.svelte.test.ts $app/navigation (b) Multiple nav stubs
routes/geschichten/new/page.svelte.test.ts $app/navigation (b) Multiple nav stubs
routes/geschichten/page.svelte.spec.ts $app/navigation (b) goto stub only
routes/geschichten/page.svelte.test.ts $app/navigation (b) Multiple nav stubs
routes/page.svelte.spec.ts $app/navigation (b) goto + invalidateAll stubs
routes/persons/page.svelte.spec.ts $app/navigation (b) goto stub only
routes/persons/page.svelte.test.ts $app/navigation (b) Multiple nav stubs

(a) Prop-injection — 10 call sites, 2 modules

Domain services with a provider pattern. Replace the factory mock with a .test-host.svelte wrapper that provides the service via context, then thread the service instance to the test via a prop callback (onReady).

$lib/shared/services/confirm.svelte — 8 call sites

confirm.test-host.svelte already exists (lib/shared/services/confirm.test-host.svelte). Migration is mechanical: render the test-host wrapping the component under test, capture the service via onReady, remove the vi.mock call.

File Module Class Rationale
lib/document/transcription/TranscriptionEditView.svelte.test.ts $lib/shared/services/confirm.svelte (a) Provider pattern; test-host exists
routes/admin/groups/[id]/page.svelte.test.ts $lib/shared/services/confirm.svelte (a) Provider pattern; test-host exists
routes/admin/tags/[id]/page.svelte.test.ts $lib/shared/services/confirm.svelte (a) Provider pattern; test-host exists
routes/admin/users/[id]/page.svelte.test.ts $lib/shared/services/confirm.svelte (a) Provider pattern; test-host exists
routes/documents/[id]/edit/page.svelte.test.ts $lib/shared/services/confirm.svelte (a) Provider pattern; test-host exists
routes/documents/[id]/page.svelte.test.ts $lib/shared/services/confirm.svelte (a) Provider pattern; test-host exists
routes/geschichten/[id]/page.svelte.test.ts $lib/shared/services/confirm.svelte (a) Provider pattern; test-host exists
routes/persons/[id]/edit/page.svelte.test.ts $lib/shared/services/confirm.svelte (a) Provider pattern; test-host exists

$lib/notification/notifications.svelte — 2 call sites

No test-host yet. notificationStore is a stateful service. Needs a new notification.test-host.svelte (modelled on confirm.test-host.svelte) before migration can begin.

File Module Class Rationale
lib/notification/NotificationBell.svelte.spec.ts $lib/notification/notifications.svelte (a) Stateful service with provider — needs test-host first; current mock uses vi.hoisted closures for per-test notifications list
routes/aktivitaeten/page.svelte.test.ts $lib/notification/notifications.svelte (a) Same service, simpler stub (static array + lifecycle stubs)

(c) Keep as factory — 5 call sites, 3 modules

Component stubs (default: () => null). No provider pattern exists for Svelte components. Factory is the correct and low-risk approach here.

File Module Class Rationale
lib/person/genealogy/StammbaumSidePanel.svelte.spec.ts $lib/person/PersonTypeahead.svelte (c) Component stub — no injection seam; default: () => null is idiomatic
lib/person/relationship/AddRelationshipForm.svelte.spec.ts $lib/person/PersonTypeahead.svelte (c) Component stub
routes/persons/[id]/PersonMergePanel.svelte.spec.ts $lib/person/PersonTypeahead.svelte (c) Component stub
lib/person/genealogy/StammbaumCard.svelte.spec.ts $lib/person/relationship/RelationshipChip.svelte (c) Child component stub — rendered inside the card
lib/person/genealogy/StammbaumCard.svelte.spec.ts $lib/person/relationship/AddRelationshipForm.svelte (c) Child component stub — same rationale

Summary

Class Modules Call sites
(b) __mocks__/ redirect 7 72
(a) prop-injection 2 10
(c) keep as factory 3 5
Total 12 87

Out-of-scope findings

Files outside *.svelte.{spec,test}.ts — run in Node mode, no birpc concern — noted for completeness.

Module Files Suggested class Notes
$lib/shared/api.server 15 (.server.spec.ts) (c) keep Per-test mock API responses required; no injection seam viable
$env/dynamic/private 1 (documents/[id]/page.server.spec.ts) (c) keep Server-only env variable
$lib/shared/errors 1 (briefwechsel/page.server.spec.ts) (c) keep Per-test error code shaping needed
$lib/paraglide/messages.js 1 (lib/ocr/translateOcrProgress.spec.ts) (b) candidate Same __mocks__ file from in-scope migration covers this consumer too

Roadmap

Phase 1 — __mocks__/ redirect (class b) — ~2 days

Pure file additions plus factory removal. No test-logic changes needed.

Priority within phase 1:

  1. $app/forms (13 files) — start here; factory is always identical, zero risk. Create frontend/src/__mocks__/$app/forms.ts:

    export const enhance = () => () => {};
    

    Remove all 13 vi.mock('$app/forms', ...) calls in one pass.

  2. $env/static/public (1 file) — trivial; create frontend/src/__mocks__/$env/static/public.ts with the poll-interval constant.

  3. $lib/paraglide/runtime (1 file) — create frontend/src/__mocks__/$lib/paraglide/runtime.ts with { getLocale: () => 'de', setLocale: vi.fn() }.

  4. $lib/paraglide/messages.js (1 in-scope + 1 out-of-scope consumer) — create frontend/src/__mocks__/$lib/paraglide/messages.js with identity functions for all message keys; covers both consumers simultaneously.

  5. $app/state (18 files) — create frontend/src/__mocks__/$app/state.ts with getter-based proxies backed by module-level variables:

    export let _navigating = { type: null, to: null };
    export let _page = { url: new URL('http://localhost/'), error: null, status: 200 };
    export const navigating = { get type() { return _navigating.type; }, get to() { return _navigating.to; } };
    export const page = { get url() { return _page.url; }, get error() { return _page.error; }, get status() { return _page.status; } };
    

    Tests that need custom values import _navigating/_page and assign before render.

  6. $app/navigation (36 files, largest batch) — create frontend/src/__mocks__/$app/navigation.ts exporting all known nav functions as vi.fn(). Tests import the mock functions for assertions; add vi.clearAllMocks() in afterEach. Functions needed: goto, invalidate, invalidateAll, beforeNavigate, afterNavigate, preloadCode, preloadData, pushState, replaceState, disableScrollHandling, onNavigate.

  7. $app/stores (2 files) — first check if admin/tags/[id]/+page.svelte uses $app/stores and can be migrated to $app/state; if yes, both mocks disappear automatically. Otherwise add frontend/src/__mocks__/$app/stores.ts.

Phase 2 — prop-injection (class a) — ~3 days

Requires creating or extending test-host components.

  1. $lib/shared/services/confirm.svelte (8 files) — test-host already exists at lib/shared/services/confirm.test-host.svelte. For each of the 8 files: wrap the component under test in the test-host, capture the ConfirmService via onReady, then remove the vi.mock call. ~0.5 day per batch of similar files.

  2. $lib/notification/notifications.svelte (2 files) — create lib/notification/notification.test-host.svelte modelled on confirm.test-host.svelte; it should call provideNotificationStore(), accept onReady: (store) => void, and render <NotificationDropdown /> (or similar provider component). Then migrate both spec files (~1 day total).

Phase 3 — keep as factory (class c) — 0 effort

5 call sites, no migration. Revisit if a component-DI pattern emerges in Svelte 5 (e.g. snippet injection as a prop).

Total scope

Phase Class New files Factories removed Effort
1 (b) 7 __mocks__ files 72 ~2 days
2 (a) 1 test-host (notification) 10 ~3 days
3 (c) 0
Total 8 82 ~5 days

Audit produced 2026-05-14. All paths relative to frontend/src/. Based on main branch state at audit time.

Closes #554. Full audit of all factory-style `vi.mock(module, factory)` call sites in `frontend/src/**/*.svelte.{spec,test}.ts`, as scoped by #554. Methodology: exhaustive `grep -rn "vi\.mock("` across `*.spec.ts` and `*.test.ts`, then filtered to `.svelte.{spec,test}.ts` files and classified per the three-bucket schema. Reference: [ADR-012 — Browser-mode test mocking strategy](../src/branch/main/docs/adr/012-browser-test-mocking-strategy.md) ## Scope - **In-scope**: `src/**/*.svelte.spec.ts` and `src/**/*.svelte.test.ts` — **87 call sites** across **12 mocked modules** - **Out-of-scope (noted below)**: `*.server.spec.ts`, `__meta__/*.test.ts`, and non-svelte `*.spec.ts` — run in Node mode, birpc race concern from ADR-012 does not apply > The issue estimated ~92; the actual count is **87** in-scope call sites across 72 distinct test files. --- ## Classification Table Files are relative to `frontend/src/`. ### (b) `__mocks__/` redirect — 72 call sites, 7 modules Stable, never-changing fakes. One shared file per module eliminates duplicate factory boilerplate and removes the birpc race surface for these modules entirely. #### `$app/forms` — 13 call sites — start here Every factory is byte-for-byte identical: `{ enhance: () => () => {} }`. One `__mocks__/$app/forms.ts` file and a single-pass removal across 13 files. | File | Module | Class | Rationale | |---|---|---|---| | `lib/person/genealogy/StammbaumCard.svelte.spec.ts` | `$app/forms` | (b) | Factory always `{ enhance: () => () => {} }` — zero per-test variation | | `lib/person/genealogy/StammbaumSidePanel.svelte.spec.ts` | `$app/forms` | (b) | Same stub, no assertions on `enhance` | | `lib/person/relationship/AddRelationshipForm.svelte.spec.ts` | `$app/forms` | (b) | Same stub | | `lib/person/relationship/RelationshipChip.svelte.spec.ts` | `$app/forms` | (b) | Same stub | | `routes/admin/groups/[id]/page.svelte.spec.ts` | `$app/forms` | (b) | Same stub | | `routes/admin/tags/[id]/page.svelte.spec.ts` | `$app/forms` | (b) | Same stub | | `routes/admin/tags/[id]/TagDeleteGuard.svelte.spec.ts` | `$app/forms` | (b) | Same stub | | `routes/admin/tags/[id]/TagMergeZone.svelte.spec.ts` | `$app/forms` | (b) | Same stub | | `routes/admin/users/[id]/page.svelte.spec.ts` | `$app/forms` | (b) | Same stub | | `routes/admin/users/new/page.svelte.spec.ts` | `$app/forms` | (b) | Same stub | | `routes/persons/[id]/edit/NameHistoryEditCard.svelte.spec.ts` | `$app/forms` | (b) | Same stub | | `routes/persons/[id]/edit/NameHistoryEditCard.svelte.test.ts` | `$app/forms` | (b) | Same stub | | `routes/persons/[id]/PersonMergePanel.svelte.spec.ts` | `$app/forms` | (b) | Same stub | #### `$env/static/public` — 1 call site | File | Module | Class | Rationale | |---|---|---|---| | `routes/layout.svelte.spec.ts` | `$env/static/public` | (b) | Static build-time env — stable fake value, single consumer | #### `$lib/paraglide/runtime` — 1 call site | File | Module | Class | Rationale | |---|---|---|---| | `lib/shared/primitives/LanguageSwitcher.svelte.spec.ts` | `$lib/paraglide/runtime` | (b) | Fixed locale stub `{ getLocale: () => 'de', setLocale: vi.fn() }` — single consumer, never changes | #### `$lib/paraglide/messages.js` — 1 in-scope call site | File | Module | Class | Rationale | |---|---|---|---| | `lib/shared/help/TranscribeCoachEmptyState.svelte.spec.ts` | `$lib/paraglide/messages.js` | (b) | i18n messages module; `__mocks__` version returns key-as-value identity functions — same module also used in `lib/ocr/translateOcrProgress.spec.ts` (out of scope, but shared `__mocks__` file covers it too) | #### `$app/stores` — 2 call sites `$app/stores` is the legacy SvelteKit store API superseded by `$app/state`. Only two files still use it. Preferred approach: first migrate the host component (`admin/tags/[id]/+page.svelte`) from `$app/stores` to `$app/state` so the mock disappears automatically; add `__mocks__/$app/stores.ts` only as a safety net if stragglers remain. | File | Module | Class | Rationale | |---|---|---|---| | `routes/admin/tags/[id]/page.svelte.spec.ts` | `$app/stores` | (b) | SvelteKit legacy virtual module; writable store mock — `__mocks__` exports a writable store, tests `.set()` before render | | `routes/admin/tags/[id]/page.svelte.test.ts` | `$app/stores` | (b) | Same — both mocks serve the same writable page-store interface | #### `$app/state` — 18 call sites Factories vary by which properties each test needs (`navigating`, `page`, `page.url`), but all return plain object stubs. A `__mocks__/$app/state.ts` with getter-based proxies for `navigating` and `page` (backed by module-level variables tests can assign) covers every consumer. | File | Module | Class | Rationale | |---|---|---|---| | `lib/document/EnrichmentBlock.svelte.spec.ts` | `$app/state` | (b) | Stubs `navigating.to` | | `routes/admin/entity-nav.svelte.spec.ts` | `$app/state` | (b) | Stubs `page.url.pathname` | | `routes/admin/EntityNav.svelte.test.ts` | `$app/state` | (b) | Stubs `page.url.pathname` | | `routes/admin/groups/GroupsListPanel.svelte.test.ts` | `$app/state` | (b) | Stubs `page.url` | | `routes/admin/groups/layout.svelte.spec.ts` | `$app/state` | (b) | Stubs `page.url.pathname` | | `routes/admin/layout.svelte.spec.ts` | `$app/state` | (b) | Stubs `page.url.pathname` | | `routes/admin/tags/layout.svelte.spec.ts` | `$app/state` | (b) | Stubs `page.url.pathname` | | `routes/admin/tags/TagTreeNode.svelte.test.ts` | `$app/state` | (b) | Stubs `page.url.pathname` | | `routes/admin/users/layout.svelte.spec.ts` | `$app/state` | (b) | Stubs `page.url.pathname` | | `routes/admin/users/UsersListPanel.svelte.test.ts` | `$app/state` | (b) | Stubs `page.url` | | `routes/aktivitaeten/page.svelte.test.ts` | `$app/state` | (b) | Stubs both `navigating` and `page.url` | | `routes/AppNav.svelte.test.ts` | `$app/state` | (b) | Stubs `page.url.pathname` | | `routes/documents/[id]/page.svelte.test.ts` | `$app/state` | (b) | Stubs `page.url` | | `routes/documents/page.svelte.spec.ts` | `$app/state` | (b) | Stubs `navigating.to` | | `routes/documents/page.svelte.test.ts` | `$app/state` | (b) | Stubs both `navigating` and `page.url` | | `routes/error.svelte.test.ts` | `$app/state` | (b) | Stubs `page.error` and `page.status` | | `routes/geschichten/page.svelte.spec.ts` | `$app/state` | (b) | Stubs `navigating.to` | | `routes/stammbaum/page.svelte.test.ts` | `$app/state` | (b) | Stubs `page.url` | #### `$app/navigation` — 36 call sites The largest group. Factories differ in which nav functions they stub (`goto`, `invalidateAll`, `beforeNavigate`, etc.), but all provide `vi.fn()` stubs. A `__mocks__/$app/navigation.ts` that exports every known function as `vi.fn()` lets each test import and spy on only what it needs, with `vi.clearAllMocks()` in `afterEach`. | File | Module | Class | Rationale | |---|---|---|---| | `lib/document/BulkDocumentEditLayout.svelte.spec.ts` | `$app/navigation` | (b) | `goto` stub only | | `lib/document/BulkSelectionBar.svelte.spec.ts` | `$app/navigation` | (b) | `goto` stub only | | `lib/document/DocumentRow.svelte.spec.ts` | `$app/navigation` | (b) | `goto` stub only | | `lib/document/DocumentRow.svelte.test.ts` | `$app/navigation` | (b) | `goto` + `invalidateAll` stubs | | `lib/notification/NotificationBell.svelte.spec.ts` | `$app/navigation` | (b) | `goto` + `beforeNavigate` stubs | | `lib/notification/NotificationDropdown.svelte.test.ts` | `$app/navigation` | (b) | `goto` stub only | | `lib/person/genealogy/StammbaumSidePanel.svelte.spec.ts` | `$app/navigation` | (b) | `invalidateAll` stub only | | `lib/person/genealogy/StammbaumSidePanel.svelte.test.ts` | `$app/navigation` | (b) | Full nav function set | | `lib/shared/hooks/useUnsavedWarning.svelte.test.ts` | `$app/navigation` | (b) | `beforeNavigate` stub | | `routes/admin/groups/[id]/page.svelte.spec.ts` | `$app/navigation` | (b) | `beforeNavigate` + `goto` stubs | | `routes/admin/groups/new/page.svelte.test.ts` | `$app/navigation` | (b) | Multiple nav stubs | | `routes/admin/page.svelte.spec.ts` | `$app/navigation` | (b) | `goto` stub only | | `routes/admin/page.svelte.test.ts` | `$app/navigation` | (b) | Multiple nav stubs | | `routes/admin/tags/[id]/page.svelte.spec.ts` | `$app/navigation` | (b) | `beforeNavigate` + `goto` + `replaceState` stubs | | `routes/admin/tags/[id]/page.svelte.test.ts` | `$app/navigation` | (b) | Full nav stub set | | `routes/admin/users/[id]/page.svelte.spec.ts` | `$app/navigation` | (b) | `beforeNavigate` + `goto` stubs | | `routes/admin/users/new/page.svelte.test.ts` | `$app/navigation` | (b) | Multiple nav stubs | | `routes/aktivitaeten/page.svelte.test.ts` | `$app/navigation` | (b) | Full nav function set | | `routes/briefwechsel/CorrespondenzHero.svelte.spec.ts` | `$app/navigation` | (b) | `goto` stub only | | `routes/briefwechsel/page.svelte.spec.ts` | `$app/navigation` | (b) | `goto` stub only | | `routes/briefwechsel/page.svelte.test.ts` | `$app/navigation` | (b) | Multiple nav stubs | | `routes/DocumentList.svelte.spec.ts` | `$app/navigation` | (b) | `goto` stub only | | `routes/DocumentList.svelte.test.ts` | `$app/navigation` | (b) | Multiple nav stubs | | `routes/documents/bulk-edit/page.svelte.test.ts` | `$app/navigation` | (b) | Multiple nav stubs | | `routes/documents/[id]/page.svelte.test.ts` | `$app/navigation` | (b) | Multiple nav stubs | | `routes/documents/page.svelte.spec.ts` | `$app/navigation` | (b) | `goto` stub only | | `routes/documents/page.svelte.test.ts` | `$app/navigation` | (b) | Multiple nav stubs | | `routes/DropZone.svelte.spec.ts` | `$app/navigation` | (b) | `invalidateAll` stub only | | `routes/DropZone.svelte.test.ts` | `$app/navigation` | (b) | Multiple nav stubs | | `routes/geschichten/[id]/edit/page.svelte.test.ts` | `$app/navigation` | (b) | Multiple nav stubs | | `routes/geschichten/new/page.svelte.test.ts` | `$app/navigation` | (b) | Multiple nav stubs | | `routes/geschichten/page.svelte.spec.ts` | `$app/navigation` | (b) | `goto` stub only | | `routes/geschichten/page.svelte.test.ts` | `$app/navigation` | (b) | Multiple nav stubs | | `routes/page.svelte.spec.ts` | `$app/navigation` | (b) | `goto` + `invalidateAll` stubs | | `routes/persons/page.svelte.spec.ts` | `$app/navigation` | (b) | `goto` stub only | | `routes/persons/page.svelte.test.ts` | `$app/navigation` | (b) | Multiple nav stubs | --- ### (a) Prop-injection — 10 call sites, 2 modules Domain services with a provider pattern. Replace the factory mock with a `.test-host.svelte` wrapper that provides the service via context, then thread the service instance to the test via a prop callback (`onReady`). #### `$lib/shared/services/confirm.svelte` — 8 call sites `confirm.test-host.svelte` **already exists** (`lib/shared/services/confirm.test-host.svelte`). Migration is mechanical: render the test-host wrapping the component under test, capture the service via `onReady`, remove the `vi.mock` call. | File | Module | Class | Rationale | |---|---|---|---| | `lib/document/transcription/TranscriptionEditView.svelte.test.ts` | `$lib/shared/services/confirm.svelte` | (a) | Provider pattern; test-host exists | | `routes/admin/groups/[id]/page.svelte.test.ts` | `$lib/shared/services/confirm.svelte` | (a) | Provider pattern; test-host exists | | `routes/admin/tags/[id]/page.svelte.test.ts` | `$lib/shared/services/confirm.svelte` | (a) | Provider pattern; test-host exists | | `routes/admin/users/[id]/page.svelte.test.ts` | `$lib/shared/services/confirm.svelte` | (a) | Provider pattern; test-host exists | | `routes/documents/[id]/edit/page.svelte.test.ts` | `$lib/shared/services/confirm.svelte` | (a) | Provider pattern; test-host exists | | `routes/documents/[id]/page.svelte.test.ts` | `$lib/shared/services/confirm.svelte` | (a) | Provider pattern; test-host exists | | `routes/geschichten/[id]/page.svelte.test.ts` | `$lib/shared/services/confirm.svelte` | (a) | Provider pattern; test-host exists | | `routes/persons/[id]/edit/page.svelte.test.ts` | `$lib/shared/services/confirm.svelte` | (a) | Provider pattern; test-host exists | #### `$lib/notification/notifications.svelte` — 2 call sites No test-host yet. `notificationStore` is a stateful service. Needs a new `notification.test-host.svelte` (modelled on `confirm.test-host.svelte`) before migration can begin. | File | Module | Class | Rationale | |---|---|---|---| | `lib/notification/NotificationBell.svelte.spec.ts` | `$lib/notification/notifications.svelte` | (a) | Stateful service with provider — needs test-host first; current mock uses `vi.hoisted` closures for per-test `notifications` list | | `routes/aktivitaeten/page.svelte.test.ts` | `$lib/notification/notifications.svelte` | (a) | Same service, simpler stub (static array + lifecycle stubs) | --- ### (c) Keep as factory — 5 call sites, 3 modules Component stubs (`default: () => null`). No provider pattern exists for Svelte components. Factory is the correct and low-risk approach here. | File | Module | Class | Rationale | |---|---|---|---| | `lib/person/genealogy/StammbaumSidePanel.svelte.spec.ts` | `$lib/person/PersonTypeahead.svelte` | (c) | Component stub — no injection seam; `default: () => null` is idiomatic | | `lib/person/relationship/AddRelationshipForm.svelte.spec.ts` | `$lib/person/PersonTypeahead.svelte` | (c) | Component stub | | `routes/persons/[id]/PersonMergePanel.svelte.spec.ts` | `$lib/person/PersonTypeahead.svelte` | (c) | Component stub | | `lib/person/genealogy/StammbaumCard.svelte.spec.ts` | `$lib/person/relationship/RelationshipChip.svelte` | (c) | Child component stub — rendered inside the card | | `lib/person/genealogy/StammbaumCard.svelte.spec.ts` | `$lib/person/relationship/AddRelationshipForm.svelte` | (c) | Child component stub — same rationale | --- ## Summary | Class | Modules | Call sites | |---|---|---| | (b) `__mocks__/` redirect | 7 | 72 | | (a) prop-injection | 2 | 10 | | (c) keep as factory | 3 | 5 | | **Total** | **12** | **87** | --- ## Out-of-scope findings Files outside `*.svelte.{spec,test}.ts` — run in Node mode, no birpc concern — noted for completeness. | Module | Files | Suggested class | Notes | |---|---|---|---| | `$lib/shared/api.server` | 15 (`.server.spec.ts`) | (c) keep | Per-test mock API responses required; no injection seam viable | | `$env/dynamic/private` | 1 (`documents/[id]/page.server.spec.ts`) | (c) keep | Server-only env variable | | `$lib/shared/errors` | 1 (`briefwechsel/page.server.spec.ts`) | (c) keep | Per-test error code shaping needed | | `$lib/paraglide/messages.js` | 1 (`lib/ocr/translateOcrProgress.spec.ts`) | (b) candidate | Same `__mocks__` file from in-scope migration covers this consumer too | --- ## Roadmap ### Phase 1 — `__mocks__/` redirect (class b) — ~2 days Pure file additions plus factory removal. No test-logic changes needed. Priority within phase 1: 1. **`$app/forms`** (13 files) — start here; factory is always identical, zero risk. Create `frontend/src/__mocks__/$app/forms.ts`: ```ts export const enhance = () => () => {}; ``` Remove all 13 `vi.mock('$app/forms', ...)` calls in one pass. 2. **`$env/static/public`** (1 file) — trivial; create `frontend/src/__mocks__/$env/static/public.ts` with the poll-interval constant. 3. **`$lib/paraglide/runtime`** (1 file) — create `frontend/src/__mocks__/$lib/paraglide/runtime.ts` with `{ getLocale: () => 'de', setLocale: vi.fn() }`. 4. **`$lib/paraglide/messages.js`** (1 in-scope + 1 out-of-scope consumer) — create `frontend/src/__mocks__/$lib/paraglide/messages.js` with identity functions for all message keys; covers both consumers simultaneously. 5. **`$app/state`** (18 files) — create `frontend/src/__mocks__/$app/state.ts` with getter-based proxies backed by module-level variables: ```ts export let _navigating = { type: null, to: null }; export let _page = { url: new URL('http://localhost/'), error: null, status: 200 }; export const navigating = { get type() { return _navigating.type; }, get to() { return _navigating.to; } }; export const page = { get url() { return _page.url; }, get error() { return _page.error; }, get status() { return _page.status; } }; ``` Tests that need custom values import `_navigating`/`_page` and assign before render. 6. **`$app/navigation`** (36 files, largest batch) — create `frontend/src/__mocks__/$app/navigation.ts` exporting all known nav functions as `vi.fn()`. Tests import the mock functions for assertions; add `vi.clearAllMocks()` in `afterEach`. Functions needed: `goto`, `invalidate`, `invalidateAll`, `beforeNavigate`, `afterNavigate`, `preloadCode`, `preloadData`, `pushState`, `replaceState`, `disableScrollHandling`, `onNavigate`. 7. **`$app/stores`** (2 files) — first check if `admin/tags/[id]/+page.svelte` uses `$app/stores` and can be migrated to `$app/state`; if yes, both mocks disappear automatically. Otherwise add `frontend/src/__mocks__/$app/stores.ts`. ### Phase 2 — prop-injection (class a) — ~3 days Requires creating or extending test-host components. 1. **`$lib/shared/services/confirm.svelte`** (8 files) — test-host already exists at `lib/shared/services/confirm.test-host.svelte`. For each of the 8 files: wrap the component under test in the test-host, capture the `ConfirmService` via `onReady`, then remove the `vi.mock` call. ~0.5 day per batch of similar files. 2. **`$lib/notification/notifications.svelte`** (2 files) — create `lib/notification/notification.test-host.svelte` modelled on `confirm.test-host.svelte`; it should call `provideNotificationStore()`, accept `onReady: (store) => void`, and render `<NotificationDropdown />` (or similar provider component). Then migrate both spec files (~1 day total). ### Phase 3 — keep as factory (class c) — 0 effort 5 call sites, no migration. Revisit if a component-DI pattern emerges in Svelte 5 (e.g. snippet injection as a prop). ### Total scope | Phase | Class | New files | Factories removed | Effort | |---|---|---|---|---| | 1 | (b) | 7 `__mocks__` files | 72 | ~2 days | | 2 | (a) | 1 test-host (`notification`) | 10 | ~3 days | | 3 | (c) | — | 0 | — | | **Total** | | **8** | **82** | **~5 days** | --- *Audit produced 2026-05-14. All paths relative to `frontend/src/`. Based on main branch state at audit time.*
marcel added the P3-lateraudittest labels 2026-05-14 10:37:33 +02:00
Sign in to join this conversation.
No Label P3-later audit test
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#560