From 79f995af10d12242f36b343df858c79585def17e Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 9 May 2026 21:19:37 +0200 Subject: [PATCH] test: cover enrich/done and documents/bulk-edit page branches enrich/done: heading, body, both CTA links. documents/bulk-edit: empty-store onMount redirect to /documents, loading spinner during in-flight fetch, error banner on backend error code, error banner on fetch rejection. Mocks fetch via vi.spyOn so the async branches are exercised without a real backend. Refs #496. Co-Authored-By: Claude Sonnet 4.6 --- .../documents/bulk-edit/page.svelte.test.ts | 80 +++++++++++++++++++ .../routes/enrich/done/page.svelte.test.ts | 36 +++++++++ 2 files changed, 116 insertions(+) create mode 100644 frontend/src/routes/documents/bulk-edit/page.svelte.test.ts create mode 100644 frontend/src/routes/enrich/done/page.svelte.test.ts diff --git a/frontend/src/routes/documents/bulk-edit/page.svelte.test.ts b/frontend/src/routes/documents/bulk-edit/page.svelte.test.ts new file mode 100644 index 00000000..c991df0d --- /dev/null +++ b/frontend/src/routes/documents/bulk-edit/page.svelte.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; + +const gotoSpy = vi.fn(); +vi.mock('$app/navigation', () => ({ + beforeNavigate: () => {}, + afterNavigate: () => {}, + goto: gotoSpy, + invalidate: vi.fn(), + invalidateAll: vi.fn(), + preloadCode: vi.fn(), + preloadData: vi.fn(), + pushState: vi.fn(), + replaceState: vi.fn(), + disableScrollHandling: vi.fn(), + onNavigate: () => () => {} +})); + +const { bulkSelectionStore } = await import('$lib/document/bulkSelection.svelte'); +const { default: BulkEditPage } = await import('./+page.svelte'); + +afterEach(() => { + cleanup(); + bulkSelectionStore.clear(); + gotoSpy.mockClear(); +}); + +describe('documents/bulk-edit page', () => { + it('redirects to /documents when no documents are selected', async () => { + render(BulkEditPage, { props: {} }); + + // onMount runs immediately — give it a tick + await new Promise((r) => setTimeout(r, 50)); + + expect(gotoSpy).toHaveBeenCalledWith('/documents'); + }); + + it('shows the loading spinner while fetching batch metadata', async () => { + bulkSelectionStore.toggle('d1'); + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementation(() => new Promise(() => {})); + try { + render(BulkEditPage, { props: {} }); + + await expect.element(page.getByRole('status')).toBeVisible(); + await expect.element(page.getByText('Dokumente werden geladen…')).toBeVisible(); + } finally { + fetchSpy.mockRestore(); + } + }); + + it('shows the error banner when the fetch fails with a backend error code', async () => { + bulkSelectionStore.toggle('d1'); + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue( + new Response(JSON.stringify({ code: 'INTERNAL_ERROR' }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }) + ); + try { + render(BulkEditPage, { props: {} }); + + await expect.element(page.getByRole('alert')).toBeVisible(); + } finally { + fetchSpy.mockRestore(); + } + }); + + it('shows the error banner when fetch throws an unexpected error', async () => { + bulkSelectionStore.toggle('d1'); + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('Network down')); + try { + render(BulkEditPage, { props: {} }); + + await expect.element(page.getByRole('alert')).toBeVisible(); + } finally { + fetchSpy.mockRestore(); + } + }); +}); diff --git a/frontend/src/routes/enrich/done/page.svelte.test.ts b/frontend/src/routes/enrich/done/page.svelte.test.ts new file mode 100644 index 00000000..3f0c1305 --- /dev/null +++ b/frontend/src/routes/enrich/done/page.svelte.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import EnrichDonePage from './+page.svelte'; + +afterEach(cleanup); + +describe('enrich/done page', () => { + it('renders the success heading', async () => { + render(EnrichDonePage, { props: {} }); + + await expect.element(page.getByRole('heading', { name: /alles erledigt/i })).toBeVisible(); + }); + + it('renders the body message', async () => { + render(EnrichDonePage, { props: {} }); + + await expect.element(page.getByText('Alle Dokumente wurden bearbeitet.')).toBeVisible(); + }); + + it('links the primary CTA to the home page', async () => { + render(EnrichDonePage, { props: {} }); + + await expect + .element(page.getByRole('link', { name: /zurück zur übersicht/i })) + .toHaveAttribute('href', '/'); + }); + + it('links the secondary CTA back to /enrich', async () => { + render(EnrichDonePage, { props: {} }); + + await expect + .element(page.getByRole('link', { name: /zurück zur liste/i })) + .toHaveAttribute('href', '/enrich'); + }); +});