fix(#535): eliminate vi.mock(pdfjs-dist) birpc teardown race via libLoader injection #536

Merged
marcel merged 18 commits from feat/issue-535-birpc-teardown-race into main 2026-05-12 09:57:30 +02:00
2 changed files with 19 additions and 16 deletions
Showing only changes of commit b00ffc358f - Show all commits

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onMount, setContext } from 'svelte'; import { onMount, setContext, untrack } from 'svelte';
import { createPdfRenderer } from '$lib/document/viewer/usePdfRenderer.svelte'; import { createPdfRenderer } from '$lib/document/viewer/usePdfRenderer.svelte';
import PdfControls from './PdfControls.svelte'; import PdfControls from './PdfControls.svelte';
import AnnotationLayer from '$lib/document/annotation/AnnotationLayer.svelte'; import AnnotationLayer from '$lib/document/annotation/AnnotationLayer.svelte';
@@ -21,7 +21,8 @@ let {
onDeleteAnnotationRequest, onDeleteAnnotationRequest,
documentFileHash, documentFileHash,
annotationsDimmed = false, annotationsDimmed = false,
flashAnnotationId = null flashAnnotationId = null,
libLoader = undefined
}: { }: {
url: string; url: string;
documentId?: string; documentId?: string;
@@ -35,9 +36,10 @@ let {
documentFileHash?: string | null; documentFileHash?: string | null;
annotationsDimmed?: boolean; annotationsDimmed?: boolean;
flashAnnotationId?: string | null; flashAnnotationId?: string | null;
libLoader?: Parameters<typeof createPdfRenderer>[0];
} = $props(); } = $props();
const renderer = createPdfRenderer(); const renderer = untrack(() => createPdfRenderer(libLoader));
// Canvas and text layer container refs — bound via bind:this // Canvas and text layer container refs — bound via bind:this
let canvasEl = $state<HTMLCanvasElement | null>(null); let canvasEl = $state<HTMLCanvasElement | null>(null);

View File

@@ -1,11 +1,11 @@
import { vi, describe, it, expect, afterEach } from 'vitest'; import { vi, describe, it, expect, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte'; import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser'; import { page } from 'vitest/browser';
import type { createPdfRenderer } from '$lib/document/viewer/usePdfRenderer.svelte';
// pdfjs-dist is a rendering dependency — we mock it so unit tests don't need afterEach(cleanup);
// a real browser PDF engine. The interesting behaviour under test here is the
// component's own UI logic (controls, page counter), not pdfjs internals. function makeFakePdfjsLib() {
vi.mock('pdfjs-dist', () => {
function TextLayerMock() {} function TextLayerMock() {}
TextLayerMock.prototype.render = () => Promise.resolve(); TextLayerMock.prototype.render = () => Promise.resolve();
TextLayerMock.prototype.cancel = () => {}; TextLayerMock.prototype.cancel = () => {};
@@ -23,31 +23,32 @@ vi.mock('pdfjs-dist', () => {
}) })
}), }),
TextLayer: TextLayerMock TextLayer: TextLayerMock
}; } as unknown as typeof import('pdfjs-dist');
}); }
vi.mock('pdfjs-dist/build/pdf.worker.min.mjs?url', () => ({ default: '' })); function makeFakeLibLoader(): Parameters<typeof createPdfRenderer>[0] {
const fakePdfjs = makeFakePdfjsLib();
return vi.fn().mockResolvedValue([fakePdfjs, { default: '' }] as const);
}
import PdfViewer from './PdfViewer.svelte'; import PdfViewer from './PdfViewer.svelte';
afterEach(cleanup);
describe('PdfViewer', () => { describe('PdfViewer', () => {
it('shows previous and next page navigation buttons', async () => { it('shows previous and next page navigation buttons', async () => {
render(PdfViewer, { url: '/api/documents/test-id/file' }); render(PdfViewer, { url: '/api/documents/test-id/file', libLoader: makeFakeLibLoader() });
await expect.element(page.getByRole('button', { name: /zurück/i })).toBeInTheDocument(); await expect.element(page.getByRole('button', { name: /zurück/i })).toBeInTheDocument();
await expect.element(page.getByRole('button', { name: /weiter/i })).toBeInTheDocument(); await expect.element(page.getByRole('button', { name: /weiter/i })).toBeInTheDocument();
}); });
it('shows zoom controls', async () => { it('shows zoom controls', async () => {
render(PdfViewer, { url: '/api/documents/test-id/file' }); render(PdfViewer, { url: '/api/documents/test-id/file', libLoader: makeFakeLibLoader() });
await expect.element(page.getByRole('button', { name: /vergrößern/i })).toBeInTheDocument(); await expect.element(page.getByRole('button', { name: /vergrößern/i })).toBeInTheDocument();
await expect.element(page.getByRole('button', { name: /verkleinern/i })).toBeInTheDocument(); await expect.element(page.getByRole('button', { name: /verkleinern/i })).toBeInTheDocument();
}); });
it('displays the page counter once the PDF has loaded', async () => { it('displays the page counter once the PDF has loaded', async () => {
render(PdfViewer, { url: '/api/documents/test-id/file' }); render(PdfViewer, { url: '/api/documents/test-id/file', libLoader: makeFakeLibLoader() });
// Mock resolves synchronously, so "1 / 2" should appear quickly // Fake loader resolves synchronously, so "1 / 2" should appear quickly
await expect.element(page.getByText(/1\s*\/\s*2/)).toBeInTheDocument(); await expect.element(page.getByText(/1\s*\/\s*2/)).toBeInTheDocument();
}); });
}); });