From e5eedc17d0693bf3ff46f93e5ae251952a90b5f8 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 10 May 2026 08:28:13 +0200 Subject: [PATCH] test(viewer): cover PdfViewer outdated-annotation notice + fetch errors Outdated notice when fileHash mismatches, no notice when matching, fetch error gracefully ignored, non-OK response ignored. 4 new tests covering ~8 branches. Refs #496. Co-Authored-By: Claude Sonnet 4.6 --- .../document/viewer/PdfViewer.svelte.test.ts | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/frontend/src/lib/document/viewer/PdfViewer.svelte.test.ts b/frontend/src/lib/document/viewer/PdfViewer.svelte.test.ts index 3918c6db..583e1f91 100644 --- a/frontend/src/lib/document/viewer/PdfViewer.svelte.test.ts +++ b/frontend/src/lib/document/viewer/PdfViewer.svelte.test.ts @@ -142,4 +142,106 @@ describe('PdfViewer — loaded state', () => { }) ).not.toThrow(); }); + + it('shows the outdated-annotation notice when annotations have non-matching fileHash', 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: 'old-hash' + } + ]), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + ); + try { + render(PdfViewer, { + url: '/api/documents/test/file', + documentId: 'test', + documentFileHash: 'new-hash' + }); + + await new Promise((r) => setTimeout(r, 100)); + const notice = document.querySelector('[data-testid="annotation-outdated-notice"]'); + expect(notice).not.toBeNull(); + } finally { + fetchSpy.mockRestore(); + } + }); + + it('does not show outdated-annotation notice when all annotations match', 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: 'matching-hash' + } + ]), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) + ); + try { + render(PdfViewer, { + url: '/api/documents/test/file', + documentId: 'test', + documentFileHash: 'matching-hash' + }); + + await new Promise((r) => setTimeout(r, 100)); + const notice = document.querySelector('[data-testid="annotation-outdated-notice"]'); + expect(notice).toBeNull(); + } finally { + fetchSpy.mockRestore(); + } + }); + + it('handles fetch error when loading annotations gracefully', 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)); + } finally { + fetchSpy.mockRestore(); + } + }); + + it('handles non-OK fetch response when loading annotations', 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)); + } finally { + fetchSpy.mockRestore(); + } + }); });