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

View File

@@ -1,11 +1,11 @@
import { vi, describe, it, expect, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
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
// a real browser PDF engine. The interesting behaviour under test here is the
// component's own UI logic (controls, page counter), not pdfjs internals.
vi.mock('pdfjs-dist', () => {
afterEach(cleanup);
function makeFakePdfjsLib() {
function TextLayerMock() {}
TextLayerMock.prototype.render = () => Promise.resolve();
TextLayerMock.prototype.cancel = () => {};
@@ -23,31 +23,32 @@ vi.mock('pdfjs-dist', () => {
})
}),
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';
afterEach(cleanup);
describe('PdfViewer', () => {
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: /weiter/i })).toBeInTheDocument();
});
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: /verkleinern/i })).toBeInTheDocument();
});
it('displays the page counter once the PDF has loaded', async () => {
render(PdfViewer, { url: '/api/documents/test-id/file' });
// Mock resolves synchronously, so "1 / 2" should appear quickly
render(PdfViewer, { url: '/api/documents/test-id/file', libLoader: makeFakeLibLoader() });
// Fake loader resolves synchronously, so "1 / 2" should appear quickly
await expect.element(page.getByText(/1\s*\/\s*2/)).toBeInTheDocument();
});
});