Files
familienarchiv/frontend/src/routes/admin/system/page.svelte.test.ts
Marcel 651684e49c test(admin): cover all admin/system status branches
Adds backfill success banner, RUNNING import-status rendering,
DONE import-status with processed count, FAILED thumbnail-status
with error message, DONE thumbnail-status with retry button.

5 new tests covering state-machine branches.

Refs #496.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 05:16:00 +02:00

242 lines
7.1 KiB
TypeScript

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<typeof vi.spyOn>;
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();
});
});