test(viewer): rewrite PdfViewer test with behavioral assertions

Replaces 6 setTimeout sleeps with vi.waitFor and expect.element
auto-wait, and converts 9 .not.toThrow smoke tests into assertions
on the rendered PDF nav controls (Zurück/Weiter/Vergrößern/Verkleinern)
and the conditional outdated-annotation notice / annotation visibility
toggle. transcribeMode test now mocks the annotations fetch so the
toggle button is actually rendered (annotationCount > 0 guard).

Runtime: 33s → 4.5s.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-11 17:13:09 +02:00
committed by marcel
parent 92af7d22da
commit 4045cec457

View File

@@ -40,107 +40,139 @@ describe('PdfViewer — empty / error states', () => {
render(PdfViewer, { url: '' }); render(PdfViewer, { url: '' });
const buttons = document.querySelectorAll('button'); const buttons = document.querySelectorAll('button');
// Empty state has no nav buttons
expect(buttons.length).toBe(0); expect(buttons.length).toBe(0);
}); });
}); });
describe('PdfViewer — loaded state', () => { 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, { render(PdfViewer, {
url: '/api/documents/test/file', url: '/api/documents/test/file',
documentId: 'test', documentId: 'test',
annotationReloadKey: 0 annotationReloadKey: 0
}); });
// The PdfControls component renders the toggle button // PdfControls renders its nav + zoom buttons once the document.promise resolves.
await new Promise((r) => setTimeout(r, 50)); await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
const buttons = document.querySelectorAll('button'); await expect.element(page.getByRole('button', { name: 'Weiter' })).toBeVisible();
expect(buttons.length).toBeGreaterThan(0); 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, { render(PdfViewer, {
url: '/api/documents/test/file', url: '/api/documents/test/file',
documentId: 'test', documentId: 'test',
annotationsDimmed: true annotationsDimmed: true
}); });
await new Promise((r) => setTimeout(r, 50)); await vi.waitFor(() => {
// just confirm no throw in the dimmed code path expect(document.querySelector('.bg-pdf-bg')).not.toBeNull();
expect(document.querySelector('.bg-pdf-bg')).not.toBeNull(); });
}); });
it('renders without throwing in transcribeMode', async () => { it('forces the annotation toggle into "hide" mode when transcribeMode is true and annotations exist', async () => {
expect(() => const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
render(PdfViewer, { new Response(
url: '/api/documents/test/file', JSON.stringify([
documentId: 'test', {
transcribeMode: true id: 'a1',
}) documentId: 'test',
).not.toThrow(); pageNumber: 1,
}); x: 0.1,
y: 0.1,
it('renders without throwing with a documentFileHash and matching annotations', async () => { width: 0.1,
expect(() => height: 0.1,
render(PdfViewer, { color: '#000',
url: '/api/documents/test/file', createdAt: '2026-01-01T00:00:00Z',
documentId: 'test', fileHash: 'match'
documentFileHash: 'abc123' }
}) ]),
).not.toThrow(); { status: 200, headers: { 'Content-Type': 'application/json' } }
}); )
);
it('renders without throwing with a flashAnnotationId set', async () => { try {
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(() =>
render(PdfViewer, { render(PdfViewer, {
url: '/api/documents/test/file', url: '/api/documents/test/file',
documentId: 'test', documentId: 'test',
transcribeMode: true, transcribeMode: true,
activeAnnotationId: 'ann-1' documentFileHash: 'match'
}) });
).not.toThrow();
// 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(); const onAnnotationClick = vi.fn();
expect(() => render(PdfViewer, {
render(PdfViewer, { url: '/api/documents/test/file',
url: '/api/documents/test/file', documentId: 'test',
documentId: 'test', onAnnotationClick
onAnnotationClick });
})
).not.toThrow(); await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
}); });
it('shows the outdated-annotation notice when annotations have non-matching fileHash', async () => { it('shows the outdated-annotation notice when annotations have non-matching fileHash', async () => {
@@ -170,9 +202,9 @@ describe('PdfViewer — loaded state', () => {
documentFileHash: 'new-hash' documentFileHash: 'new-hash'
}); });
await new Promise((r) => setTimeout(r, 100)); await vi.waitFor(() => {
const notice = document.querySelector('[data-testid="annotation-outdated-notice"]'); expect(document.querySelector('[data-testid="annotation-outdated-notice"]')).not.toBeNull();
expect(notice).not.toBeNull(); });
} finally { } finally {
fetchSpy.mockRestore(); fetchSpy.mockRestore();
} }
@@ -205,41 +237,42 @@ describe('PdfViewer — loaded state', () => {
documentFileHash: 'matching-hash' documentFileHash: 'matching-hash'
}); });
await new Promise((r) => setTimeout(r, 100)); // Controls finish mounting, and the outdated notice stays absent.
const notice = document.querySelector('[data-testid="annotation-outdated-notice"]'); await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
expect(notice).toBeNull(); expect(document.querySelector('[data-testid="annotation-outdated-notice"]')).toBeNull();
} finally { } finally {
fetchSpy.mockRestore(); 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')); const fetchSpy = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('network'));
try { try {
expect(() => render(PdfViewer, {
render(PdfViewer, { url: '/api/documents/test/file',
url: '/api/documents/test/file', documentId: 'test'
documentId: 'test' });
})
).not.toThrow(); // PDF rendering does not depend on the annotations fetch — controls still appear.
await new Promise((r) => setTimeout(r, 50)); await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
expect(document.querySelector('[data-testid="annotation-outdated-notice"]')).toBeNull();
} finally { } finally {
fetchSpy.mockRestore(); 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 const fetchSpy = vi
.spyOn(globalThis, 'fetch') .spyOn(globalThis, 'fetch')
.mockResolvedValue(new Response('error', { status: 500 })); .mockResolvedValue(new Response('error', { status: 500 }));
try { try {
expect(() => render(PdfViewer, {
render(PdfViewer, { url: '/api/documents/test/file',
url: '/api/documents/test/file', documentId: 'test'
documentId: 'test' });
})
).not.toThrow(); await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
await new Promise((r) => setTimeout(r, 50)); expect(document.querySelector('[data-testid="annotation-outdated-notice"]')).toBeNull();
} finally { } finally {
fetchSpy.mockRestore(); fetchSpy.mockRestore();
} }