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'); }); it('applies the dragging style on dragover', async () => { render(DropZone, { props: {} }); const dropZone = document.querySelector('div[role="button"]') as HTMLElement; dropZone.dispatchEvent(new DragEvent('dragover', { bubbles: true })); // The dragging class is applied via reactive isDragging await new Promise((r) => setTimeout(r, 30)); expect(dropZone.className).toMatch(/border-primary|bg-accent-bg/); }); it('drops dragging style on dragleave', async () => { render(DropZone, { props: {} }); const dropZone = document.querySelector('div[role="button"]') as HTMLElement; dropZone.dispatchEvent(new DragEvent('dragover', { bubbles: true })); await new Promise((r) => setTimeout(r, 10)); dropZone.dispatchEvent(new DragEvent('dragleave', { bubbles: true })); await new Promise((r) => setTimeout(r, 30)); expect(dropZone.className).not.toMatch(/^border-primary bg-accent-bg/); }); it('handles drop event without files (no-op)', async () => { render(DropZone, { props: {} }); const dropZone = document.querySelector('div[role="button"]') as HTMLElement; const dropEvent = new DragEvent('drop', { bubbles: true }); Object.defineProperty(dropEvent, 'dataTransfer', { value: { files: [] }, writable: false }); expect(() => dropZone.dispatchEvent(dropEvent)).not.toThrow(); }); it('rejects multiple invalid files and lists them all', async () => { render(DropZone, { props: {} }); const input = document.querySelector('input[type="file"]') as HTMLInputElement; const f1 = new File(['x'], 'a.docx', { type: 'application/x-bad' }); const f2 = new File(['y'], 'b.txt', { type: 'text/plain' }); Object.defineProperty(input, 'files', { value: [f1, f2], writable: false }); input.dispatchEvent(new Event('change', { bubbles: true })); await new Promise((r) => setTimeout(r, 30)); const errors = document.querySelectorAll('.text-red-600'); expect(errors.length).toBe(2); }); it('mixes valid and invalid files without throwing during upload', async () => { render(DropZone, { props: {} }); const input = document.querySelector('input[type="file"]') as HTMLInputElement; const f1 = new File(['x'], 'a.docx', { type: 'application/x-bad' }); const f2 = new File(['%PDF'], 'b.pdf', { type: 'application/pdf' }); Object.defineProperty(input, 'files', { value: [f1, f2], writable: false }); expect(() => input.dispatchEvent(new Event('change', { bubbles: true }))).not.toThrow(); }); it('Enter handler ignores other keys', 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: 'a', bubbles: true })); expect(clickSpy).not.toHaveBeenCalled(); }); it('responds to window-level dragenter only when dataTransfer.types includes "Files"', async () => { render(DropZone, { props: {} }); // Non-files dragenter should not trigger windowDragging const evt1 = new DragEvent('dragenter', { bubbles: true }); Object.defineProperty(evt1, 'dataTransfer', { value: { types: ['text/html'] } }); window.dispatchEvent(evt1); await new Promise((r) => setTimeout(r, 10)); // Files dragenter should — we just confirm no error const evt2 = new DragEvent('dragenter', { bubbles: true }); Object.defineProperty(evt2, 'dataTransfer', { value: { types: ['Files'] } }); expect(() => window.dispatchEvent(evt2)).not.toThrow(); // Trailing window drop to clean up window.dispatchEvent(new DragEvent('drop', { bubbles: true })); }); it('window-level dragleave decrements counter without going negative', async () => { render(DropZone, { props: {} }); // Multiple dragleaves without dragenters should not throw expect(() => window.dispatchEvent(new DragEvent('dragleave', { bubbles: true }))).not.toThrow(); expect(() => window.dispatchEvent(new DragEvent('dragleave', { bubbles: true }))).not.toThrow(); }); });