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>
90 lines
3.3 KiB
TypeScript
90 lines
3.3 KiB
TypeScript
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
import { cleanup, render } from 'vitest-browser-svelte';
|
|
import { page } from 'vitest/browser';
|
|
|
|
import DropZone from './DropZone.svelte';
|
|
|
|
// 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();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
function stubXhrWith(responseBody: string) {
|
|
class FakeXhr {
|
|
upload = { addEventListener: vi.fn() };
|
|
status = 200;
|
|
responseText = responseBody;
|
|
private loadHandler: (() => void) | null = null;
|
|
open = vi.fn();
|
|
addEventListener = vi.fn((event: string, handler: () => void) => {
|
|
if (event === 'load') this.loadHandler = handler;
|
|
});
|
|
send = vi.fn(() => {
|
|
queueMicrotask(() => this.loadHandler?.());
|
|
});
|
|
}
|
|
vi.stubGlobal('XMLHttpRequest', FakeXhr);
|
|
}
|
|
|
|
describe('DropZone onUploadComplete', () => {
|
|
it('invokes callback with created.length after a successful upload', async () => {
|
|
stubXhrWith(JSON.stringify({ created: [{ id: 'd1' }, { id: 'd2' }], updated: [], errors: [] }));
|
|
|
|
const onUploadComplete = vi.fn();
|
|
render(DropZone, { onUploadComplete });
|
|
|
|
const input = document.querySelector('input[type="file"]') as HTMLInputElement | null;
|
|
expect(input).not.toBeNull();
|
|
const file = new File(['%PDF-1.4'], 'test.pdf', { type: 'application/pdf' });
|
|
const dataTransfer = new DataTransfer();
|
|
dataTransfer.items.add(file);
|
|
input!.files = dataTransfer.files;
|
|
input!.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
|
await vi.waitFor(() => {
|
|
expect(onUploadComplete).toHaveBeenCalledTimes(1);
|
|
});
|
|
expect(onUploadComplete).toHaveBeenCalledWith(2);
|
|
});
|
|
|
|
it('does not invoke callback when no files were created', async () => {
|
|
stubXhrWith(JSON.stringify({ created: [], updated: [], errors: [] }));
|
|
|
|
const onUploadComplete = vi.fn();
|
|
render(DropZone, { onUploadComplete });
|
|
|
|
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
const file = new File(['%PDF-1.4'], 'dupe.pdf', { type: 'application/pdf' });
|
|
const dt = new DataTransfer();
|
|
dt.items.add(file);
|
|
input.files = dt.files;
|
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
|
|
// 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();
|
|
});
|
|
|
|
it('works when the onUploadComplete prop is not supplied', async () => {
|
|
stubXhrWith(JSON.stringify({ created: [{ id: 'x' }], updated: [], errors: [] }));
|
|
|
|
render(DropZone, {});
|
|
const input = document.querySelector('input[type="file"]') as HTMLInputElement;
|
|
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;
|
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
await expect.element(page.getByText(/1 Dokument/)).toBeInTheDocument();
|
|
});
|
|
});
|