diff --git a/frontend/src/routes/admin/system/page.svelte.test.ts b/frontend/src/routes/admin/system/page.svelte.test.ts index 9a028cee..e0de72d8 100644 --- a/frontend/src/routes/admin/system/page.svelte.test.ts +++ b/frontend/src/routes/admin/system/page.svelte.test.ts @@ -9,18 +9,21 @@ describe('admin/system page', () => { let fetchSpy: ReturnType; beforeEach(() => { - fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue( - new Response( - JSON.stringify({ - state: 'IDLE', - message: '', - total: 0, - processed: 0, - skipped: 0, - failed: 0 - }), - { status: 200, headers: { 'Content-Type': 'application/json' } } - ) + // mockImplementation (not mockResolvedValue) returns a fresh Response per call so the + // body stream isn't already-consumed after the first read. + fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation( + async () => + new Response( + JSON.stringify({ + state: 'IDLE', + message: '', + total: 0, + processed: 0, + skipped: 0, + failed: 0 + }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ) ); }); @@ -79,36 +82,33 @@ describe('admin/system page', () => { render(AdminSystemPage, { props: {} }); - const btn = (await page - .getByRole('button', { name: /jetzt auffüllen/i }) - .element()) as HTMLButtonElement; - btn.click(); + await page.getByRole('button', { name: /jetzt auffüllen/i }).click(); - await new Promise((r) => setTimeout(r, 50)); - const calls = fetchSpy.mock.calls.map((c) => c[0].toString()); - expect(calls.some((c) => c.includes('backfill-versions'))).toBe(true); + await vi.waitFor(() => { + const calls = fetchSpy.mock.calls.map((c) => c[0].toString()); + expect(calls.some((c) => c.includes('backfill-versions'))).toBe(true); + }); }); it('triggers file-hashes backfill when its button is clicked', async () => { render(AdminSystemPage, { props: {} }); - const btn = (await page - .getByRole('button', { name: /datei-hashes berechnen/i }) - .element()) as HTMLButtonElement; - btn.click(); + await page.getByRole('button', { name: /datei-hashes berechnen/i }).click(); - await new Promise((r) => setTimeout(r, 50)); - const calls = fetchSpy.mock.calls.map((c) => c[0].toString()); - expect(calls.some((c) => c.includes('backfill-file-hashes'))).toBe(true); + await vi.waitFor(() => { + const calls = fetchSpy.mock.calls.map((c) => c[0].toString()); + expect(calls.some((c) => c.includes('backfill-file-hashes'))).toBe(true); + }); }); it('initial fetch loads import-status and thumbnail-status', async () => { render(AdminSystemPage, { props: {} }); - await new Promise((r) => setTimeout(r, 50)); - const calls = fetchSpy.mock.calls.map((c) => c[0].toString()); - expect(calls.some((c) => c.includes('import-status'))).toBe(true); - expect(calls.some((c) => c.includes('thumbnail-status'))).toBe(true); + await vi.waitFor(() => { + const calls = fetchSpy.mock.calls.map((c) => c[0].toString()); + expect(calls.some((c) => c.includes('import-status'))).toBe(true); + expect(calls.some((c) => c.includes('thumbnail-status'))).toBe(true); + }); }); it('shows the success banner after backfill versions completes', async () => { @@ -128,14 +128,11 @@ describe('admin/system page', () => { render(AdminSystemPage, { props: {} }); - const btn = (await page - .getByRole('button', { name: /jetzt auffüllen/i }) - .element()) as HTMLButtonElement; - btn.click(); + await page.getByRole('button', { name: /jetzt auffüllen/i }).click(); - await new Promise((r) => setTimeout(r, 100)); - const banner = document.querySelector('.bg-green-50'); - expect(banner).not.toBeNull(); + await vi.waitFor(() => { + expect(document.querySelector('.bg-green-50')).not.toBeNull(); + }); }); it('renders the running state for import-status', async () => { @@ -155,8 +152,9 @@ describe('admin/system page', () => { render(AdminSystemPage, { props: {} }); - await new Promise((r) => setTimeout(r, 100)); - expect(document.body.textContent).toMatch(/läuft|wird ausgeführt/i); + await vi.waitFor(() => { + expect(document.body.textContent).toMatch(/läuft|wird ausgeführt/i); + }); }); it('renders the DONE state with processed count for import-status', async () => { @@ -176,8 +174,9 @@ describe('admin/system page', () => { render(AdminSystemPage, { props: {} }); - await new Promise((r) => setTimeout(r, 100)); - expect(document.body.textContent).toContain('99'); + await vi.waitFor(() => { + expect(document.body.textContent).toContain('99'); + }); }); it('renders the FAILED state with the error message for thumbnail-status', async () => { @@ -205,8 +204,9 @@ describe('admin/system page', () => { render(AdminSystemPage, { props: {} }); - await new Promise((r) => setTimeout(r, 100)); - expect(document.body.textContent).toContain('connection refused'); + await vi.waitFor(() => { + expect(document.body.textContent).toContain('connection refused'); + }); }); it('renders the DONE state for thumbnail-status with retry button', async () => { @@ -234,9 +234,9 @@ describe('admin/system page', () => { render(AdminSystemPage, { props: {} }); - await new Promise((r) => setTimeout(r, 100)); - const banner = document.querySelector('[data-testid="thumbnails-status-done"]'); - expect(banner).not.toBeNull(); + await vi.waitFor(() => { + expect(document.querySelector('[data-testid="thumbnails-status-done"]')).not.toBeNull(); + }); }); it('renders the FAILED state for import-status with retry button', async () => { @@ -261,8 +261,9 @@ describe('admin/system page', () => { render(AdminSystemPage, { props: {} }); - await new Promise((r) => setTimeout(r, 100)); - expect(document.body.textContent).toContain('database error'); + await vi.waitFor(() => { + expect(document.body.textContent).toContain('database error'); + }); }); it('renders the running thumbnail status with progress count', async () => { @@ -290,33 +291,40 @@ describe('admin/system page', () => { render(AdminSystemPage, { props: {} }); - await new Promise((r) => setTimeout(r, 100)); - // Total 100, processed+skipped+failed = 36 - expect(document.body.textContent).toMatch(/36|100/); + await vi.waitFor(() => { + // Total 100, processed+skipped+failed = 36 — at least one of these surfaces. + expect(document.body.textContent).toMatch(/36|100/); + }); }); it('triggers thumbnail backfill when its button is clicked', async () => { render(AdminSystemPage, { props: {} }); - await new Promise((r) => setTimeout(r, 50)); + await vi.waitFor(() => { + expect(document.querySelector('[data-thumbnails-trigger]')).not.toBeNull(); + }); const btns = Array.from(document.querySelectorAll('[data-thumbnails-trigger]')); - (btns[0] as HTMLButtonElement)?.click(); + (btns[0] as HTMLButtonElement).click(); - await new Promise((r) => setTimeout(r, 50)); - const calls = fetchSpy.mock.calls.map((c) => c[0].toString()); - expect(calls.some((c) => c.includes('generate-thumbnails'))).toBe(true); + await vi.waitFor(() => { + const calls = fetchSpy.mock.calls.map((c) => c[0].toString()); + expect(calls.some((c) => c.includes('generate-thumbnails'))).toBe(true); + }); }); it('triggers import when import button is clicked from idle state', async () => { render(AdminSystemPage, { props: {} }); - await new Promise((r) => setTimeout(r, 50)); + await vi.waitFor(() => { + expect(document.querySelector('[data-import-trigger]')).not.toBeNull(); + }); const btns = Array.from(document.querySelectorAll('[data-import-trigger]')); - (btns[0] as HTMLButtonElement)?.click(); + (btns[0] as HTMLButtonElement).click(); - await new Promise((r) => setTimeout(r, 50)); - const calls = fetchSpy.mock.calls.map((c) => c[0].toString()); - expect(calls.some((c) => c.includes('trigger-import'))).toBe(true); + await vi.waitFor(() => { + const calls = fetchSpy.mock.calls.map((c) => c[0].toString()); + expect(calls.some((c) => c.includes('trigger-import'))).toBe(true); + }); }); it('renders the running thumbnail status without progress when total is 0', async () => { @@ -344,8 +352,8 @@ describe('admin/system page', () => { render(AdminSystemPage, { props: {} }); - await new Promise((r) => setTimeout(r, 100)); - // running state shown but no progress count when total=0 - expect(document.body.textContent).toMatch(/läuft|wird|generier/i); + await vi.waitFor(() => { + expect(document.body.textContent).toMatch(/läuft|wird|generier/i); + }); }); });