diff --git a/frontend/src/routes/DropZone.svelte.test.ts b/frontend/src/routes/DropZone.svelte.test.ts new file mode 100644 index 00000000..1394cdc5 --- /dev/null +++ b/frontend/src/routes/DropZone.svelte.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; + +vi.mock('$app/navigation', () => ({ + beforeNavigate: () => {}, + afterNavigate: () => {}, + goto: vi.fn(), + invalidate: vi.fn(), + invalidateAll: vi.fn(), + preloadCode: vi.fn(), + preloadData: vi.fn(), + pushState: vi.fn(), + replaceState: vi.fn(), + disableScrollHandling: vi.fn(), + onNavigate: () => () => {} +})); + +const { default: DropZone } = await import('./DropZone.svelte'); + +afterEach(cleanup); + +describe('DropZone', () => { + it('renders the drop hint and accepted types by default', async () => { + render(DropZone, { props: {} }); + + await expect.element(page.getByText(/einzeln oder mehrere/i)).toBeVisible(); + await expect.element(page.getByText('PDF, JPEG, PNG, TIFF')).toBeVisible(); + }); + + it('does not render the progress bar by default', async () => { + render(DropZone, { props: {} }); + + expect(document.querySelector('.bg-primary.h-full')).toBeNull(); + }); + + it('rejects files with unaccepted MIME types and shows an error message', async () => { + render(DropZone, { props: {} }); + + const input = document.querySelector('input[type="file"]') as HTMLInputElement; + const badFile = new File(['bad'], 'doc.docx', { + type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + }); + Object.defineProperty(input, 'files', { value: [badFile], writable: false }); + input.dispatchEvent(new Event('change', { bubbles: true })); + + await expect.element(page.getByText(/Dateiformat nicht unterstützt/i)).toBeVisible(); + }); + + it('accepts a PDF file as a valid type', async () => { + const onUploadComplete = vi.fn(); + render(DropZone, { props: { onUploadComplete } }); + + const input = document.querySelector('input[type="file"]') as HTMLInputElement; + const pdfFile = new File(['%PDF'], 'brief.pdf', { type: 'application/pdf' }); + Object.defineProperty(input, 'files', { value: [pdfFile], writable: false }); + + // Just verify the change event triggers without throwing + expect(() => input.dispatchEvent(new Event('change', { bubbles: true }))).not.toThrow(); + }); + + it('returns early when no files are selected', async () => { + render(DropZone, { props: {} }); + + const input = document.querySelector('input[type="file"]') as HTMLInputElement; + Object.defineProperty(input, 'files', { value: [], writable: false }); + input.dispatchEvent(new Event('change', { bubbles: true })); + + // No error message rendered + const errors = document.querySelectorAll('.text-red-600'); + expect(errors.length).toBe(0); + }); + + it('opens the file input when the drop zone is clicked', async () => { + render(DropZone, { props: {} }); + + const dropZone = document.querySelector('div[role="button"]') as HTMLElement; + const input = document.querySelector('input[type="file"]') as HTMLInputElement; + const clickSpy = vi.spyOn(input, 'click'); + dropZone.click(); + expect(clickSpy).toHaveBeenCalled(); + }); + + it('opens the file input when Enter is pressed on the drop zone', async () => { + render(DropZone, { props: {} }); + + const dropZone = document.querySelector('div[role="button"]') as HTMLElement; + const input = document.querySelector('input[type="file"]') as HTMLInputElement; + const clickSpy = vi.spyOn(input, 'click'); + dropZone.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); + expect(clickSpy).toHaveBeenCalled(); + }); + + it('exposes file input as multi-file with accept whitelist', async () => { + render(DropZone, { props: {} }); + + const input = document.querySelector('input[type="file"]') as HTMLInputElement; + expect(input.multiple).toBe(true); + expect(input.accept).toContain('.pdf'); + }); +});