The in-browser pixel-render fixture test was green locally but flaky in
CI: the real pdf.js worker could not fetch /pdfjs-wasm/ in the CI
Chromium container, so the CCITT canvas stayed blank (0 sampled pixels)
and failed the suite — green locally, red in CI, root cause not locally
reproducible. A flaky gate is worse than none.
This bug is a build/serve parity failure, so guard it deterministically
where it actually breaks: a postbuild assertion that jbig2.wasm and
openjpeg.wasm shipped into build/client/pdfjs-wasm/ (non-empty). It runs
after `npm run build` — including the Docker build stage — and fails the
build loudly if a future pdfjs bump makes the static-copy glob match
nothing. Combined with the getDocument(wasmUrl) unit guard and the
negative-path render test, the regression is covered without CI flake.
Addresses re-review: Tobias (no automated parity check), Sara (pixel
test not pinned). Render-decode correctness verified manually via
`node build` serving /pdfjs-wasm/jbig2.wasm as application/wasm.
Refs #708
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Render committed synthetic fixtures through PdfViewer with the REAL
pdf.js loader and assert the canvas is non-blank (sampled dark-pixel
count). The CCITT (G4 fax) fixture exercises the shared jbig2.wasm
decode path — the same module pdf.js uses for JBIG2 — so it transitively
covers the JBIG2 acceptance criterion (the archive sample found zero
true JBIG2 docs and jbig2enc is unavailable to synthesize one). The
JPEG/DCTDecode fixture guards against regressing the natively-decoded
path. Verified the CCITT case goes red when wasmUrl is removed.
Fixtures are hermetic, committed assets (~2-5 KB each), generated with
ImageMagick — never fetched from staging at test time. CI browser mode.
Refs #708
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The error state showed a hardcoded German string ("Fehler beim Laden
der PDF" / "Direkt öffnen") to all users regardless of locale. Use the
localized doc_render_failed and doc_download_link messages so the
recovery path (message + working download link) is honest in de/en/es.
Refs #708
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
renderCurrentPage swallowed every render rejection with a bare return,
so a decode failure left a blank white viewer with no feedback. Now a
non-cancellation rejection sets a localized doc_render_failed message,
which routes into the existing error UI (message + download link).
Cancellation (page-nav / zoom) still returns silently — no error.
Refs #708
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
getDocument was called with a bare src string, so pdf.js 5.x had no
`wasmUrl` and could not initialise the JBIG2/CCITTFax wasm decoder —
CCITT (G4 fax) scans painted a blank canvas. Pass
{ url, wasmUrl: '/pdfjs-wasm/' }; the directory URL (trailing slash
required) is the single source of truth next to the worker config.
Refs #708
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On the document detail page, pass canEdit={canWrite} to the panel header,
guard onModeChange so a reader can never flip to edit, and default panelMode
to 'read' for readers. Thread canAnnotate={canWrite} through DocumentViewer
to PdfViewer so the annotation layer's canDraw (which also gates delete and
resize) is off for readers — they can open and read, but not draw, edit, or
delete. The writer-only OCR status check is also skipped for readers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduces `csrfFetch` (= `makeCsrfFetch(fetch)`) in cookies.ts as a
drop-in fetch replacement that auto-injects X-XSRF-TOKEN on POST/PUT/PATCH/DELETE.
Previously 8 call sites sent mutating requests without the CSRF header —
annotation resize, comment POST/PATCH/DELETE, Geschichte CRUD, Stammbaum
relationship creation, bulk-edit PATCH, and file upload — all would fail
with CSRF_TOKEN_MISSING if the backend's cookie-based protection triggered.
All 14 client-side mutating fetches now use csrfFetch; withCsrf/makeCsrfFetch
remain in the API for injectable-fetch use cases (e.g. useTranscriptionBlocks).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces hardcoded Zurück/Weiter/Verkleinern/Vergrößern aria-label strings
with m.viewer_previous_page(), m.viewer_next_page(), m.viewer_zoom_out(),
and m.viewer_zoom_in() so viewer controls translate in EN and ES locales.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract makeFakePdfjsLib / makeFakeLibLoader to testHelpers.ts — single
source of truth used by both PdfViewer.svelte.test.ts and
usePdfRenderer.svelte.test.ts; removes the diverging-fidelity DRY violation
flagged by @felixbrandt and @saraholt in the PR review
- Add 'loadDocument sets error and loading=false when getDocument().promise
rejects' test to usePdfRenderer.svelte.test.ts — closes the error-path gap
flagged by @felixbrandt and @saraholt
- Replace toBeInTheDocument() with toBeVisible() in the three absorbed
spec-file tests — uniform assertion style across the loaded-state describe
block, as flagged by @felixbrandt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Five tests in usePdfRenderer.svelte.test.ts called createPdfRenderer() without
a libLoader, causing init() to dynamically import pdfjs-dist in the browser.
Every dynamic import goes through Playwright's route handler, which calls
resolveManualMock via birpc to check for mocks. If the RPC closes during
teardown while one of these imports is in flight, the birpc race fires —
even though pdfjs-dist was never explicitly vi.mock()-ed.
Replace all bare createPdfRenderer() calls that invoke init() with
createPdfRenderer(makeFakeLibLoader()), identical to the pattern already
used in PdfViewer.svelte.test.ts. No real module loads, no route-handler
calls, no birpc exposure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Absorbs the three tests from PdfViewer.svelte.spec.ts (nav buttons, zoom
controls, page counter) into the loaded-state describe in test.ts, then
deletes the now-empty spec file. One spec file per component.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Prototype-style assignment was a vi.mock hoisting artifact from the old
version of the file. Rest of the codebase uses class syntax — aligning.
Addresses Felix Brandt round-4 suggestion on PR #536.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds idempotency test: calling init() twice must invoke libLoader only once.
Adds `if (pdfjsReady) return;` guard to satisfy the contract.
Addresses Felix Brandt round-4 suggestion on PR #536.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
.catch(()=>{}) swallowed the rejection, so the test passed vacuously even
if a future refactor silently caught the error. rejects.toThrow() proves
the propagation contract holds before asserting pdfjsReady stays false.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Regression-protection test: init() propagates the loader rejection
before pdfjsReady is set, so the renderer stays in a safe unready state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Without untrack, a reactive libLoader prop reference change would
reinitialise the whole renderer and lose all loaded state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Exporting LibLoader gives the type a stable, named identity.
PdfViewer.svelte and PdfViewer.svelte.spec.ts now import it directly
instead of using Parameters<typeof createPdfRenderer>[0].
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- import PdfViewer left mid-file from vi.mock hoisting — no longer needed (Sara/Felix)
- adds one-line comment explaining as unknown as cast is an intentional partial fake (Felix)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes both vi.mock('pdfjs-dist', factory) and
vi.mock('pdfjs-dist/build/pdf.worker.min.mjs?url', factory) from
PdfViewer.svelte.spec.ts — the ManualMockedModule registrations that were
racing with vitest-browser-playwright's birpc teardown channel.
PdfViewer.svelte now accepts an optional libLoader prop (typed as
Parameters<typeof createPdfRenderer>[0]) that is passed untracked to
createPdfRenderer(). Tests supply a vi.fn() fake loader directly as a prop;
production code uses the default loader that imports the real pdfjs-dist.
The birpc route handler for pdfjs-dist is never registered, so no teardown
race is possible. Fixes#535.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an optional LibLoader parameter (defaults to the real pdfjs-dist dynamic
imports) and a failing test that verified the loader is called during init().
This is the first step toward removing ManualMockedModule registrations that
race with vitest-browser-playwright's birpc teardown (#535).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces 6 setTimeout sleeps with vi.waitFor and expect.element
auto-wait, and converts 9 .not.toThrow smoke tests into assertions
on the rendered PDF nav controls (Zurück/Weiter/Vergrößern/Verkleinern)
and the conditional outdated-annotation notice / annotation visibility
toggle. transcribeMode test now mocks the annotations fetch so the
toggle button is actually rendered (annotationCount > 0 guard).
Runtime: 33s → 4.5s.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
renderCurrentPage early-returns when canvasEl/textLayerEl null,
init() idempotent on second call, zoomIn after floor, goToPage(1)
no-op.
5 new tests covering ~6 branches.
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Empty state when url is empty (no controls, placeholder shown),
loaded state with controls, annotationsDimmed branch, transcribeMode
flag, documentFileHash filtering branch.
6 tests covering ~10 branches.
Refs #496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add min-h-[44px] min-w-[44px] to all five PDF viewer buttons (prev,
next, zoom in, zoom out, annotation toggle) and widen icon-only
padding from p-1 to p-2. Adds aria-pressed to the annotation toggle
for correct toggle semantics (WCAG 2.2 §2.5.8 + ARIA 1.2).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>