test(coverage): drive browser tests to 80% on all metrics (#496) #505

Merged
marcel merged 189 commits from feat/issue-496-browser-coverage-tests into main 2026-05-11 21:50:39 +02:00
Showing only changes of commit c89b9a2d60 - Show all commits

View File

@@ -9,18 +9,21 @@ 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' } }
)
// 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' } }
)
);
});
@@ -79,36 +82,33 @@ describe('admin/system page', () => {
render(AdminSystemPage, { props: {} });
const btn = (await page
.getByRole('button', { name: /jetzt auffüllen/i })
.element()) as HTMLButtonElement;
btn.click();
await page.getByRole('button', { name: /jetzt auffüllen/i }).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);
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: {} });
const btn = (await page
.getByRole('button', { name: /datei-hashes berechnen/i })
.element()) as HTMLButtonElement;
btn.click();
await page.getByRole('button', { name: /datei-hashes berechnen/i }).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);
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 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);
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 () => {
@@ -128,14 +128,11 @@ describe('admin/system page', () => {
render(AdminSystemPage, { props: {} });
const btn = (await page
.getByRole('button', { name: /jetzt auffüllen/i })
.element()) as HTMLButtonElement;
btn.click();
await page.getByRole('button', { name: /jetzt auffüllen/i }).click();
await new Promise((r) => setTimeout(r, 100));
const banner = document.querySelector('.bg-green-50');
expect(banner).not.toBeNull();
await vi.waitFor(() => {
expect(document.querySelector('.bg-green-50')).not.toBeNull();
});
});
it('renders the running state for import-status', async () => {
@@ -155,8 +152,9 @@ describe('admin/system page', () => {
render(AdminSystemPage, { props: {} });
await new Promise((r) => setTimeout(r, 100));
expect(document.body.textContent).toMatch(/läuft|wird ausgeführt/i);
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 () => {
@@ -176,8 +174,9 @@ describe('admin/system page', () => {
render(AdminSystemPage, { props: {} });
await new Promise((r) => setTimeout(r, 100));
expect(document.body.textContent).toContain('99');
await vi.waitFor(() => {
expect(document.body.textContent).toContain('99');
});
});
it('renders the FAILED state with the error message for thumbnail-status', async () => {
@@ -205,8 +204,9 @@ describe('admin/system page', () => {
render(AdminSystemPage, { props: {} });
await new Promise((r) => setTimeout(r, 100));
expect(document.body.textContent).toContain('connection refused');
await vi.waitFor(() => {
expect(document.body.textContent).toContain('connection refused');
});
});
it('renders the DONE state for thumbnail-status with retry button', async () => {
@@ -234,9 +234,9 @@ describe('admin/system page', () => {
render(AdminSystemPage, { props: {} });
await new Promise((r) => setTimeout(r, 100));
const banner = document.querySelector('[data-testid="thumbnails-status-done"]');
expect(banner).not.toBeNull();
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 () => {
@@ -261,8 +261,9 @@ describe('admin/system page', () => {
render(AdminSystemPage, { props: {} });
await new Promise((r) => setTimeout(r, 100));
expect(document.body.textContent).toContain('database error');
await vi.waitFor(() => {
expect(document.body.textContent).toContain('database error');
});
});
it('renders the running thumbnail status with progress count', async () => {
@@ -290,33 +291,40 @@ describe('admin/system page', () => {
render(AdminSystemPage, { props: {} });
await new Promise((r) => setTimeout(r, 100));
// Total 100, processed+skipped+failed = 36
expect(document.body.textContent).toMatch(/36|100/);
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 new Promise((r) => setTimeout(r, 50));
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();
(btns[0] as HTMLButtonElement).click();
await new Promise((r) => setTimeout(r, 50));
const calls = fetchSpy.mock.calls.map((c) => c[0].toString());
expect(calls.some((c) => c.includes('generate-thumbnails'))).toBe(true);
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 new Promise((r) => setTimeout(r, 50));
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();
(btns[0] as HTMLButtonElement).click();
await new Promise((r) => setTimeout(r, 50));
const calls = fetchSpy.mock.calls.map((c) => c[0].toString());
expect(calls.some((c) => c.includes('trigger-import'))).toBe(true);
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 () => {
@@ -344,8 +352,8 @@ describe('admin/system page', () => {
render(AdminSystemPage, { props: {} });
await new Promise((r) => setTimeout(r, 100));
// running state shown but no progress count when total=0
expect(document.body.textContent).toMatch(/läuft|wird|generier/i);
await vi.waitFor(() => {
expect(document.body.textContent).toMatch(/läuft|wird|generier/i);
});
});
});