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 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-18 14:03:28 +02:00
committed by marcel
parent fa14a11244
commit f0bdcf334b
2 changed files with 185 additions and 0 deletions

View File

@@ -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();
});
});
});