From 97e8e4fc7412eb73e49411185abdf7bb6b527a7c Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 20 Apr 2026 22:42:23 +0200 Subject: [PATCH] test(dropzone): replace setTimeout flake with vi.waitFor + hoisted mock The "no-callback" and "no-prop" tests no longer rely on an arbitrary 50ms sleep. Test 2 awaits the mocked invalidateAll call (the last async step of the upload handler) before asserting the callback was not invoked. Test 3 lets vitest-browser-svelte's own expect.element poll until the success message appears. Addresses Sara's and Felix's review concern about flake-prone timing. Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/routes/DropZone.svelte.spec.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/src/routes/DropZone.svelte.spec.ts b/frontend/src/routes/DropZone.svelte.spec.ts index 514f1b74..440c2f0c 100644 --- a/frontend/src/routes/DropZone.svelte.spec.ts +++ b/frontend/src/routes/DropZone.svelte.spec.ts @@ -4,7 +4,10 @@ import { page } from 'vitest/browser'; import DropZone from './DropZone.svelte'; -vi.mock('$app/navigation', () => ({ invalidateAll: vi.fn(async () => {}) })); +// vi.hoisted lets the mock fn reference survive vi.mock's hoisting so tests +// can assert on it from below while the factory remains self-contained. +const { invalidateAllMock } = vi.hoisted(() => ({ invalidateAllMock: vi.fn(async () => {}) })); +vi.mock('$app/navigation', () => ({ invalidateAll: invalidateAllMock })); afterEach(() => { cleanup(); @@ -62,8 +65,11 @@ describe('DropZone onUploadComplete', () => { input.files = dt.files; input.dispatchEvent(new Event('change', { bubbles: true })); - // Wait a tick to let the microtask flush - await new Promise((r) => setTimeout(r, 50)); + // invalidateAll is the last async step of the upload handler — once it + // has been called, the callback decision has already been made. + await vi.waitFor(() => { + expect(invalidateAllMock).toHaveBeenCalled(); + }); expect(onUploadComplete).not.toHaveBeenCalled(); }); @@ -75,10 +81,9 @@ describe('DropZone onUploadComplete', () => { const file = new File(['%PDF-1.4'], 'x.pdf', { type: 'application/pdf' }); const dt = new DataTransfer(); dt.items.add(file); + // Should not throw when the optional callback is absent. input.files = dt.files; - // Should not throw input.dispatchEvent(new Event('change', { bubbles: true })); - await new Promise((r) => setTimeout(r, 50)); await expect.element(page.getByText(/1 Dokument/)).toBeInTheDocument(); }); });