test(documents): rewrite [id]/page test with behavioral assertions

Replaces 13 setTimeout sleeps with vi.waitFor and expect.element
auto-wait, and converts 17 .not.toThrow smoke tests into behavioral
assertions that verify what each scenario actually exposes:

- topbar mount + svelte:head title for prop pass-through cases
- Edit anchor surfaced when canWrite=true
- Details drawer open + sender displayName visible for sender data
- panel-close testid for transcribe-mode entry
- OCR progress heading 'OCR läuft' for RUNNING + jobId
- OCR spinner absent for 500 / DONE / PENDING-without-jobId / network-error

Runtime: 34s → 3.5s, no sleeps. Addresses Sara's "118 setTimeout" and
"74 .not.toThrow" blockers on PR #505.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-11 17:01:58 +02:00
committed by marcel
parent f75f34cbff
commit 57dc467f26

View File

@@ -1,5 +1,6 @@
import { describe, it, expect, vi, afterEach } from 'vitest'; import { describe, it, expect, vi, afterEach } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte'; import { cleanup, render } from 'vitest-browser-svelte';
import { page as browserPage } from 'vitest/browser';
const mockPage = { const mockPage = {
url: new URL('http://localhost/documents/d1'), url: new URL('http://localhost/documents/d1'),
@@ -63,22 +64,19 @@ const baseData = (overrides: Record<string, unknown> = {}) => ({
}); });
describe('documents/[id] page', () => { describe('documents/[id] page', () => {
it('renders the DocumentTopBar with the document title', async () => { it('renders the DocumentTopBar and resolves the document title in svelte:head', async () => {
mockPage.url = new URL('http://localhost/documents/d1'); mockPage.url = new URL('http://localhost/documents/d1');
render(DocumentDetailPage, { props: { data: baseData() } }); render(DocumentDetailPage, { props: { data: baseData() } });
// Just verify the page mounts and renders the top bar expect(document.querySelector('[data-topbar]')).not.toBeNull();
const topbar = document.querySelector('[data-topbar]'); await vi.waitFor(() => expect(document.title).toContain('Brief an Helene'));
expect(topbar).not.toBeNull();
}); });
it('renders the DocumentViewer in the page', async () => { it('mounts the page region with the [data-hydrated] container', async () => {
mockPage.url = new URL('http://localhost/documents/d1'); mockPage.url = new URL('http://localhost/documents/d1');
render(DocumentDetailPage, { props: { data: baseData() } }); render(DocumentDetailPage, { props: { data: baseData() } });
// DocumentViewer renders an absolute container; just check the page mounted expect(document.querySelector('[data-hydrated]')).not.toBeNull();
const main = document.body.firstElementChild;
expect(main).not.toBeNull();
}); });
it('persists last-visited document ID to localStorage on mount', async () => { it('persists last-visited document ID to localStorage on mount', async () => {
@@ -86,19 +84,17 @@ describe('documents/[id] page', () => {
mockPage.url = new URL('http://localhost/documents/d1'); mockPage.url = new URL('http://localhost/documents/d1');
render(DocumentDetailPage, { props: { data: baseData() } }); render(DocumentDetailPage, { props: { data: baseData() } });
await new Promise((r) => setTimeout(r, 50)); await vi.waitFor(() => {
const stored = localStorage.getItem('familienarchiv.lastVisited');
const stored = localStorage.getItem('familienarchiv.lastVisited'); expect(stored).toContain('d1');
expect(stored).toContain('d1'); });
}); });
it('uses doc.title as the document title when set', async () => { it('uses doc.title as the document title when set', async () => {
mockPage.url = new URL('http://localhost/documents/d1'); mockPage.url = new URL('http://localhost/documents/d1');
render(DocumentDetailPage, { props: { data: baseData() } }); render(DocumentDetailPage, { props: { data: baseData() } });
// The browser <title> reflects doc.title when set await vi.waitFor(() => expect(document.title).toContain('Brief an Helene'));
await new Promise((r) => setTimeout(r, 30));
expect(document.title).toContain('Brief an Helene');
}); });
it('falls back to originalFilename when title is empty', async () => { it('falls back to originalFilename when title is empty', async () => {
@@ -111,8 +107,7 @@ describe('documents/[id] page', () => {
} }
}); });
await new Promise((r) => setTimeout(r, 30)); await vi.waitFor(() => expect(document.title).toContain('fallback.pdf'));
expect(document.title).toContain('fallback.pdf');
}); });
it('falls back to "Dokument" when title and originalFilename are empty', async () => { it('falls back to "Dokument" when title and originalFilename are empty', async () => {
@@ -125,165 +120,165 @@ describe('documents/[id] page', () => {
} }
}); });
await new Promise((r) => setTimeout(r, 30)); await vi.waitFor(() => expect(document.title).toContain('Dokument'));
expect(document.title).toContain('Dokument');
}); });
it('renders without throwing when canWrite is true', async () => { it('renders the topbar Edit-link affordance when canWrite is true', async () => {
mockPage.url = new URL('http://localhost/documents/d4'); mockPage.url = new URL('http://localhost/documents/d4');
expect(() => render(DocumentDetailPage, { props: { data: baseData({ canWrite: true }) } });
render(DocumentDetailPage, { props: { data: baseData({ canWrite: true }) } })
).not.toThrow(); await expect.element(browserPage.getByRole('link', { name: 'Bearbeiten' })).toBeVisible();
}); });
it('renders without throwing when geschichten and inferredRelationship are set', async () => { it('renders the topbar when geschichten and inferredRelationship are passed through', async () => {
mockPage.url = new URL('http://localhost/documents/d5'); mockPage.url = new URL('http://localhost/documents/d5');
expect(() => render(DocumentDetailPage, {
render(DocumentDetailPage, { props: {
props: { data: baseData({
data: baseData({ geschichten: [{ id: 'g1', title: 'Story', publishedAt: null }],
geschichten: [{ id: 'g1', title: 'Story', publishedAt: null }], inferredRelationship: { label: 'PARENT_OF', from: 'p1', to: 'p2' },
inferredRelationship: { label: 'PARENT_OF', from: 'p1', to: 'p2' }, canBlogWrite: true
canBlogWrite: true })
}) }
} });
})
).not.toThrow(); expect(document.querySelector('[data-topbar]')).not.toBeNull();
await vi.waitFor(() => expect(document.title).toContain('Brief an Helene'));
}); });
it('renders without throwing when doc.id is empty', async () => { it('renders the topbar even when doc.id is empty (defensive)', async () => {
mockPage.url = new URL('http://localhost/documents/d-empty'); mockPage.url = new URL('http://localhost/documents/d-empty');
expect(() => render(DocumentDetailPage, {
render(DocumentDetailPage, { props: { data: baseData({ document: { ...baseDoc, id: '', title: 'No ID' } }) }
props: { data: baseData({ document: { ...baseDoc, id: '', title: 'No ID' } }) } });
})
).not.toThrow(); expect(document.querySelector('[data-topbar]')).not.toBeNull();
await vi.waitFor(() => expect(document.title).toContain('No ID'));
}); });
it('renders with task=transcribe in the URL without throwing', async () => { it('renders sender data in the metadata drawer when sender is populated', async () => {
mockPage.url = new URL('http://localhost/documents/d6?task=transcribe');
expect(() =>
render(DocumentDetailPage, {
props: { data: baseData({ document: { ...baseDoc, id: 'd6' } }) }
})
).not.toThrow();
});
it('renders without throwing with sender/receivers populated', async () => {
mockPage.url = new URL('http://localhost/documents/d7'); mockPage.url = new URL('http://localhost/documents/d7');
expect(() => render(DocumentDetailPage, {
render(DocumentDetailPage, { props: {
props: { data: baseData({
data: baseData({ document: {
document: { ...baseDoc,
...baseDoc, id: 'd7',
id: 'd7', sender: { id: 's1', displayName: 'Anna Schmidt' },
sender: { id: 's1', displayName: 'Anna Schmidt' }, receivers: [{ id: 'r1', displayName: 'Bert Meier' }]
receivers: [{ id: 'r1', displayName: 'Bert Meier' }] }
} })
}) }
} });
})
).not.toThrow(); // The topbar chip row is hidden below md; the drawer renders the full displayName at any viewport.
await browserPage.getByRole('button', { name: 'Details' }).click();
await expect.element(browserPage.getByText('Anna Schmidt')).toBeVisible();
}); });
it('renders without throwing when filePath is set on the document', async () => { it('renders the topbar when filePath is set on the document', async () => {
mockPage.url = new URL('http://localhost/documents/d8'); mockPage.url = new URL('http://localhost/documents/d8');
expect(() => render(DocumentDetailPage, {
render(DocumentDetailPage, { props: {
props: { data: baseData({
data: baseData({ document: {
document: { ...baseDoc,
...baseDoc, id: 'd8',
id: 'd8', filePath: 's3://bucket/file.pdf',
filePath: 's3://bucket/file.pdf', contentType: 'application/pdf'
contentType: 'application/pdf' }
} })
}) }
} });
})
).not.toThrow(); expect(document.querySelector('[data-topbar]')).not.toBeNull();
await vi.waitFor(() => expect(document.title).toContain('Brief an Helene'));
}); });
it('renders without throwing with a complete user object', async () => { it('renders the topbar with a complete user object passed through', async () => {
mockPage.url = new URL('http://localhost/documents/d9'); mockPage.url = new URL('http://localhost/documents/d9');
expect(() => render(DocumentDetailPage, {
render(DocumentDetailPage, { props: {
props: { data: baseData({
data: baseData({ document: { ...baseDoc, id: 'd9' },
document: { ...baseDoc, id: 'd9' }, user: { id: 'u1', firstName: 'Anna', lastName: 'S', email: 'a@x' }
user: { id: 'u1', firstName: 'Anna', lastName: 'S', email: 'a@x' } })
}) }
} });
})
).not.toThrow(); expect(document.querySelector('[data-topbar]')).not.toBeNull();
}); });
it('handles Escape keydown without throwing (close transcribe path)', async () => { it('Escape keydown leaves the transcribe panel hidden when not already in transcribe mode', async () => {
mockPage.url = new URL('http://localhost/documents/d10'); mockPage.url = new URL('http://localhost/documents/d10');
render(DocumentDetailPage, { render(DocumentDetailPage, {
props: { data: baseData({ document: { ...baseDoc, id: 'd10' } }) } props: { data: baseData({ document: { ...baseDoc, id: 'd10' } }) }
}); });
expect(() => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
).not.toThrow(); expect(document.querySelector('[data-testid="panel-close"]')).toBeNull();
}); });
it('handles non-Escape keydown without firing close handler', async () => { it('non-Escape keydown does not affect the transcribe panel state', async () => {
mockPage.url = new URL('http://localhost/documents/d11'); mockPage.url = new URL('http://localhost/documents/d11');
render(DocumentDetailPage, { render(DocumentDetailPage, {
props: { data: baseData({ document: { ...baseDoc, id: 'd11' } }) } props: { data: baseData({ document: { ...baseDoc, id: 'd11' } }) }
}); });
expect(() => document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }))
).not.toThrow(); expect(document.querySelector('[data-topbar]')).not.toBeNull();
expect(document.querySelector('[data-testid="panel-close"]')).toBeNull();
}); });
it('renders without throwing with a deep-link comment query param', async () => { it('renders the topbar with a deep-link comment query param', async () => {
mockPage.url = new URL('http://localhost/documents/d12?comment=c-abc'); mockPage.url = new URL('http://localhost/documents/d12?comment=c-abc');
expect(() => render(DocumentDetailPage, {
render(DocumentDetailPage, { props: { data: baseData({ document: { ...baseDoc, id: 'd12' } }) }
props: { data: baseData({ document: { ...baseDoc, id: 'd12' } }) } });
})
).not.toThrow(); expect(document.querySelector('[data-topbar]')).not.toBeNull();
}); });
it('renders without throwing with all metadata populated', async () => { it('renders sender name and Edit affordance with all metadata populated', async () => {
mockPage.url = new URL('http://localhost/documents/d-meta'); mockPage.url = new URL('http://localhost/documents/d-meta');
expect(() => render(DocumentDetailPage, {
render(DocumentDetailPage, { props: {
props: { data: baseData({
data: baseData({ document: {
document: { ...baseDoc,
...baseDoc, id: 'd-meta',
id: 'd-meta', sender: { id: 's1', displayName: 'Anna' },
sender: { id: 's1', displayName: 'Anna' }, receivers: [
receivers: [ { id: 'r1', displayName: 'Bert' },
{ id: 'r1', displayName: 'Bert' }, { id: 'r2', displayName: 'Carl' }
{ id: 'r2', displayName: 'Carl' } ],
], tags: [
tags: [ { id: 't1', name: 'Familie' },
{ id: 't1', name: 'Familie' }, { id: 't2', name: 'Reise' }
{ id: 't2', name: 'Reise' } ],
], location: 'Berlin',
location: 'Berlin', scriptType: 'KURRENT',
scriptType: 'KURRENT', trainingLabels: ['KURRENT_RECOGNITION']
trainingLabels: ['KURRENT_RECOGNITION'] },
}, user: { id: 'u1', firstName: 'Anna' },
user: { id: 'u1', firstName: 'Anna' }, geschichten: [{ id: 'g1', title: 'Story', publishedAt: null }],
geschichten: [{ id: 'g1', title: 'Story', publishedAt: null }], inferredRelationship: { labelFromA: 'Vater', labelFromB: 'Tochter' },
inferredRelationship: { labelFromA: 'Vater', labelFromB: 'Tochter' }, canBlogWrite: true,
canBlogWrite: true, canWrite: true
canWrite: true })
}) }
} });
})
).not.toThrow(); // Edit affordance is always rendered when canWrite=true; sender chip is below md, so
// open the drawer and assert sender displayName surfaces there.
await expect.element(browserPage.getByRole('link', { name: 'Bearbeiten' })).toBeVisible();
await browserPage.getByRole('button', { name: 'Details' }).click();
await expect.element(browserPage.getByText('Anna')).toBeVisible();
}); });
it('renders the transcribe-mode forced via query string', async () => { it('enters transcribe mode and shows the panel close button when ?task=transcribe is set', async () => {
mockPage.url = new URL('http://localhost/documents/d-task?task=transcribe'); mockPage.url = new URL('http://localhost/documents/d-task?task=transcribe');
render(DocumentDetailPage, { render(DocumentDetailPage, {
props: { props: {
@@ -294,30 +289,29 @@ describe('documents/[id] page', () => {
} }
}); });
await new Promise((r) => setTimeout(r, 80)); await vi.waitFor(() => {
// DocumentTopBar should mount; transcribeMode flag was set by the URL param expect(document.querySelector('[data-testid="panel-close"]')).not.toBeNull();
const main = document.body.firstElementChild; });
expect(main).not.toBeNull();
}); });
it('handles transcription-block fetch failure gracefully', async () => { it('keeps the transcribe panel mounted when the transcription-block 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 {
mockPage.url = new URL('http://localhost/documents/d-fail?task=transcribe'); mockPage.url = new URL('http://localhost/documents/d-fail?task=transcribe');
expect(() => render(DocumentDetailPage, {
render(DocumentDetailPage, { props: {
props: { data: baseData({ document: { ...baseDoc, id: 'd-fail' } })
data: baseData({ document: { ...baseDoc, id: 'd-fail' } }) }
} });
}) await vi.waitFor(() => {
).not.toThrow(); expect(document.querySelector('[data-testid="panel-close"]')).not.toBeNull();
await new Promise((r) => setTimeout(r, 100)); });
} finally { } finally {
fetchSpy.mockRestore(); fetchSpy.mockRestore();
} }
}); });
it('renders blocks fetched in transcribe mode', async () => { it('renders fetched block text in read mode after blocks resolve', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue( const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(
new Response( new Response(
JSON.stringify([ JSON.stringify([
@@ -339,15 +333,13 @@ describe('documents/[id] page', () => {
render(DocumentDetailPage, { render(DocumentDetailPage, {
props: { data: baseData({ document: { ...baseDoc, id: 'd-blocks' } }) } props: { data: baseData({ document: { ...baseDoc, id: 'd-blocks' } }) }
}); });
await new Promise((r) => setTimeout(r, 100)); await expect.element(browserPage.getByText('Erster')).toBeVisible();
// No throw; the page rendered with fetched blocks.
expect(document.body.firstElementChild).not.toBeNull();
} finally { } finally {
fetchSpy.mockRestore(); fetchSpy.mockRestore();
} }
}); });
it('reads localStorage on mount for last-visited (existing data overwritten)', async () => { it('overwrites a previously stored last-visited document with the current one', async () => {
localStorage.setItem( localStorage.setItem(
'familienarchiv.lastVisited', 'familienarchiv.lastVisited',
JSON.stringify({ id: 'old-doc', title: 'Old' }) JSON.stringify({ id: 'old-doc', title: 'Old' })
@@ -358,29 +350,31 @@ describe('documents/[id] page', () => {
data: baseData({ document: { ...baseDoc, id: 'd-new', title: 'New Doc' } }) data: baseData({ document: { ...baseDoc, id: 'd-new', title: 'New Doc' } })
} }
}); });
await new Promise((r) => setTimeout(r, 50)); await vi.waitFor(() => {
const stored = JSON.parse(localStorage.getItem('familienarchiv.lastVisited') ?? '{}'); const stored = JSON.parse(localStorage.getItem('familienarchiv.lastVisited') ?? '{}');
expect(stored.id).toBe('d-new'); expect(stored.id).toBe('d-new');
});
}); });
it('renders the OCR error path when ocr-status fetch returns 500', async () => { it('does not show the OCR-running spinner when ocr-status fetch returns 500', 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 {
mockPage.url = new URL('http://localhost/documents/d-ocr-fail?task=transcribe'); mockPage.url = new URL('http://localhost/documents/d-ocr-fail?task=transcribe');
expect(() => render(DocumentDetailPage, {
render(DocumentDetailPage, { props: { data: baseData({ document: { ...baseDoc, id: 'd-ocr-fail' } }) }
props: { data: baseData({ document: { ...baseDoc, id: 'd-ocr-fail' } }) } });
}) await vi.waitFor(() => {
).not.toThrow(); expect(document.querySelector('[data-testid="panel-close"]')).not.toBeNull();
await new Promise((r) => setTimeout(r, 100)); });
expect(browserPage.getByText('OCR läuft').elements()).toHaveLength(0);
} finally { } finally {
fetchSpy.mockRestore(); fetchSpy.mockRestore();
} }
}); });
it('renders ocr-status RUNNING and starts polling without throwing', async () => { it('shows the OCR-running spinner heading when ocr-status returns RUNNING with a jobId', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (url) => { const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (url) => {
const u = url.toString(); const u = url.toString();
if (u.includes('ocr-status')) { if (u.includes('ocr-status')) {
@@ -402,52 +396,49 @@ describe('documents/[id] page', () => {
render(DocumentDetailPage, { render(DocumentDetailPage, {
props: { data: baseData({ document: { ...baseDoc, id: 'd-ocr-run' } }) } props: { data: baseData({ document: { ...baseDoc, id: 'd-ocr-run' } }) }
}); });
await new Promise((r) => setTimeout(r, 100)); await expect.element(browserPage.getByText('OCR läuft')).toBeVisible();
// Page renders; the ocrRunning state is on but we don't assert specific UI
// because that requires deep DOM access into TranscriptionEditView.
expect(document.body.firstElementChild).not.toBeNull();
} finally { } finally {
fetchSpy.mockRestore(); fetchSpy.mockRestore();
} }
}); });
it('renders without throwing when document has all OCR-relevant fields populated', async () => { it('renders the topbar when the document has all OCR-relevant fields populated', async () => {
mockPage.url = new URL('http://localhost/documents/d-ocr-meta?task=transcribe'); mockPage.url = new URL('http://localhost/documents/d-ocr-meta?task=transcribe');
expect(() => render(DocumentDetailPage, {
render(DocumentDetailPage, { props: {
props: { data: baseData({
data: baseData({ document: {
document: { ...baseDoc,
...baseDoc, id: 'd-ocr-meta',
id: 'd-ocr-meta', scriptType: 'KURRENT',
scriptType: 'KURRENT', trainingLabels: ['KURRENT_RECOGNITION'],
trainingLabels: ['KURRENT_RECOGNITION'], filePath: 's3://bucket/file.pdf',
filePath: 's3://bucket/file.pdf', fileHash: 'hash-abc'
fileHash: 'hash-abc' },
}, canWrite: true,
canWrite: true, user: { id: 'u1', firstName: 'Anna' }
user: { id: 'u1', firstName: 'Anna' } })
}) }
} });
})
).not.toThrow(); expect(document.querySelector('[data-topbar]')).not.toBeNull();
}); });
it('handles empty geschichten array as falsy (geschichten ?? []) branch', async () => { it('treats undefined geschichten as the empty array (geschichten ?? [] branch)', async () => {
mockPage.url = new URL('http://localhost/documents/d-no-stories'); mockPage.url = new URL('http://localhost/documents/d-no-stories');
expect(() => render(DocumentDetailPage, {
render(DocumentDetailPage, { props: {
props: { data: baseData({
data: baseData({ document: { ...baseDoc, id: 'd-no-stories' },
document: { ...baseDoc, id: 'd-no-stories' }, geschichten: undefined
geschichten: undefined })
}) }
} });
})
).not.toThrow(); expect(document.querySelector('[data-topbar]')).not.toBeNull();
}); });
it('handles ocr-status DONE state without restarting polling', async () => { it('does not show the OCR-running spinner when ocr-status returns DONE', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (url) => { const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (url) => {
const u = url.toString(); const u = url.toString();
if (u.includes('ocr-status')) { if (u.includes('ocr-status')) {
@@ -463,15 +454,16 @@ describe('documents/[id] page', () => {
render(DocumentDetailPage, { render(DocumentDetailPage, {
props: { data: baseData({ document: { ...baseDoc, id: 'd-ocr-done' } }) } props: { data: baseData({ document: { ...baseDoc, id: 'd-ocr-done' } }) }
}); });
await new Promise((r) => setTimeout(r, 100)); await vi.waitFor(() => {
// No throw — the ocrRunning state stays false because status was DONE expect(document.querySelector('[data-testid="panel-close"]')).not.toBeNull();
expect(document.body.firstElementChild).not.toBeNull(); });
expect(browserPage.getByText('OCR läuft').elements()).toHaveLength(0);
} finally { } finally {
fetchSpy.mockRestore(); fetchSpy.mockRestore();
} }
}); });
it('handles ocr-status without jobId (no polling started)', async () => { it('does not show the OCR-running spinner when ocr-status returns PENDING without jobId', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (url) => { const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (url) => {
const u = url.toString(); const u = url.toString();
if (u.includes('ocr-status')) { if (u.includes('ocr-status')) {
@@ -484,18 +476,19 @@ describe('documents/[id] page', () => {
}); });
try { try {
mockPage.url = new URL('http://localhost/documents/d-ocr-no-job?task=transcribe'); mockPage.url = new URL('http://localhost/documents/d-ocr-no-job?task=transcribe');
expect(() => render(DocumentDetailPage, {
render(DocumentDetailPage, { props: { data: baseData({ document: { ...baseDoc, id: 'd-ocr-no-job' } }) }
props: { data: baseData({ document: { ...baseDoc, id: 'd-ocr-no-job' } }) } });
}) await vi.waitFor(() => {
).not.toThrow(); expect(document.querySelector('[data-testid="panel-close"]')).not.toBeNull();
await new Promise((r) => setTimeout(r, 100)); });
expect(browserPage.getByText('OCR läuft').elements()).toHaveLength(0);
} finally { } finally {
fetchSpy.mockRestore(); fetchSpy.mockRestore();
} }
}); });
it('handles ocr-status fetch network error gracefully', async () => { it('does not show the OCR-running spinner when the ocr-status fetch throws', async () => {
const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (url) => { const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(async (url) => {
const u = url.toString(); const u = url.toString();
if (u.includes('ocr-status')) { if (u.includes('ocr-status')) {
@@ -505,12 +498,13 @@ describe('documents/[id] page', () => {
}); });
try { try {
mockPage.url = new URL('http://localhost/documents/d-ocr-throw?task=transcribe'); mockPage.url = new URL('http://localhost/documents/d-ocr-throw?task=transcribe');
expect(() => render(DocumentDetailPage, {
render(DocumentDetailPage, { props: { data: baseData({ document: { ...baseDoc, id: 'd-ocr-throw' } }) }
props: { data: baseData({ document: { ...baseDoc, id: 'd-ocr-throw' } }) } });
}) await vi.waitFor(() => {
).not.toThrow(); expect(document.querySelector('[data-testid="panel-close"]')).not.toBeNull();
await new Promise((r) => setTimeout(r, 100)); });
expect(browserPage.getByText('OCR läuft').elements()).toHaveLength(0);
} finally { } finally {
fetchSpy.mockRestore(); fetchSpy.mockRestore();
} }