From 448d1256db0e15211fcbee703dfb4a1e732ef112 Mon Sep 17 00:00:00 2001 From: Marcel Date: Thu, 14 May 2026 12:11:00 +0200 Subject: [PATCH] =?UTF-8?q?feat(admin/system):=20extract=20ImportStatusCar?= =?UTF-8?q?d=20=E2=80=94=20spinner,=20text-base=20count,=20statusCode=20i1?= =?UTF-8?q?8n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracts the mass-import block from +page.svelte into ImportStatusCard.svelte. Changes per the three UX fixes from issue #533: - RUNNING: animated spinner (animate-spin) + processed count at text-base; auto-poll at 2 s was already in place - DONE: processed count at text-base, label at text-xs uppercase tracking-widest - FAILED: maps statusCode (IMPORT_FAILED_NO_SPREADSHEET / IMPORT_FAILED_INTERNAL) to Paraglide messages — no raw German backend string rendered Adds vitest-browser tests covering spinner visibility, count display, and per-statusCode FAILED message selection. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/routes/admin/system/+page.svelte | 44 +--------- .../admin/system/ImportStatusCard.svelte | 80 +++++++++++++++++++ .../system/ImportStatusCard.svelte.test.ts | 71 ++++++++++++++++ 3 files changed, 154 insertions(+), 41 deletions(-) create mode 100644 frontend/src/routes/admin/system/ImportStatusCard.svelte create mode 100644 frontend/src/routes/admin/system/ImportStatusCard.svelte.test.ts diff --git a/frontend/src/routes/admin/system/+page.svelte b/frontend/src/routes/admin/system/+page.svelte index aa734abf..f7a61332 100644 --- a/frontend/src/routes/admin/system/+page.svelte +++ b/frontend/src/routes/admin/system/+page.svelte @@ -1,6 +1,7 @@ + +
+

{m.admin_system_import_heading()}

+

{m.admin_system_import_description()}

+ + {#if importStatus?.state === 'RUNNING'} +
+ +
+

{importStatus.processed}

+

+ {m.admin_system_import_status_running()} +

+
+
+ {:else if importStatus?.state === 'DONE'} +
+

{importStatus.processed}

+

+ {m.admin_system_import_status_done_label()} +

+

{m.admin_system_import_status_done()}

+
+ + {:else if importStatus?.state === 'FAILED'} +

+ {importStatus.statusCode === 'IMPORT_FAILED_NO_SPREADSHEET' + ? m.admin_system_import_failed_no_spreadsheet() + : m.admin_system_import_failed_internal()} +

+ + {:else} + {#if importStatus !== null} +

{m.admin_system_import_status_idle()}

+ {/if} + + {/if} +
diff --git a/frontend/src/routes/admin/system/ImportStatusCard.svelte.test.ts b/frontend/src/routes/admin/system/ImportStatusCard.svelte.test.ts new file mode 100644 index 00000000..1c98fe99 --- /dev/null +++ b/frontend/src/routes/admin/system/ImportStatusCard.svelte.test.ts @@ -0,0 +1,71 @@ +import { describe, it } from 'vitest'; +import { render } from 'vitest-browser-svelte'; +import { expect } from '@vitest/browser/context'; +import ImportStatusCard from './ImportStatusCard.svelte'; + +type ImportStatus = { + state: 'IDLE' | 'RUNNING' | 'DONE' | 'FAILED'; + statusCode: string; + message: string; + processed: number; + startedAt: string | null; +}; + +const makeStatus = (overrides: Partial = {}): ImportStatus => ({ + state: 'IDLE', + statusCode: 'IMPORT_IDLE', + message: '', + processed: 0, + startedAt: null, + ...overrides +}); + +describe('ImportStatusCard', () => { + it('shows spinner while state is RUNNING', async () => { + const { getByTestId } = render(ImportStatusCard, { + props: { + importStatus: makeStatus({ state: 'RUNNING', statusCode: 'IMPORT_RUNNING', processed: 3 }), + ontrigger: () => {} + } + }); + + await expect.element(getByTestId('spinner')).toBeVisible(); + }); + + it('shows processed count at text-base while RUNNING', async () => { + const { getByText } = render(ImportStatusCard, { + props: { + importStatus: makeStatus({ state: 'RUNNING', statusCode: 'IMPORT_RUNNING', processed: 7 }), + ontrigger: () => {} + } + }); + + await expect.element(getByText('7')).toBeVisible(); + }); + + it('shows processed count while DONE', async () => { + const { getByText } = render(ImportStatusCard, { + props: { + importStatus: makeStatus({ state: 'DONE', statusCode: 'IMPORT_DONE', processed: 42 }), + ontrigger: () => {} + } + }); + + await expect.element(getByText('42')).toBeVisible(); + }); + + it('shows no-spreadsheet message when statusCode is IMPORT_FAILED_NO_SPREADSHEET', async () => { + const { getByText } = render(ImportStatusCard, { + props: { + importStatus: makeStatus({ + state: 'FAILED', + statusCode: 'IMPORT_FAILED_NO_SPREADSHEET', + message: 'Keine Tabellendatei...' + }), + ontrigger: () => {} + } + }); + + await expect.element(getByText('No spreadsheet file found.')).toBeVisible(); + }); +});