From 6c596babcbc9e19fbfa03d6d48182a6b77f69288 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 12 May 2026 12:17:25 +0200 Subject: [PATCH] =?UTF-8?q?test(pdf-viewer):=20port=20PdfViewer.svelte.tes?= =?UTF-8?q?t.ts=20to=20libLoader=20prop=20injection=20=E2=80=94=20remove?= =?UTF-8?q?=20vi.mock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../document/viewer/PdfViewer.svelte.test.ts | 94 ++++++++++++------- 1 file changed, 62 insertions(+), 32 deletions(-) diff --git a/frontend/src/lib/document/viewer/PdfViewer.svelte.test.ts b/frontend/src/lib/document/viewer/PdfViewer.svelte.test.ts index 12383c6d..a4b49ba9 100644 --- a/frontend/src/lib/document/viewer/PdfViewer.svelte.test.ts +++ b/frontend/src/lib/document/viewer/PdfViewer.svelte.test.ts @@ -1,11 +1,18 @@ import { describe, it, expect, vi, afterEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; +import type { LibLoader } from '$lib/document/viewer/usePdfRenderer.svelte'; +import PdfViewer from './PdfViewer.svelte'; -vi.mock('pdfjs-dist', () => { - function TextLayerMock() {} - TextLayerMock.prototype.render = () => Promise.resolve(); - TextLayerMock.prototype.cancel = () => {}; +afterEach(cleanup); + +function makeFakePdfjsLib() { + class TextLayerMock { + render() { + return Promise.resolve(); + } + cancel() {} + } return { GlobalWorkerOptions: { workerSrc: '' }, @@ -20,24 +27,23 @@ vi.mock('pdfjs-dist', () => { }) }), TextLayer: TextLayerMock - }; -}); + } as unknown as typeof import('pdfjs-dist'); +} -vi.mock('pdfjs-dist/build/pdf.worker.min.mjs?url', () => ({ default: '' })); - -const { default: PdfViewer } = await import('./PdfViewer.svelte'); - -afterEach(cleanup); +function makeFakeLibLoader(): LibLoader { + const fakePdfjs = makeFakePdfjsLib(); + return vi.fn().mockResolvedValue([fakePdfjs, { default: '' }] as const); +} describe('PdfViewer — empty / error states', () => { it('renders the no-file placeholder when url is empty', async () => { - render(PdfViewer, { url: '' }); + render(PdfViewer, { url: '', libLoader: makeFakeLibLoader() }); await expect.element(page.getByText('Keine Datei vorhanden')).toBeVisible(); }); it('does not render the controls when url is empty', async () => { - render(PdfViewer, { url: '' }); + render(PdfViewer, { url: '', libLoader: makeFakeLibLoader() }); const buttons = document.querySelectorAll('button'); expect(buttons.length).toBe(0); @@ -49,10 +55,10 @@ describe('PdfViewer — loaded state', () => { render(PdfViewer, { url: '/api/documents/test/file', documentId: 'test', - annotationReloadKey: 0 + annotationReloadKey: 0, + libLoader: makeFakeLibLoader() }); - // PdfControls renders its nav + zoom buttons once the document.promise resolves. await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible(); await expect.element(page.getByRole('button', { name: 'Weiter' })).toBeVisible(); await expect.element(page.getByRole('button', { name: 'Vergrößern' })).toBeVisible(); @@ -63,7 +69,8 @@ describe('PdfViewer — loaded state', () => { render(PdfViewer, { url: '/api/documents/test/file', documentId: 'test', - annotationsDimmed: true + annotationsDimmed: true, + libLoader: makeFakeLibLoader() }); await vi.waitFor(() => { @@ -96,11 +103,10 @@ describe('PdfViewer — loaded state', () => { url: '/api/documents/test/file', documentId: 'test', transcribeMode: true, - documentFileHash: 'match' + documentFileHash: 'match', + libLoader: makeFakeLibLoader() }); - // transcribeMode forces showAnnotations=true; toggle button surfaces with "hide" label - // (only when annotationCount > 0). await expect .element(page.getByRole('button', { name: /annotierungen verbergen/i })) .toBeVisible(); @@ -113,7 +119,8 @@ describe('PdfViewer — loaded state', () => { render(PdfViewer, { url: '/api/documents/test/file', documentId: 'test', - documentFileHash: 'abc123' + documentFileHash: 'abc123', + libLoader: makeFakeLibLoader() }); await vi.waitFor(() => { @@ -125,7 +132,8 @@ describe('PdfViewer — loaded state', () => { render(PdfViewer, { url: '/api/documents/test/file', documentId: 'test', - flashAnnotationId: 'ann-flashing' + flashAnnotationId: 'ann-flashing', + libLoader: makeFakeLibLoader() }); await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible(); @@ -135,7 +143,8 @@ describe('PdfViewer — loaded state', () => { render(PdfViewer, { url: '/api/documents/test/file', documentId: 'test', - blockNumbers: { 'ann-1': 1, 'ann-2': 2 } + blockNumbers: { 'ann-1': 1, 'ann-2': 2 }, + libLoader: makeFakeLibLoader() }); await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible(); @@ -145,7 +154,8 @@ describe('PdfViewer — loaded state', () => { render(PdfViewer, { url: '/api/documents/test/file', documentId: 'test', - activeAnnotationId: 'ann-1' + activeAnnotationId: 'ann-1', + libLoader: makeFakeLibLoader() }); await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible(); @@ -156,10 +166,10 @@ describe('PdfViewer — loaded state', () => { url: '/api/documents/test/file', documentId: 'test', transcribeMode: true, - activeAnnotationId: 'ann-1' + activeAnnotationId: 'ann-1', + libLoader: makeFakeLibLoader() }); - // Without an annotations fetch, the visibility toggle is hidden — just assert the always-on nav. await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible(); await expect.element(page.getByRole('button', { name: 'Weiter' })).toBeVisible(); }); @@ -169,7 +179,8 @@ describe('PdfViewer — loaded state', () => { render(PdfViewer, { url: '/api/documents/test/file', documentId: 'test', - onAnnotationClick + onAnnotationClick, + libLoader: makeFakeLibLoader() }); await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible(); @@ -199,7 +210,8 @@ describe('PdfViewer — loaded state', () => { render(PdfViewer, { url: '/api/documents/test/file', documentId: 'test', - documentFileHash: 'new-hash' + documentFileHash: 'new-hash', + libLoader: makeFakeLibLoader() }); await vi.waitFor(() => { @@ -234,10 +246,10 @@ describe('PdfViewer — loaded state', () => { render(PdfViewer, { url: '/api/documents/test/file', documentId: 'test', - documentFileHash: 'matching-hash' + documentFileHash: 'matching-hash', + libLoader: makeFakeLibLoader() }); - // Controls finish mounting, and the outdated notice stays absent. await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible(); expect(document.querySelector('[data-testid="annotation-outdated-notice"]')).toBeNull(); } finally { @@ -250,10 +262,10 @@ describe('PdfViewer — loaded state', () => { try { render(PdfViewer, { url: '/api/documents/test/file', - documentId: 'test' + documentId: 'test', + libLoader: makeFakeLibLoader() }); - // PDF rendering does not depend on the annotations fetch — controls still appear. await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible(); expect(document.querySelector('[data-testid="annotation-outdated-notice"]')).toBeNull(); } finally { @@ -268,7 +280,8 @@ describe('PdfViewer — loaded state', () => { try { render(PdfViewer, { url: '/api/documents/test/file', - documentId: 'test' + documentId: 'test', + libLoader: makeFakeLibLoader() }); await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible(); @@ -277,4 +290,21 @@ describe('PdfViewer — loaded state', () => { fetchSpy.mockRestore(); } }); + + it('shows previous and next page navigation buttons', async () => { + 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', 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', libLoader: makeFakeLibLoader() }); + await expect.element(page.getByText(/1\s*\/\s*2/)).toBeInTheDocument(); + }); });