fix(pdf-viewer): remove banned vi.mock('pdfjs-dist') — ADR 012 enforcement (issue #546) #549
Reference in New Issue
Block a user
Delete Branch "worktree-feat+issue-546-pdf-viewer-test-fix"
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?
Summary
PdfViewer.svelte.test.tsfromvi.mock('pdfjs-dist', …)to the libLoader prop injection pattern — eliminates the birpc teardown race documented in ADR 012PdfViewer.svelte.spec.tsintotest.ts(absorbs nav/zoom/page-counter tests, deletes the spec file)no-restricted-syntaxrule scoped to*.spec.ts/*.test.tsthat flags the banned pattern at save timefrontend/src/, before Playwright even spins upCommits
1f7f6bdetest(pdf-viewer): port PdfViewer.svelte.test.ts to libLoader prop injection — remove vi.mock32d02b79test(pdf-viewer): consolidate spec.ts into test.ts — absorb page-counter test, delete specc1e5732frefactor(eslint): ban vi.mock('pdfjs-dist', ...) in spec/test files — ADR 012f9389712ci(unit-tests): add early grep check for banned vi.mock pdfjs-dist patternTest plan
npm run lintclean — ESLint rule fires if you manually addvi.mock('pdfjs-dist'to any spec/test filePdfViewer.svelte.spec.tsno longer exists; all 18 tests live intest.tsCloses #546
🤖 Generated with Claude Code
Removes both vi.mock('pdfjs-dist', …) calls that caused the birpc teardown race (ADR 012). Replaces with static import + makeFakeLibLoader() helper injected via the libLoader prop on every render() call. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>Adds a static grep step that runs after Lint and before the test suite. Fails in ~1 s if any file under frontend/src/ contains the banned vi.mock('pdfjs-dist' pattern, catching the regression before Playwright spins up. Belt-and-suspenders with the ESLint rule (ADR 012). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ✅ Approved
This is clean, disciplined test migration. The pattern is correct and consistently applied across all render calls.
What I checked
makeFakePdfjsLib()andmakeFakeLibLoader()are well-named, return fresh mocks per call (eachrender()gets its ownvi.fn()), and follow the factory pattern correctly. No shared mock state between tests.afterEach(cleanup)is correctly present — was previously only in spec.ts."PdfControls renders its nav + zoom buttons once the document.promise resolves","transcribeMode forces showAnnotations=true...","Without an annotations fetch, the visibility toggle is hidden","PDF rendering does not depend on the annotations fetch") were all "what" comments. The test names and assertions carry the intent. Good removal.shows previous and next page navigation buttons,shows zoom controls,displays the page counter) sit correctly insidedescribe('PdfViewer — loaded state')— they require a resolved fake library to assert on controls.[fakePdfjs, { default: '' }] as constgives the tuple its exact type, matching theLibLoaderreturn type contract."CallExpression[callee.object.name='vi'][callee.property.name='mock'] > Literal[value=/^pdfjs-dist/]"— the/^pdfjs-dist/regex covers bothpdfjs-distandpdfjs-dist/build/pdf.worker.min.mjs?url. The>(direct child combinator) is correct since the first argument ofvi.mock()is a direct child of the CallExpression.Minor observation (not a blocker)
The three absorbed tests call
render()without adocumentIdprop. This is intentionally minimal — they only care about controls appearing when a PDF loads. The omission is correct for their scope, but the absence is slightly jarring in a describe block where most peers passdocumentId. Not a blocker.All 18 tests in one file. Consolidation reduces cognitive overhead when reading this suite.
🏗️ Markus Keller — Senior Application Architect
Verdict: ✅ Approved
The structural changes are correct. ADR-012 is the authoritative record of the mocking decision, and this PR correctly updates it to reflect the enforcement reality that now exists.
Documentation audit
Per the doc update matrix:
docs/adr/No other doc updates required: no new routes, no new backend packages, no DB schema changes, no new Docker services, no new
ErrorCodeorPermissionvalues.Pattern assessment
The libLoader prop injection pattern is the right call architecturally. It:
The ESLint rule + CI grep combination gives three enforcement layers: save-time (ESLint), pre-test CI (grep), and coverage CI (birpc guard). Belt-and-suspenders for a failure mode that cost real CI time. Proportionate response.
ADR-012 update quality
The updated enforcement note accurately describes what was added and what remains. The revisit signal ("If three or more components adopt this pattern, consider extracting a shared type...") is intact — good. That's the right moment to revisit, not before.
No architectural concerns.
🛠️ Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
CI change assessment
New step inserted between
npm run lintandRun unit and component tests with coverage:What's correct
shell: bash: Theif/then/ficonstruct is bash-specific. Explicit declaration is correct — some runners default tosh.-Fflag: Fixed-string matching, not regex. More efficient and unambiguous for a literal string check.frontend/src/is relative to the repo root, which is the default working directory. Noworking-directory:override needed.One suggestion (not a blocker)
grepexits with code 2 on error (path doesn't exist, permission issue). The currentif grep ...treats exit code 2 the same as exit code 1 (no match), so a broken grep invocation silently passes. For a safety check this is mildly ironic.Hardening option, if desired:
Could set
set -o pipefailor check[ -d frontend/src ]first. For this codebase and runner environment the current form is pragmatically fine — just worth being aware of.No blockers.
🔒 Nora Steiner — Application Security Engineer
Verdict: ✅ Approved
This PR modifies test infrastructure and enforcement tooling exclusively. No production code is touched. My security review finds nothing to flag.
What I checked
grep -rF "vi.mock('pdfjs-dist'" frontend/src/uses fixed-string matching, targets a known source directory, operates only on committed source files. No user input in the pipeline, no injection risk.no-restricted-syntaxselector operates on parsed AST nodes, not on user-controlled data. The regex/^pdfjs-dist/is a static literal embedded in the config, not constructed from external input. No security concern.vi.fn().mockResolvedValue()for prop-based fake injection. No auth handling, no secrets, no input validation bypass, no XSS surface.Indirect security note
Eliminating a flaky CI pattern is good for security posture. When CI suites produce non-deterministic failures, teams learn to dismiss red builds — and that's when real security regressions slip through unnoticed. A reliable CI is a prerequisite for trusting the suite as a security gate. This PR moves in the right direction.
No findings.
🧪 Sara Holt — QA Engineer & Test Strategist
Verdict: ✅ Approved
This is a solid test consolidation and regression-prevention investment. Let me work through it systematically.
Test quality
Factory pattern ✅ —
makeFakePdfjsLib()andmakeFakeLibLoader()are correct factory functions. Eachrender()call passeslibLoader: makeFakeLibLoader(), creating a freshvi.fn()per test. No shared mock state across tests — isolation is correct.Cleanup ✅ —
afterEach(cleanup)is in place. It was previously only in spec.ts; test.ts now owns it correctly.Test naming ✅ — Sentences describing behavior. The absorbed tests maintain the same naming style as the existing suite (
'shows previous and next page navigation buttons','shows zoom controls','displays the page counter once the PDF has loaded').Behavioral assertions ✅ — All assertions use
getByRole,getByText,toBeVisible(),toBeInTheDocument(). Testing what users see, not component internals.Test placement ✅ — The three absorbed tests from spec.ts belong inside
describe('PdfViewer — loaded state'): they require the fake library to resolve before asserting on controls. Correct grouping.Flakiness elimination
The birpc teardown race was non-deterministic by nature — it depended on GC and IPC timing after each test. The libLoader prop injection doesn't touch the module system's hoisting or teardown lifecycle at all. The race is architecturally eliminated, not suppressed. This is the right fix.
Enforcement layers
no-restricted-syntaxThree layers for one failure mode that cost real time. Proportionate and maintainable.
Minor observation
The three absorbed tests at the bottom call
render()withoutdocumentId. This is intentionally minimal — they only assert on basic controls appearing when any PDF loads. The behavior is correct. If this suite grows and those tests need annotation context, they'll needdocumentIdadded then. Not a blocker today.No blockers.
🎨 Leonie Voss — UX Designer & Accessibility Strategist
Verdict: ✅ Approved
This PR contains no UI component changes, no template changes, no CSS changes, and no user-facing modifications. My review scope is minimal but I do check one thing that lives at the intersection of tests and accessibility.
Accessibility-aligned test assertions
The test assertions use role-based selectors with German localized labels matching the production component:
getByRole('button', { name: 'Zurück' })getByRole('button', { name: /zurück/i })getByRole('button', { name: 'Weiter' })getByRole('button', { name: 'Vergrößern' })getByRole('button', { name: 'Verkleinern' })getByRole('button', { name: /vergrößern/i })getByRole('button', { name: /verkleinern/i })Using
getByRole('button', { name: ... })to locate buttons means these tests will fail if the buttons lose their accessible name (e.g., ifaria-labelis removed or the visible text disappears). This is the correct testing pattern — it verifies accessibility correctness as a side effect of verifying behavior. Nothing to flag here.LGTM.
📋 Elicit — Requirements Engineer
Verdict: ✅ Approved
This PR delivers on issue #546 clearly and completely. I review it for requirements coverage and traceability.
Issue-to-delivery mapping
vi.mock('pdfjs-dist', …)from test.tsvi.mockblock deleted, replaced with prop injectioneslint.config.js—no-restricted-syntaxscoped to spec/test filesci.yml— grep step before test suitespec.tsdeleted; 3 tests absorbed intotest.tsspec.tsshown as deleted; 3 tests visible at bottom oftest.tsdiffdocs/adr/012-browser-test-mocking-strategy.mdupdatedTest plan verifiability
The PR's test plan checklist is well-formed and each item is independently verifiable:
npm run lintclean → observable locally and in CI lint stepPdfViewer.svelte.spec.tsno longer exists → observable in repo file treeScope discipline
The PR delivers both the remediation (remove banned pattern) and the prevention mechanism (ESLint + CI grep), without adding unrelated changes. This is tight scope. The issue is fully closed without feature creep.
No open ambiguities
The PR description references ADR-012 and issue #546. The rationale (birpc teardown race) is documented in the ADR. The replacement pattern (libLoader prop injection) is documented in both the ADR and demonstrated in the test changes. No requirements gaps.
No findings.