diff --git a/frontend/src/routes/DropZone.svelte.test.ts b/frontend/src/routes/DropZone.svelte.test.ts index 1394cdc5..c4399c85 100644 --- a/frontend/src/routes/DropZone.svelte.test.ts +++ b/frontend/src/routes/DropZone.svelte.test.ts @@ -98,4 +98,95 @@ describe('DropZone', () => { 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(); + }); });