import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import AdminSystemPage from './+page.svelte'; afterEach(cleanup); 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' } } ) ); }); afterEach(() => { fetchSpy?.mockRestore(); }); it('renders the backfill versions card', async () => { render(AdminSystemPage, { props: {} }); await expect .element(page.getByRole('heading', { name: /verlaufsdaten auffüllen/i })) .toBeVisible(); }); it('renders the backfill versions button enabled by default', async () => { render(AdminSystemPage, { props: {} }); const btn = (await page .getByRole('button', { name: /jetzt auffüllen/i }) .element()) as HTMLButtonElement; expect(btn.disabled).toBe(false); }); it('renders the backfill file-hashes card', async () => { render(AdminSystemPage, { props: {} }); await expect .element(page.getByRole('heading', { name: /datei-hashes berechnen/i })) .toBeVisible(); }); it('renders the backfill file-hashes button enabled by default', async () => { render(AdminSystemPage, { props: {} }); const btn = (await page .getByRole('button', { name: /datei-hashes berechnen/i }) .element()) as HTMLButtonElement; expect(btn.disabled).toBe(false); }); it('does not render the backfill success banner before any action', async () => { render(AdminSystemPage, { props: {} }); const banners = document.querySelectorAll('.bg-green-50'); expect(banners.length).toBe(0); }); it('triggers backfill versions when its button is clicked', async () => { fetchSpy.mockResolvedValueOnce( new Response(JSON.stringify({ count: 7 }), { status: 200, headers: { 'Content-Type': 'application/json' } }) ); render(AdminSystemPage, { props: {} }); const btn = (await page .getByRole('button', { name: /jetzt auffüllen/i }) .element()) as HTMLButtonElement; btn.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); }); 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 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); }); 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); }); it('shows the success banner after backfill versions completes', async () => { fetchSpy.mockImplementation(async (url: RequestInfo | URL) => { const u = url.toString(); if (u.includes('backfill-versions')) { return new Response(JSON.stringify({ count: 12 }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } return new Response(JSON.stringify({ state: 'IDLE' }), { status: 200, headers: { 'Content-Type': 'application/json' } }); }); render(AdminSystemPage, { props: {} }); const btn = (await page .getByRole('button', { name: /jetzt auffüllen/i }) .element()) as HTMLButtonElement; btn.click(); await new Promise((r) => setTimeout(r, 100)); const banner = document.querySelector('.bg-green-50'); expect(banner).not.toBeNull(); }); it('renders the running state for import-status', async () => { fetchSpy.mockImplementation(async (url: RequestInfo | URL) => { const u = url.toString(); if (u.includes('import-status')) { return new Response( JSON.stringify({ state: 'RUNNING', message: '', processed: 0, startedAt: null }), { status: 200, headers: { 'Content-Type': 'application/json' } } ); } return new Response(JSON.stringify({ state: 'IDLE' }), { status: 200, headers: { 'Content-Type': 'application/json' } }); }); render(AdminSystemPage, { props: {} }); await new Promise((r) => setTimeout(r, 100)); expect(document.body.textContent).toMatch(/läuft|wird ausgeführt/i); }); it('renders the DONE state with processed count for import-status', async () => { fetchSpy.mockImplementation(async (url: RequestInfo | URL) => { const u = url.toString(); if (u.includes('import-status')) { return new Response( JSON.stringify({ state: 'DONE', message: '', processed: 99, startedAt: null }), { status: 200, headers: { 'Content-Type': 'application/json' } } ); } return new Response(JSON.stringify({ state: 'IDLE' }), { status: 200, headers: { 'Content-Type': 'application/json' } }); }); render(AdminSystemPage, { props: {} }); await new Promise((r) => setTimeout(r, 100)); expect(document.body.textContent).toContain('99'); }); it('renders the FAILED state with the error message for thumbnail-status', async () => { fetchSpy.mockImplementation(async (url: RequestInfo | URL) => { const u = url.toString(); if (u.includes('thumbnail-status')) { return new Response( JSON.stringify({ state: 'FAILED', message: 'connection refused', total: 0, processed: 0, skipped: 0, failed: 0, startedAt: null }), { status: 200, headers: { 'Content-Type': 'application/json' } } ); } return new Response(JSON.stringify({ state: 'IDLE' }), { status: 200, headers: { 'Content-Type': 'application/json' } }); }); render(AdminSystemPage, { props: {} }); await new Promise((r) => setTimeout(r, 100)); expect(document.body.textContent).toContain('connection refused'); }); it('renders the DONE state for thumbnail-status with retry button', async () => { fetchSpy.mockImplementation(async (url: RequestInfo | URL) => { const u = url.toString(); if (u.includes('thumbnail-status')) { return new Response( JSON.stringify({ state: 'DONE', message: '', total: 100, processed: 95, skipped: 3, failed: 2, startedAt: null }), { status: 200, headers: { 'Content-Type': 'application/json' } } ); } return new Response(JSON.stringify({ state: 'IDLE' }), { status: 200, headers: { 'Content-Type': 'application/json' } }); }); render(AdminSystemPage, { props: {} }); await new Promise((r) => setTimeout(r, 100)); const banner = document.querySelector('[data-testid="thumbnails-status-done"]'); expect(banner).not.toBeNull(); }); });