import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import OcrProgress from './OcrProgress.svelte'; // Mock EventSource so the $effect doesn't open a real SSE connection. class MockEventSource { url: string; listeners = new Map(); onerror: (() => void) | null = null; close = vi.fn(); constructor(url: string) { this.url = url; } addEventListener(type: string, fn: EventListener) { this.listeners.set(type, fn); } dispatch(type: string, data: unknown) { const fn = this.listeners.get(type); if (fn) fn({ data: JSON.stringify(data) } as MessageEvent); } } let lastSource: MockEventSource | null = null; beforeEach(() => { const trackedFactory = function (url: string) { const src = new MockEventSource(url); lastSource = src; return src; }; (globalThis as unknown as { EventSource: unknown }).EventSource = new Proxy(MockEventSource, { construct(_target, args) { return trackedFactory(args[0]); } }); }); afterEach(() => { cleanup(); 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: () => {} } }); await expect.element(page.getByRole('heading', { name: /ocr läuft/i })).toBeVisible(); }); it('renders the progress bar with the running label', async () => { render(OcrProgress, { props: { jobId: 'job-1', onDone: () => {} } }); expect(document.querySelector('[role="progressbar"]')).not.toBeNull(); }); it('updates the progress bar when document events arrive', async () => { render(OcrProgress, { props: { jobId: 'job-1', onDone: () => {} } }); const src = await waitForSource(); src.dispatch('document', { processed: 5, total: 10 }); 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 } }); const src = await waitForSource(); src.dispatch('done', {}); 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: () => {} } }); const src = await waitForSource(); src.dispatch('error', {}); await expect.element(page.getByRole('heading', { name: /ocr fehlgeschlagen/i })).toBeVisible(); }); it('renders the retry button in the error state', async () => { render(OcrProgress, { props: { jobId: 'job-1', onDone: () => {} } }); const src = await waitForSource(); src.dispatch('error', {}); await expect.element(page.getByRole('button', { name: /erneut versuchen/i })).toBeVisible(); }); });