From f0bdcf334b1588e1c2b07d845b08ded2f33d171d Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 18 Apr 2026 14:03:28 +0200 Subject: [PATCH] feat(frontend): add UploadZone component for PLACEHOLDER document file upload Presentational component with idle/uploading/error states, drag-and-drop, client-side MIME type + 50 MB size validation, accessible touch targets (44px), aria-live region, and indeterminate progress animation. Co-Authored-By: Claude Sonnet 4.6 --- .../lib/components/document/UploadZone.svelte | 100 ++++++++++++++++++ .../document/UploadZone.svelte.test.ts | 85 +++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 frontend/src/lib/components/document/UploadZone.svelte create mode 100644 frontend/src/lib/components/document/UploadZone.svelte.test.ts diff --git a/frontend/src/lib/components/document/UploadZone.svelte b/frontend/src/lib/components/document/UploadZone.svelte new file mode 100644 index 00000000..50aac4eb --- /dev/null +++ b/frontend/src/lib/components/document/UploadZone.svelte @@ -0,0 +1,100 @@ + + +
+ {#if isUploading} +
+
+
+
+

{filename}

+

Wird hochgeladen …

+ +
+ {:else} +
{ + e.preventDefault(); + isDragging = true; + }} + ondragleave={() => (isDragging = false)} + ondrop={handleDrop} + > +
+ ↑ +
+

{filename}

+

Noch keine Datei hochgeladen

+ {#if displayError} +

{displayError}

+ {/if} + +

oder Datei hier ablegen

+
+ {/if} +
diff --git a/frontend/src/lib/components/document/UploadZone.svelte.test.ts b/frontend/src/lib/components/document/UploadZone.svelte.test.ts new file mode 100644 index 00000000..a661a2a6 --- /dev/null +++ b/frontend/src/lib/components/document/UploadZone.svelte.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect, vi } from 'vitest'; +import { render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import UploadZone from './UploadZone.svelte'; + +describe('UploadZone', () => { + describe('idle state', () => { + it('shows the filename in the upload zone', async () => { + render(UploadZone, { + props: { filename: 'brief_1920.pdf', isUploading: false, isDragging: false, error: null } + }); + await expect.element(page.getByText('brief_1920.pdf')).toBeVisible(); + }); + + it('shows "Datei auswählen" button', async () => { + render(UploadZone, { + props: { filename: 'scan.pdf', isUploading: false, isDragging: false, error: null } + }); + await expect.element(page.getByText('Datei auswählen')).toBeVisible(); + }); + + it('does not show the uploading animation', async () => { + render(UploadZone, { + props: { filename: 'scan.pdf', isUploading: false, isDragging: false, error: null } + }); + expect(document.querySelector('[role="status"]')).toBeNull(); + }); + }); + + describe('uploading state', () => { + it('shows the uploading progress region', async () => { + render(UploadZone, { + props: { filename: 'scan.pdf', isUploading: true, isDragging: false, error: null } + }); + await expect.element(page.getByRole('status')).toBeVisible(); + }); + + it('shows Abbrechen button during upload', async () => { + render(UploadZone, { + props: { filename: 'scan.pdf', isUploading: true, isDragging: false, error: null } + }); + await expect.element(page.getByText('Abbrechen')).toBeVisible(); + }); + + it('calls onCancel when Abbrechen is clicked', async () => { + const onCancel = vi.fn(); + render(UploadZone, { + props: { filename: 'scan.pdf', isUploading: true, isDragging: false, error: null, onCancel } + }); + const btn = document.querySelector('button')!; + btn.dispatchEvent(new MouseEvent('click', { bubbles: true })); + expect(onCancel).toHaveBeenCalledOnce(); + }); + }); + + describe('error state', () => { + it('shows the error message', async () => { + render(UploadZone, { + props: { + filename: 'scan.pdf', + isUploading: false, + isDragging: false, + error: 'Dateityp nicht unterstützt' + } + }); + await expect.element(page.getByText('Dateityp nicht unterstützt')).toBeVisible(); + }); + }); + + describe('file selection', () => { + it('does not call onFile for an unsupported MIME type', async () => { + const onFile = vi.fn(); + render(UploadZone, { + props: { filename: 'scan.pdf', isUploading: false, isDragging: false, error: null, onFile } + }); + const input = document.querySelector('input[type="file"]') as HTMLInputElement; + const docxFile = new File(['x'], 'test.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + }); + Object.defineProperty(input, 'files', { value: [docxFile], writable: false }); + input.dispatchEvent(new Event('change', { bubbles: true })); + expect(onFile).not.toHaveBeenCalled(); + }); + }); +});