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) <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-20 22:42:23 +02:00
parent 30ea1f0dcf
commit 97e8e4fc74

View File

@@ -4,7 +4,10 @@ import { page } from 'vitest/browser';
import DropZone from './DropZone.svelte'; 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(() => { afterEach(() => {
cleanup(); cleanup();
@@ -62,8 +65,11 @@ describe('DropZone onUploadComplete', () => {
input.files = dt.files; input.files = dt.files;
input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true }));
// Wait a tick to let the microtask flush // invalidateAll is the last async step of the upload handler — once it
await new Promise((r) => setTimeout(r, 50)); // has been called, the callback decision has already been made.
await vi.waitFor(() => {
expect(invalidateAllMock).toHaveBeenCalled();
});
expect(onUploadComplete).not.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 file = new File(['%PDF-1.4'], 'x.pdf', { type: 'application/pdf' });
const dt = new DataTransfer(); const dt = new DataTransfer();
dt.items.add(file); dt.items.add(file);
// Should not throw when the optional callback is absent.
input.files = dt.files; input.files = dt.files;
// Should not throw
input.dispatchEvent(new Event('change', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true }));
await new Promise((r) => setTimeout(r, 50));
await expect.element(page.getByText(/1 Dokument/)).toBeInTheDocument(); await expect.element(page.getByText(/1 Dokument/)).toBeInTheDocument();
}); });
}); });