diff --git a/frontend/src/lib/document/viewer/PdfViewer.svelte.test.ts b/frontend/src/lib/document/viewer/PdfViewer.svelte.test.ts index 583e1f91..12383c6d 100644 --- a/frontend/src/lib/document/viewer/PdfViewer.svelte.test.ts +++ b/frontend/src/lib/document/viewer/PdfViewer.svelte.test.ts @@ -40,107 +40,139 @@ describe('PdfViewer — empty / error states', () => { render(PdfViewer, { url: '' }); const buttons = document.querySelectorAll('button'); - // Empty state has no nav buttons expect(buttons.length).toBe(0); }); }); describe('PdfViewer — loaded state', () => { - it('renders annotation toggle controls', async () => { + it('renders the PDF navigation controls (Zurück/Weiter/Vergrößern/Verkleinern) when a url is provided', async () => { render(PdfViewer, { url: '/api/documents/test/file', documentId: 'test', annotationReloadKey: 0 }); - // The PdfControls component renders the toggle button - await new Promise((r) => setTimeout(r, 50)); - const buttons = document.querySelectorAll('button'); - expect(buttons.length).toBeGreaterThan(0); + // 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(); + await expect.element(page.getByRole('button', { name: 'Verkleinern' })).toBeVisible(); }); - it('passes annotationsDimmed=true to the AnnotationLayer wrapper', async () => { + it('renders the canvas background container when annotationsDimmed=true', async () => { render(PdfViewer, { url: '/api/documents/test/file', documentId: 'test', annotationsDimmed: true }); - await new Promise((r) => setTimeout(r, 50)); - // just confirm no throw in the dimmed code path - expect(document.querySelector('.bg-pdf-bg')).not.toBeNull(); + await vi.waitFor(() => { + expect(document.querySelector('.bg-pdf-bg')).not.toBeNull(); + }); }); - it('renders without throwing in transcribeMode', async () => { - expect(() => - render(PdfViewer, { - url: '/api/documents/test/file', - documentId: 'test', - transcribeMode: true - }) - ).not.toThrow(); - }); - - it('renders without throwing with a documentFileHash and matching annotations', async () => { - expect(() => - render(PdfViewer, { - url: '/api/documents/test/file', - documentId: 'test', - documentFileHash: 'abc123' - }) - ).not.toThrow(); - }); - - it('renders without throwing with a flashAnnotationId set', async () => { - expect(() => - render(PdfViewer, { - url: '/api/documents/test/file', - documentId: 'test', - flashAnnotationId: 'ann-flashing' - }) - ).not.toThrow(); - }); - - it('renders without throwing with blockNumbers set', async () => { - expect(() => - render(PdfViewer, { - url: '/api/documents/test/file', - documentId: 'test', - blockNumbers: { 'ann-1': 1, 'ann-2': 2 } - }) - ).not.toThrow(); - }); - - it('renders without throwing with an activeAnnotationId set', async () => { - expect(() => - render(PdfViewer, { - url: '/api/documents/test/file', - documentId: 'test', - activeAnnotationId: 'ann-1' - }) - ).not.toThrow(); - }); - - it('renders without throwing in transcribeMode + canvas + activeAnnotationId combo', async () => { - expect(() => + it('forces the annotation toggle into "hide" mode when transcribeMode is true and annotations exist', async () => { + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue( + new Response( + JSON.stringify([ + { + id: 'a1', + documentId: 'test', + pageNumber: 1, + x: 0.1, + y: 0.1, + width: 0.1, + height: 0.1, + color: '#000', + createdAt: '2026-01-01T00:00:00Z', + fileHash: 'match' + } + ]), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + ); + try { render(PdfViewer, { url: '/api/documents/test/file', documentId: 'test', transcribeMode: true, - activeAnnotationId: 'ann-1' - }) - ).not.toThrow(); + documentFileHash: 'match' + }); + + // 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(); + } finally { + fetchSpy.mockRestore(); + } }); - it('survives onAnnotationClick callback being invoked indirectly', async () => { + it('renders the canvas region when documentFileHash is provided', async () => { + render(PdfViewer, { + url: '/api/documents/test/file', + documentId: 'test', + documentFileHash: 'abc123' + }); + + await vi.waitFor(() => { + expect(document.querySelector('.bg-pdf-bg')).not.toBeNull(); + }); + }); + + it('renders the PDF controls when flashAnnotationId is set', async () => { + render(PdfViewer, { + url: '/api/documents/test/file', + documentId: 'test', + flashAnnotationId: 'ann-flashing' + }); + + await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible(); + }); + + it('renders the PDF controls when blockNumbers map is provided', async () => { + render(PdfViewer, { + url: '/api/documents/test/file', + documentId: 'test', + blockNumbers: { 'ann-1': 1, 'ann-2': 2 } + }); + + await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible(); + }); + + it('renders the PDF controls when activeAnnotationId is set', async () => { + render(PdfViewer, { + url: '/api/documents/test/file', + documentId: 'test', + activeAnnotationId: 'ann-1' + }); + + await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible(); + }); + + it('renders the PDF nav controls in transcribeMode + activeAnnotationId combo', async () => { + render(PdfViewer, { + url: '/api/documents/test/file', + documentId: 'test', + transcribeMode: true, + activeAnnotationId: 'ann-1' + }); + + // 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(); + }); + + it('renders the PDF controls when an onAnnotationClick callback is wired up', async () => { const onAnnotationClick = vi.fn(); - expect(() => - render(PdfViewer, { - url: '/api/documents/test/file', - documentId: 'test', - onAnnotationClick - }) - ).not.toThrow(); + render(PdfViewer, { + url: '/api/documents/test/file', + documentId: 'test', + onAnnotationClick + }); + + await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible(); }); it('shows the outdated-annotation notice when annotations have non-matching fileHash', async () => { @@ -170,9 +202,9 @@ describe('PdfViewer — loaded state', () => { documentFileHash: 'new-hash' }); - await new Promise((r) => setTimeout(r, 100)); - const notice = document.querySelector('[data-testid="annotation-outdated-notice"]'); - expect(notice).not.toBeNull(); + await vi.waitFor(() => { + expect(document.querySelector('[data-testid="annotation-outdated-notice"]')).not.toBeNull(); + }); } finally { fetchSpy.mockRestore(); } @@ -205,41 +237,42 @@ describe('PdfViewer — loaded state', () => { documentFileHash: 'matching-hash' }); - await new Promise((r) => setTimeout(r, 100)); - const notice = document.querySelector('[data-testid="annotation-outdated-notice"]'); - expect(notice).toBeNull(); + // 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 { fetchSpy.mockRestore(); } }); - it('handles fetch error when loading annotations gracefully', async () => { + it('still renders the controls when the annotations fetch rejects', async () => { const fetchSpy = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('network')); try { - expect(() => - render(PdfViewer, { - url: '/api/documents/test/file', - documentId: 'test' - }) - ).not.toThrow(); - await new Promise((r) => setTimeout(r, 50)); + render(PdfViewer, { + url: '/api/documents/test/file', + documentId: 'test' + }); + + // 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 { fetchSpy.mockRestore(); } }); - it('handles non-OK fetch response when loading annotations', async () => { + it('still renders the controls when the annotations fetch returns a non-OK status', async () => { const fetchSpy = vi .spyOn(globalThis, 'fetch') .mockResolvedValue(new Response('error', { status: 500 })); try { - expect(() => - render(PdfViewer, { - url: '/api/documents/test/file', - documentId: 'test' - }) - ).not.toThrow(); - await new Promise((r) => setTimeout(r, 50)); + render(PdfViewer, { + url: '/api/documents/test/file', + documentId: 'test' + }); + + await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible(); + expect(document.querySelector('[data-testid="annotation-outdated-notice"]')).toBeNull(); } finally { fetchSpy.mockRestore(); }