import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import Page from './+page.svelte'; afterEach(cleanup); afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); }); describe('Admin system page', () => { it('renders the backfill versions heading', async () => { render(Page, {}); await expect.element(page.getByText(/Verlaufsdaten auffüllen/i)).toBeInTheDocument(); }); it('renders the backfill versions button', async () => { render(Page, {}); await expect .element(page.getByRole('button', { name: /jetzt auffüllen/i })) .toBeInTheDocument(); }); it('renders the backfill file hashes heading', async () => { render(Page, {}); await expect .element(page.getByRole('heading', { name: /Datei-Hashes berechnen/i })) .toBeInTheDocument(); }); it('renders the file hashes button', async () => { render(Page, {}); await expect .element(page.getByRole('button', { name: /Datei-Hashes berechnen/i })) .toBeInTheDocument(); }); }); describe('Admin system page — mass import card', () => { beforeEach(() => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ state: 'IDLE', message: 'Kein Import gestartet.', processed: 0, startedAt: null }) }) ); }); it('renders the mass import heading', async () => { render(Page, {}); // getByText(/Massenimport/i) would match both the H2 heading AND the thumbnail // description paragraph that mentions "Massenimport" — use heading role instead. await expect.element(page.getByRole('heading', { name: /Massenimport/i })).toBeInTheDocument(); }); it('renders the start import button when idle', async () => { render(Page, {}); await expect.element(page.getByRole('button', { name: /Import starten/i })).toBeInTheDocument(); }); it('shows idle status text', async () => { render(Page, {}); await expect.element(page.getByText(/Kein Import gestartet/i)).toBeInTheDocument(); }); it('disables the start button and shows running state after click', async () => { // $effect calls fetchImportStatus() then fetchThumbnailStatus() on mount — // the mock must cover both before the POST trigger call. const fetchMock = vi .fn() // call 1: fetchImportStatus() on mount → IDLE .mockResolvedValueOnce({ ok: true, json: async () => ({ state: 'IDLE', message: 'Kein Import gestartet.', processed: 0, startedAt: null }) }) // call 2: fetchThumbnailStatus() on mount → IDLE (prevent thumbnail polling) .mockResolvedValueOnce({ ok: true, json: async () => ({ state: 'IDLE', message: '', total: 0, processed: 0, skipped: 0, failed: 0, startedAt: null }) }) // call 3: trigger POST → returns RUNNING .mockResolvedValueOnce({ ok: true, json: async () => ({ state: 'RUNNING', message: 'Import läuft...', processed: 0, startedAt: '2026-01-01T10:00:00' }) }); vi.stubGlobal('fetch', fetchMock); render(Page, {}); await expect.element(page.getByRole('button', { name: /Import starten/i })).toBeInTheDocument(); document.querySelector('[data-import-trigger]')!.click(); await expect.element(page.getByText(/Import läuft/i)).toBeInTheDocument(); }); it('shows done status and retry button after successful import', async () => { // Use mockResolvedValueOnce per call so thumbnail gets IDLE, not DONE. // Both cards in DONE state would render two "Erneut starten" buttons → strict mode. vi.stubGlobal( 'fetch', vi .fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ state: 'DONE', message: 'Import abgeschlossen.', processed: 42, startedAt: '2026-01-01T10:00:00' }) }) .mockResolvedValueOnce({ ok: true, json: async () => ({ state: 'IDLE', message: '', total: 0, processed: 0, skipped: 0, failed: 0, startedAt: null }) }) ); render(Page, {}); await expect.element(page.getByText(/42 Dokumente/i)).toBeInTheDocument(); await expect.element(page.getByRole('button', { name: /Erneut starten/i })).toBeInTheDocument(); }); it('shows failed status and retry button on error', async () => { // Use mockResolvedValueOnce so thumbnail gets IDLE, not FAILED. // Both cards in FAILED state would render two identical error messages → strict mode. vi.stubGlobal( 'fetch', vi .fn() .mockResolvedValueOnce({ ok: true, json: async () => ({ state: 'FAILED', message: 'Datei nicht gefunden.', processed: 0, startedAt: '2026-01-01T10:00:00' }) }) .mockResolvedValueOnce({ ok: true, json: async () => ({ state: 'IDLE', message: '', total: 0, processed: 0, skipped: 0, failed: 0, startedAt: null }) }) ); render(Page, {}); await expect.element(page.getByText(/Datei nicht gefunden/i)).toBeInTheDocument(); await expect.element(page.getByRole('button', { name: /Erneut starten/i })).toBeInTheDocument(); }); }); // ─── Polling lifecycle ──────────────────────────────────────────────────────── describe('Admin system page — polling lifecycle', () => { let setIntervalSpy: MockInstance; let clearIntervalSpy: MockInstance; beforeEach(() => { setIntervalSpy = vi.spyOn(globalThis, 'setInterval'); clearIntervalSpy = vi.spyOn(globalThis, 'clearInterval'); }); afterEach(() => { setIntervalSpy.mockRestore(); clearIntervalSpy.mockRestore(); }); it('starts polling when initial status is RUNNING', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ state: 'RUNNING', message: 'Import läuft...', processed: 0, startedAt: '2026-01-01T10:00:00' }) }) ); render(Page, {}); await expect.element(page.getByText(/Import läuft/i)).toBeInTheDocument(); expect(setIntervalSpy).toHaveBeenCalled(); }); it('does not start polling when initial status is IDLE', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ ok: true, json: async () => ({ state: 'IDLE', message: '', processed: 0, startedAt: null }) }) ); render(Page, {}); await expect.element(page.getByRole('button', { name: /Import starten/i })).toBeInTheDocument(); expect(setIntervalSpy).not.toHaveBeenCalled(); }); });