From 81fe998c1756978ce1ed32a517bcbb8ce37c7563 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 11 May 2026 17:31:49 +0200 Subject: [PATCH] test(ocr): replace 8 setTimeout sleeps in OcrProgress with vi.waitFor waitForSource() helper polls for the EventSource constructor effect to register the mock; assertion blocks use vi.waitFor on the progress bar / heading / button changes after each SSE event dispatch. Co-Authored-By: Claude Opus 4.7 --- .../src/lib/ocr/OcrProgress.svelte.test.ts | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/frontend/src/lib/ocr/OcrProgress.svelte.test.ts b/frontend/src/lib/ocr/OcrProgress.svelte.test.ts index ec343d2d..ea24f1e3 100644 --- a/frontend/src/lib/ocr/OcrProgress.svelte.test.ts +++ b/frontend/src/lib/ocr/OcrProgress.svelte.test.ts @@ -41,6 +41,11 @@ afterEach(() => { lastSource = null; }); +async function waitForSource(): Promise { + await vi.waitFor(() => expect(lastSource).not.toBeNull()); + return lastSource as MockEventSource; +} + describe('OcrProgress', () => { it('renders the running progress block by default', async () => { render(OcrProgress, { props: { jobId: 'job-1', onDone: () => {} } }); @@ -57,33 +62,31 @@ describe('OcrProgress', () => { it('updates the progress bar when document events arrive', async () => { render(OcrProgress, { props: { jobId: 'job-1', onDone: () => {} } }); - // Wait a tick for $effect to run and EventSource to be created. - await new Promise((r) => setTimeout(r, 50)); - lastSource?.dispatch('document', { processed: 5, total: 10 }); - await new Promise((r) => setTimeout(r, 50)); + const src = await waitForSource(); + src.dispatch('document', { processed: 5, total: 10 }); - const bar = (await page.getByRole('progressbar').element()) as HTMLElement; - expect(bar.getAttribute('aria-valuenow')).toBe('50'); + await vi.waitFor(async () => { + const bar = (await page.getByRole('progressbar').element()) as HTMLElement; + expect(bar.getAttribute('aria-valuenow')).toBe('50'); + }); }); it('switches to the done state and calls onDone when the done event arrives', async () => { const onDone = vi.fn(); render(OcrProgress, { props: { jobId: 'job-1', onDone } }); - await new Promise((r) => setTimeout(r, 50)); - lastSource?.dispatch('done', {}); - await new Promise((r) => setTimeout(r, 50)); + const src = await waitForSource(); + src.dispatch('done', {}); - expect(onDone).toHaveBeenCalledOnce(); + await vi.waitFor(() => expect(onDone).toHaveBeenCalledOnce()); await expect.element(page.getByRole('heading', { name: /ocr läuft/i })).not.toBeInTheDocument(); }); it('switches to the error state when the error event arrives', async () => { render(OcrProgress, { props: { jobId: 'job-1', onDone: () => {} } }); - await new Promise((r) => setTimeout(r, 50)); - lastSource?.dispatch('error', {}); - await new Promise((r) => setTimeout(r, 50)); + const src = await waitForSource(); + src.dispatch('error', {}); await expect.element(page.getByRole('heading', { name: /ocr fehlgeschlagen/i })).toBeVisible(); }); @@ -91,9 +94,8 @@ describe('OcrProgress', () => { it('renders the retry button in the error state', async () => { render(OcrProgress, { props: { jobId: 'job-1', onDone: () => {} } }); - await new Promise((r) => setTimeout(r, 50)); - lastSource?.dispatch('error', {}); - await new Promise((r) => setTimeout(r, 50)); + const src = await waitForSource(); + src.dispatch('error', {}); await expect.element(page.getByRole('button', { name: /erneut versuchen/i })).toBeVisible(); });