diff --git a/frontend/src/routes/DropZone.svelte.test.ts b/frontend/src/routes/DropZone.svelte.test.ts index c4399c85..2499b3dd 100644 --- a/frontend/src/routes/DropZone.svelte.test.ts +++ b/frontend/src/routes/DropZone.svelte.test.ts @@ -47,16 +47,17 @@ describe('DropZone', () => { await expect.element(page.getByText(/Dateiformat nicht unterstützt/i)).toBeVisible(); }); - it('accepts a PDF file as a valid type', async () => { + it('accepts a PDF file as a valid type and renders no "invalid type" message', 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 }); + input.dispatchEvent(new Event('change', { bubbles: true })); - // Just verify the change event triggers without throwing - expect(() => input.dispatchEvent(new Event('change', { bubbles: true }))).not.toThrow(); + // The validation guard never raises an invalid-type error for application/pdf. + expect(document.body.textContent).not.toMatch(/Dateiformat nicht unterstützt/i); }); it('returns early when no files are selected', async () => { @@ -66,7 +67,6 @@ describe('DropZone', () => { 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); }); @@ -105,9 +105,10 @@ describe('DropZone', () => { 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/); + // isDragging=true switches the class to border-primary bg-accent-bg — wait for the next paint. + await vi.waitFor(() => { + expect(dropZone.className).toMatch(/bg-accent-bg/); + }); }); it('drops dragging style on dragleave', async () => { @@ -115,22 +116,27 @@ describe('DropZone', () => { const dropZone = document.querySelector('div[role="button"]') as HTMLElement; dropZone.dispatchEvent(new DragEvent('dragover', { bubbles: true })); - await new Promise((r) => setTimeout(r, 10)); + await vi.waitFor(() => { + expect(dropZone.className).toMatch(/bg-accent-bg/); + }); dropZone.dispatchEvent(new DragEvent('dragleave', { bubbles: true })); - await new Promise((r) => setTimeout(r, 30)); - expect(dropZone.className).not.toMatch(/^border-primary bg-accent-bg/); + await vi.waitFor(() => { + expect(dropZone.className).not.toMatch(/bg-accent-bg/); + }); }); - it('handles drop event without files (no-op)', async () => { + it('drop event with no files is a no-op (no error message)', 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(); + dropZone.dispatchEvent(dropEvent); + + expect(document.querySelectorAll('.text-red-600')).toHaveLength(0); }); - it('rejects multiple invalid files and lists them all', async () => { + it('rejects multiple invalid files and lists one error message per file', async () => { render(DropZone, { props: {} }); const input = document.querySelector('input[type="file"]') as HTMLInputElement; @@ -139,19 +145,21 @@ describe('DropZone', () => { 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); + await vi.waitFor(() => { + expect(document.querySelectorAll('.text-red-600')).toHaveLength(2); + }); }); - it('mixes valid and invalid files without throwing during upload', async () => { + it('mixed valid+invalid files raises an error only for the invalid one', 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(); + input.dispatchEvent(new Event('change', { bubbles: true })); + + await expect.element(page.getByText(/Dateiformat nicht unterstützt/i)).toBeVisible(); }); it('Enter handler ignores other keys', async () => { @@ -167,26 +175,35 @@ describe('DropZone', () => { it('responds to window-level dragenter only when dataTransfer.types includes "Files"', async () => { render(DropZone, { props: {} }); - // Non-files dragenter should not trigger windowDragging + const dropZone = document.querySelector('div[role="button"]') as HTMLElement; + + // Non-files dragenter should not trigger the windowDragging style. 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)); + expect(dropZone.className).not.toMatch(/bg-accent-bg/); - // Files dragenter should — we just confirm no error + // Files dragenter flips windowDragging=true → drop-zone gains the border-primary style. const evt2 = new DragEvent('dragenter', { bubbles: true }); Object.defineProperty(evt2, 'dataTransfer', { value: { types: ['Files'] } }); - expect(() => window.dispatchEvent(evt2)).not.toThrow(); + window.dispatchEvent(evt2); + await vi.waitFor(() => { + expect(dropZone.className).toMatch(/bg-accent-bg/); + }); - // Trailing window drop to clean up + // Trailing window drop to clean up. window.dispatchEvent(new DragEvent('drop', { bubbles: true })); }); - it('window-level dragleave decrements counter without going negative', async () => { + it('window-level dragleave without prior dragenter is safe (counter does not go 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(); + const dropZone = document.querySelector('div[role="button"]') as HTMLElement; + + // Two consecutive dragleaves on the window without dragenters should leave the drop-zone + // in its idle (non-highlighted) state. + window.dispatchEvent(new DragEvent('dragleave', { bubbles: true })); + window.dispatchEvent(new DragEvent('dragleave', { bubbles: true })); + expect(dropZone.className).not.toMatch(/bg-accent-bg/); }); });