Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 4m6s
CI / OCR Service Tests (pull_request) Successful in 17s
CI / Backend Unit Tests (pull_request) Successful in 4m23s
CI / fail2ban Regex (pull_request) Successful in 38s
CI / Compose Bucket Idempotency (pull_request) Successful in 57s
Three test files were written against the old API shape (raw `message` field) before the statusCode i18n field was introduced, or used the wrong `expect` import path: - ImportStatusCard.svelte.test.ts: `@vitest/browser/context` does not export `expect` in this project's Vitest setup — use `vitest` like every other test file. - page.svelte.spec.ts: FAILED mock lacked `statusCode`; assertion matched old German raw message instead of the i18n string for IMPORT_FAILED_NO_SPREADSHEET. - page.svelte.test.ts: same pattern — mock lacked `statusCode`; assertion checked for raw backend string "database error" instead of the rendered i18n text. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
360 lines
10 KiB
TypeScript
360 lines
10 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(() => {
|
|
// 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);
|
|
});
|
|
});
|
|
});
|