test: PdfViewer.svelte.test.ts re-introduces banned vi.mock('pdfjs-dist') factory — restores birpc teardown race #546
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?
Problem
frontend/src/lib/document/viewer/PdfViewer.svelte.test.ts(added in the recent merge to main) usesvi.mock('pdfjs-dist', factory)on lines 5 and 26:This is exactly the pattern that PR #535 / commit
11547645eliminated from the oldPdfViewer.svelte.spec.ts. The old spec file was fixed to use prop injection (libLoader); the new test file was written with the banned pattern.Effect: running
npm run testlocally exits with:This also fails the CI coverage guard (
Assert no birpc teardown race in coverage run).Root cause
Per ADR 012:
pdfjs-distis loaded via dynamic import insideusePdfRenderer.svelte.ts::init(), which is called fromonMount. The birpc factory route handler can receive this request after the worker channel has already closed, producing the unhandled rejection that exits vitest with code 1.$app/navigation,$app/state, etc. are safe because they are resolved statically at module-eval time.pdfjs-distis not.Fix
Port
PdfViewer.svelte.test.tsto use thelibLoaderprop-injection pattern already established inPdfViewer.svelte.spec.tsand documented in ADR 012:Remove the two
vi.mock(...)calls at the top of the file. No production code change is needed —PdfViewer.sveltealready acceptslibLoaderas an optional prop.Acceptance criteria
PdfViewer.svelte.test.tscontains novi.mock('pdfjs-dist', ...)callnpm run testexits 0 with no birpc unhandled rejection👨💻 Felix Brandt — Senior Fullstack Developer
Observations
PdfViewer.svelte.test.tslines 5 and 26 carry twovi.mock('pdfjs-dist', ...)calls — the exact banned pattern from ADR 012.PdfViewer.svelte.spec.tsalready demonstrates the approved replacement:makeFakeLibLoader()returnsvi.fn().mockResolvedValue([fakePdfjs, { default: '' }] as const)— novi.mockinvolved at all.LibLoadertype is exported fromusePdfRenderer.svelte.tsand can be imported directly in the migrated file — no redefinition needed.PdfViewer.svelte.test.tscovers 32 test cases;PdfViewer.svelte.spec.tscovers only 3. The migration must carry all 32 behaviors across, not just remove the offending lines.PdfViewer.sveltealready acceptslibLoader?: LibLoader(line ~43 via$props()), so zero production code changes are needed.Recommendations
makeFakePdfjsLib()andmakeFakeLibLoader()fromspec.tsintotest.ts(or extract to a shared__fixtures__/pdfjs.ts), then delete bothvi.mockcalls at the top oftest.ts.libLoader: makeFakeLibLoader()to everyrender(PdfViewer, { ... })call — or hoist it into abeforeEachvariable if all tests in adescribeblock share the same fake.spec.tsandtest.tsinto one canonical file. Two files for one component create discovery confusion and split ownership over which behaviors are covered where. The natural choice is to keeptest.ts(larger coverage) and deletespec.ts.🏛️ Markus Keller — Application Architect
Observations
libLoaderprop pattern is correct architectural thinking: inversion of control at the component boundary. The production default is the real dynamic import; tests supply a synchronous fake via prop. No global module state is manipulated.PdfViewer.svelte.spec.tswith 3 tests andPdfViewer.svelte.test.tswith 32 tests) is an architectural smell. They will diverge further over time, and neither file owns the full behavioral specification.ci.yml(lines 48–58) runsif: always()and grep-checks the coverage log — that is structurally correct. The problem is that it is the only line of defense, which is why this regression landed.Recommendations
no-restricted-syntaxrule that matchesvi.mockcalls whose first argument starts withpdfjs-dist. This turns a ~2-minute CI wait into an immediate lint failure on save. The rule body should include a message referencing ADR 012 so the developer understands why.spec.ts; the migratedtest.tsbecomes the canonical specification forPdfViewer.svelte. One component, one test file.7f07180c.🚀 Tobias Wendt — DevOps & Platform Engineer
Observations
.gitea/workflows/ci.yml(lines 48–58) is structurally correct: it usesif: always(), tee's the coverage run to/tmp/coverage-test-${{ github.run_id }}.log, and grep-fails on[birpc] rpc is closed. It works.npm teststep) because that is where the log file is captured. This is a documented design choice, not a gap.npm run teststep would also exit 1 directly (as described in the issue), so CI does catch this at two points — but neither is fast.Recommendations
🧪 Sara Holt — QA Engineer & Test Strategist
Observations
PdfViewer.svelte.test.tscontains 32 test cases covering loaded state, empty/error states, pagination, zoom, text layer rendering, and canvas output.PdfViewer.svelte.spec.tscontains 3 tests. These are not equivalent files — they test different behavioral slices.vi.mockcall,npm run testexits 0, CI guard green) are necessary but not sufficient. A migration that removes 29 tests to make the error go away would satisfy all three criteria while destroying coverage.spec.ts+test.ts) for one component creates split coverage ownership. After migration, it is unclear which file should receive new tests forPdfViewer.svelte.Recommendations
PdfViewer.svelte.test.tsbefore migration continue to exist and pass after migration." This prevents a lazy fix that silently reduces test coverage.test.ts(the comprehensive one), deletespec.ts. One component, one test file, unambiguous coverage ownership.makeFakeLibLoader()helper fromspec.tsas the foundation. It already handles theTextLayerMockclass that the 32 tests intest.tsdepend on for text layer assertions.🔒 Nora "NullX" Steiner — Application Security Engineer
Observations
libLoaderprop is an internal testing seam: it defaults to the real dynamic import in production (() => Promise.all([import('pdfjs-dist'), import('pdfjs-dist/build/pdf.worker.min.mjs?url')])), and is only overridden in tests. This pattern does not widen the attack surface.vi.mockapproach that is being removed operates at the Vite module resolution layer during test runs — it has no runtime presence and no security implications in either direction.No security concerns from my angle. The dependency injection pattern being adopted is strictly tighter than the global mock approach: it narrows the surface by eliminating test-time global module state manipulation.
🎨 Leonie Voss — UX Designer & Accessibility Strategist
Observations
PdfViewer.svelteitself is not modified — no component markup, props visible to users, interaction states, focus behavior, or brand tokens change.libLoaderprop is an optional internal testing seam (libLoader?: LibLoader). It is not rendered, not announced to assistive technology, and has no effect on the PDF viewer's visual or interactive behavior in production.No UX or accessibility concerns from my angle. The PDF viewer's rendered experience, touch targets, keyboard navigation, and ARIA semantics are unchanged by this fix.
📋 Elicit — Requirements Engineer
Observations
vi.mock('pdfjs-dist', ...)call" and "npm run test exits 0," but nothing protects the 32 behavioral test cases that currently live inPdfViewer.svelte.test.ts. A migration that satisfies the three listed criteria while deleting tests is technically compliant with the issue as written.PdfViewer.svelteorusePdfRenderer.svelte.tsas part of this fix, nothing in the AC would catch it.Recommendations
PdfViewer.svelte.test.tsbefore this fix continue to exist and pass after migration." This closes the coverage-reduction gap.frontend/src/lib/document/viewer/PdfViewer.svelte.test.tsare modified" — makes the scope boundary explicit and reviewable.