Files
familienarchiv/frontend/src/lib/document/viewer/PdfViewer.svelte.test.ts
Marcel d21ba8fed2
Some checks failed
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / OCR Service Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / fail2ban Regex (pull_request) Has been cancelled
CI / Compose Bucket Idempotency (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Failing after 2m3s
CI / OCR Service Tests (push) Successful in 16s
CI / Backend Unit Tests (push) Successful in 4m15s
CI / fail2ban Regex (push) Successful in 38s
CI / Compose Bucket Idempotency (push) Failing after 11s
refactor(pdf-viewer-tests): extract shared fake, add loadDocument error path, fix assertions
- Extract makeFakePdfjsLib / makeFakeLibLoader to testHelpers.ts — single
  source of truth used by both PdfViewer.svelte.test.ts and
  usePdfRenderer.svelte.test.ts; removes the diverging-fidelity DRY violation
  flagged by @felixbrandt and @saraholt in the PR review
- Add 'loadDocument sets error and loading=false when getDocument().promise
  rejects' test to usePdfRenderer.svelte.test.ts — closes the error-path gap
  flagged by @felixbrandt and @saraholt
- Replace toBeInTheDocument() with toBeVisible() in the three absorbed
  spec-file tests — uniform assertion style across the loaded-state describe
  block, as flagged by @felixbrandt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 16:19:26 +02:00

282 lines
8.4 KiB
TypeScript

import { describe, it, expect, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import PdfViewer from './PdfViewer.svelte';
import { makeFakeLibLoader } from './testHelpers';
afterEach(cleanup);
describe('PdfViewer — empty / error states', () => {
it('renders the no-file placeholder when url is empty', async () => {
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: '', libLoader: makeFakeLibLoader() });
const buttons = document.querySelectorAll('button');
expect(buttons.length).toBe(0);
});
});
describe('PdfViewer — loaded state', () => {
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,
libLoader: makeFakeLibLoader()
});
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('renders the canvas background container when annotationsDimmed=true', async () => {
render(PdfViewer, {
url: '/api/documents/test/file',
documentId: 'test',
annotationsDimmed: true,
libLoader: makeFakeLibLoader()
});
await vi.waitFor(() => {
expect(document.querySelector('.bg-pdf-bg')).not.toBeNull();
});
});
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,
documentFileHash: 'match',
libLoader: makeFakeLibLoader()
});
await expect
.element(page.getByRole('button', { name: /annotierungen verbergen/i }))
.toBeVisible();
} finally {
fetchSpy.mockRestore();
}
});
it('renders the canvas region when documentFileHash is provided', async () => {
render(PdfViewer, {
url: '/api/documents/test/file',
documentId: 'test',
documentFileHash: 'abc123',
libLoader: makeFakeLibLoader()
});
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',
libLoader: makeFakeLibLoader()
});
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 },
libLoader: makeFakeLibLoader()
});
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',
libLoader: makeFakeLibLoader()
});
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',
libLoader: makeFakeLibLoader()
});
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();
render(PdfViewer, {
url: '/api/documents/test/file',
documentId: 'test',
onAnnotationClick,
libLoader: makeFakeLibLoader()
});
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
});
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',
libLoader: makeFakeLibLoader()
});
await vi.waitFor(() => {
expect(document.querySelector('[data-testid="annotation-outdated-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',
libLoader: makeFakeLibLoader()
});
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
expect(document.querySelector('[data-testid="annotation-outdated-notice"]')).toBeNull();
} finally {
fetchSpy.mockRestore();
}
});
it('still renders the controls when the annotations fetch rejects', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('network'));
try {
render(PdfViewer, {
url: '/api/documents/test/file',
documentId: 'test',
libLoader: makeFakeLibLoader()
});
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
expect(document.querySelector('[data-testid="annotation-outdated-notice"]')).toBeNull();
} finally {
fetchSpy.mockRestore();
}
});
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 {
render(PdfViewer, {
url: '/api/documents/test/file',
documentId: 'test',
libLoader: makeFakeLibLoader()
});
await expect.element(page.getByRole('button', { name: 'Zurück' })).toBeVisible();
expect(document.querySelector('[data-testid="annotation-outdated-notice"]')).toBeNull();
} finally {
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 })).toBeVisible();
await expect.element(page.getByRole('button', { name: /weiter/i })).toBeVisible();
});
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 })).toBeVisible();
await expect.element(page.getByRole('button', { name: /verkleinern/i })).toBeVisible();
});
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/)).toBeVisible();
});
});