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(() => { // 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' } } ) ); }); 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: {} }); await page.getByRole('button', { name: /jetzt auffüllen/i }).click(); 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: {} }); await page.getByRole('button', { name: /datei-hashes berechnen/i }).click(); 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 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 () => { 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: {} }); await page.getByRole('button', { name: /jetzt auffüllen/i }).click(); await vi.waitFor(() => { expect(document.querySelector('.bg-green-50')).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 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 () => { 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 vi.waitFor(() => { 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 vi.waitFor(() => { 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 vi.waitFor(() => { expect(document.querySelector('[data-testid="thumbnails-status-done"]')).not.toBeNull(); }); }); it('renders the FAILED state for import-status with retry button', async () => { fetchSpy.mockImplementation(async (url: RequestInfo | URL) => { const u = url.toString(); if (u.includes('import-status')) { return new Response( JSON.stringify({ state: 'FAILED', statusCode: 'IMPORT_FAILED_INTERNAL', 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 vi.waitFor(() => { expect(document.body.textContent).toContain('Interner Fehler beim Import'); }); }); it('renders the running thumbnail status with progress count', async () => { fetchSpy.mockImplementation(async (url: RequestInfo | URL) => { const u = url.toString(); if (u.includes('thumbnail-status')) { return new Response( JSON.stringify({ state: 'RUNNING', message: '', total: 100, processed: 30, skipped: 5, failed: 1, 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 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 vi.waitFor(() => { expect(document.querySelector('[data-thumbnails-trigger]')).not.toBeNull(); }); const btns = Array.from(document.querySelectorAll('[data-thumbnails-trigger]')); (btns[0] as HTMLButtonElement).click(); 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 vi.waitFor(() => { expect(document.querySelector('[data-import-trigger]')).not.toBeNull(); }); const btns = Array.from(document.querySelectorAll('[data-import-trigger]')); (btns[0] as HTMLButtonElement).click(); 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 () => { fetchSpy.mockImplementation(async (url: RequestInfo | URL) => { const u = url.toString(); if (u.includes('thumbnail-status')) { return new Response( JSON.stringify({ state: 'RUNNING', message: '', 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 vi.waitFor(() => { expect(document.body.textContent).toMatch(/läuft|wird|generier/i); }); }); });