import { renderHook, act } from "@testing-library/react"; import assert from "node:assert/strict"; import { describe, it, afterEach, mock, } from "node:test"; import { useFilesUploader } from "./useFilesUploader"; import type { IFilesUploaderItem, TDOMDragEvent, TInputChangeEvent, } from "../config/types/index"; describe("useFilesUploader", () => { type TMockFn = ReturnType; type TTestDragEvent = TDOMDragEvent & { preventDefault: TMockFn; stopPropagation: TMockFn; dataTransfer: { files: File[]; items: File[]; clearData: TMockFn; }; }; type TTestInputChangeEvent = TInputChangeEvent & { target: { files: File[]; value: string; }; }; // Helper to create a mock File (used only where actual File is expected) const createFile = (name: string, size: number = 1024) => { return new File([ "a".repeat(size) ], name, { type: "text/plain" }); }; // Helper to create a drag event with mock functions const createDragEvent = (files: File[]): TTestDragEvent => { return { preventDefault: mock.fn(), stopPropagation: mock.fn(), dataTransfer: { files, items: files, clearData: mock.fn(), }, } as unknown as TTestDragEvent; }; // Helper to create an input change event with mock target const createInputChangeEvent = (files: File[]): TTestInputChangeEvent => { return { target: { files: files, value: "", }, } as unknown as TTestInputChangeEvent; }; // Restore all mocks (especially console.error spies) after each test afterEach(() => { mock.restoreAll(); }); describe("initialization", () => { it("initializes with default values", () => { const { result } = renderHook(() => useFilesUploader()); assert.strictEqual(result.current.isLoading, false); assert.strictEqual(result.current.isDragging, false); assert.strictEqual(result.current.isError, false); assert.strictEqual(result.current.filesMap.size, 0); }); it("initializes with initial files", () => { const initialFiles: IFilesUploaderItem[] = [ { name: "file1.pdf", size: 100 }, { name: "file2.jpg", size: 200 }, ]; const { result } = renderHook(() => useFilesUploader(initialFiles)); assert.strictEqual(result.current.filesMap.size, 2); assert.ok(result.current.filesMap.get("file1.pdf")); assert.ok(result.current.filesMap.get("file2.jpg")); }); }); describe("drag and drop handlers", () => { it("handles drag start", () => { const { result } = renderHook(() => useFilesUploader()); const event = createDragEvent([]); act(() => { result.current.handleDragStart(event); }); assert.strictEqual(event.dataTransfer.clearData.mock.calls.length, 1); }); it("handles drag enter and sets dragging state", () => { const { result } = renderHook(() => useFilesUploader()); const file = createFile("test.pdf"); const event = createDragEvent([ file ]); act(() => { result.current.handleDragEnter(event); }); assert.strictEqual(result.current.isDragging, true); assert.strictEqual(event.preventDefault.mock.calls.length, 1); assert.strictEqual(event.stopPropagation.mock.calls.length, 1); }); it("does not set dragging when disabled", () => { const { result } = renderHook(() => useFilesUploader([], true)); const file = createFile("test.pdf"); const event = createDragEvent([ file ]); act(() => { result.current.handleDragEnter(event); }); assert.strictEqual(result.current.isDragging, false); }); it("handles drag leave and set isDragging to false", () => { const { result } = renderHook(() => useFilesUploader()); const file = createFile("test.pdf"); const event = createDragEvent([ file ]); act(() => { result.current.handleDragEnter(event); }); assert.strictEqual(result.current.isDragging, true); act(() => { result.current.handleDragLeave(event); }); assert.strictEqual(result.current.isDragging, false); assert.strictEqual(event.preventDefault.mock.calls.length, 2); assert.strictEqual(event.stopPropagation.mock.calls.length, 2); }); it("handles multiple drag enters and leaves correctly", () => { const { result } = renderHook(() => useFilesUploader()); const file = createFile("test.pdf"); const event = createDragEvent([ file ]); act(() => { result.current.handleDragEnter(event); result.current.handleDragEnter(event); }); assert.strictEqual(result.current.isDragging, true); act(() => { result.current.handleDragLeave(event); }); assert.strictEqual(result.current.isDragging, true); act(() => { result.current.handleDragLeave(event); }); assert.strictEqual(result.current.isDragging, false); }); it("handles drag over", () => { const { result } = renderHook(() => useFilesUploader()); const event = createDragEvent([]); act(() => { result.current.handleDragOver(event); }); assert.strictEqual(event.preventDefault.mock.calls.length, 1); assert.strictEqual(event.stopPropagation.mock.calls.length, 1); }); it("handles drop with valid files", async () => { const { result } = renderHook(() => useFilesUploader([], false, [ "pdf" ], 10, 5) ); const file = createFile("test.pdf"); const event = createDragEvent([ file ]); act(() => { result.current.handleDragEnter(event); }); assert.strictEqual(result.current.isDragging, true); await act(async () => { result.current.handleDrop(event); }); assert.strictEqual(result.current.isDragging, false); assert.strictEqual(result.current.filesMap.size, 1); assert.strictEqual(result.current.filesMap.get("test.pdf"), file); }); it("does not handle drop when disabled", () => { const { result } = renderHook(() => useFilesUploader([], true)); const file = createFile("test.pdf"); const event = createDragEvent([ file ]); const initialMapSize = result.current.filesMap.size; act(() => { result.current.handleDrop(event); }); assert.strictEqual(result.current.filesMap.size, initialMapSize); }); it("sets error when dropping too many files", () => { const { result } = renderHook(() => useFilesUploader([], false, [ "pdf" ], 10, 2) ); const files = [ createFile("file1.pdf"), createFile("file2.pdf"), createFile("file3.pdf"), ]; const event = createDragEvent(files); act(() => { result.current.handleDrop(event); }); assert.strictEqual(result.current.isError, true); assert.strictEqual(result.current.filesMap.size, 0); }); }); describe("file input change handler", () => { it("handles file input change with valid files", async () => { const { result } = renderHook(() => useFilesUploader([], false, [ "pdf" ], 10, 5) ); const file = createFile("test.pdf"); const event = createInputChangeEvent([ file ]); await act(async () => { result.current.handleChangeFiles(event); }); assert.strictEqual(result.current.filesMap.size, 1); }); it("sets error when selecting too many files", () => { const { result } = renderHook(() => useFilesUploader([], false, [ "pdf" ], 10, 1) ); const files = [ createFile("file1.pdf"), createFile("file2.pdf") ]; const event = createInputChangeEvent(files); act(() => { result.current.handleChangeFiles(event); }); assert.strictEqual(result.current.isError, true); assert.strictEqual(result.current.filesMap.size, 0); }); it("handles empty file list", async () => { const { result } = renderHook(() => useFilesUploader()); const event = createInputChangeEvent([]); await act(async () => { result.current.handleChangeFiles(event); }); assert.strictEqual(result.current.filesMap.size, 0); assert.strictEqual(result.current.isError, false); }); }); describe("file validation", () => { it("rejects files with invalid types", () => { const { result } = renderHook(() => useFilesUploader([], false, [ "pdf", "jpg" ], 10, 5) ); const file = createFile("test.exe"); const event = createDragEvent([ file ]); act(() => { result.current.handleDrop(event); }); assert.strictEqual(result.current.isError, true); }); it("accepts files with valid types", async () => { const { result } = renderHook(() => useFilesUploader([], false, [ "pdf" ], 10, 5) ); const file = createFile("test.pdf"); const event = createDragEvent([ file ]); await act(async () => { result.current.handleDrop(event); }); assert.strictEqual(result.current.isError, false); }); }); describe("maxFiles and maxSize handling", () => { it("replaces file when maxFiles is 1", async () => { const { result } = renderHook(() => useFilesUploader([], false, [ "pdf" ], 10, 1) ); const file1 = createFile("file1.pdf"); const file2 = createFile("file2.pdf"); await act(async () => { result.current.handleDrop(createDragEvent([ file1 ])); }); assert.strictEqual(result.current.filesMap.size, 1); await act(async () => { result.current.handleDrop(createDragEvent([ file2 ])); }); assert.strictEqual(result.current.filesMap.size, 1); assert.ok(result.current.filesMap.get("file2.pdf")); assert.strictEqual(result.current.filesMap.get("file1.pdf"), undefined); }); it("sets error when exceeding maxSize", async () => { const { result } = renderHook(() => useFilesUploader([], false, [ "pdf" ], 1, 1) ); const file = createFile("test.pdf", 2 * 1024 * 1024); await act(async () => { await result.current.handleDrop(createDragEvent([ file ])); }); assert.strictEqual(result.current.filesMap.size, 0); assert.strictEqual(result.current.isError, true); }); it("rejects multiple files when total size exceeds maxSize by default", async () => { const { result } = renderHook(() => useFilesUploader([], false, [ "pdf" ], 1, 5) ); const file1 = createFile("file1.pdf", 700 * 1024); const file2 = createFile("file2.pdf", 800 * 1024); await act(async () => { result.current.handleDrop(createDragEvent([ file1, file2 ])); }); assert.strictEqual(result.current.isError, true); assert.strictEqual(result.current.filesMap.size, 0); }); it("rejects sequential file additions when total size exceeds maxSize by default", async () => { const { result } = renderHook(() => useFilesUploader([], false, [ "pdf" ], 1, 5) ); const file1 = createFile("file1.pdf", 600 * 1024); const file2 = createFile("file2.pdf", 650 * 1024); const file3 = createFile("file3.pdf", 700 * 1024); await act(async () => { result.current.handleDrop(createDragEvent([ file1 ])); }); assert.strictEqual(result.current.isError, false); assert.strictEqual(result.current.filesMap.size, 1); await act(async () => { result.current.handleDrop(createDragEvent([ file2 ])); }); assert.strictEqual(result.current.isError, true); assert.strictEqual(result.current.filesMap.size, 1); await act(async () => { result.current.handleDrop(createDragEvent([ file3 ])); }); assert.strictEqual(result.current.isError, true); assert.strictEqual(result.current.filesMap.size, 1); assert.ok(result.current.filesMap.get("file1.pdf")); assert.strictEqual(result.current.filesMap.get("file2.pdf"), undefined); assert.strictEqual(result.current.filesMap.get("file3.pdf"), undefined); }); it("allows multiple files when maxSizeType is every", async () => { const { result } = renderHook(() => useFilesUploader([], false, [ "pdf" ], 1, 5, null, null, null, null, "every") ); const file1 = createFile("file1.pdf", 700 * 1024); const file2 = createFile("file2.pdf", 800 * 1024); await act(async () => { result.current.handleDrop(createDragEvent([ file1, file2 ])); }); assert.strictEqual(result.current.isError, false); assert.strictEqual(result.current.filesMap.size, 2); assert.ok(result.current.filesMap.get("file1.pdf")); assert.ok(result.current.filesMap.get("file2.pdf")); }); it("rejects a batch that would exceed maxFiles with existing files", async () => { const initial = createFile("initial.pdf"); const { result } = renderHook(() => useFilesUploader([ initial ], false, [ "pdf" ], 10, 2) ); await act(async () => { result.current.handleDrop(createDragEvent([ createFile("second.pdf"), createFile("third.pdf") ])); }); assert.strictEqual(result.current.filesMap.size, 1); assert.ok(result.current.filesMap.has("initial.pdf")); assert.strictEqual(result.current.isError, true); }); }); describe("file deletion", () => { it("deletes file from map", async () => { const file = createFile("test.pdf", 1024); const { result } = renderHook(() => useFilesUploader([ file ])); assert.strictEqual(result.current.filesMap.size, 1); await act(async () => { await result.current.handleFileItemDeleteClick(file); }); assert.strictEqual(result.current.filesMap.size, 0); }); it("deletes an empty file", async () => { const file = createFile("empty.pdf", 0); const { result } = renderHook(() => useFilesUploader([ file ])); await act(async () => { await result.current.handleFileItemDeleteClick(file); }); assert.strictEqual(result.current.filesMap.size, 0); }); it("calls custom onFileDelete callback", async () => { const file = createFile("test.pdf", 1024); let called = false; const onFileDelete = async () => { called = true; }; const { result } = renderHook(() => useFilesUploader( [ file ], false, [], Infinity, Infinity, null, null, null, onFileDelete ) ); await act(async () => { await result.current.handleFileItemDeleteClick(file); }); assert.strictEqual(called, true); }); it("sets loading state during async delete", async () => { const file = createFile("test.pdf", 1024); let resolveDelete: () => void; const onFileDelete = async () => { await new Promise((resolve) => { resolveDelete = resolve; }); }; const { result } = renderHook(() => useFilesUploader( [ file ], false, [], Infinity, Infinity, null, null, null, onFileDelete ) ); act(() => { result.current.handleFileItemDeleteClick(file); }); assert.strictEqual(result.current.isLoading, true); await act(async () => { resolveDelete(); }); assert.strictEqual(result.current.isLoading, false); }); it("handles delete error", async () => { const file = createFile("test.pdf", 1024); const onFileDelete = async () => { throw new Error("Delete failed"); }; const consoleErrorSpy = mock.method(console, "error", () => {}); const { result } = renderHook(() => useFilesUploader( [ file ], false, [], Infinity, Infinity, null, null, null, onFileDelete ) ); await act(async () => { await result.current.handleFileItemDeleteClick(file); }); assert.strictEqual(result.current.isError, true); assert.strictEqual(consoleErrorSpy.mock.calls.length, 1); }); it("rejects when file has no name or size", async () => { const invalidFile = { name: "", size: 0 } as File; const consoleErrorSpy = mock.method(console, "error", () => {}); const { result } = renderHook(() => useFilesUploader()); try { await result.current.handleFileItemDeleteClick(invalidFile); assert.fail("Expected promise to reject"); } catch (err) { assert.strictEqual(err, undefined); } assert.strictEqual(consoleErrorSpy.mock.calls.length, 1); }); }); describe("custom onFilesChange callback", () => { it("calls custom onFilesChange callback", async () => { const onFilesChange = mock.fn(); const { result } = renderHook(() => useFilesUploader([], false, [ "pdf" ], 10, 5, null, null, onFilesChange) ); const file = createFile("test.pdf"); await act(async () => { result.current.handleDrop(createDragEvent([ file ])); }); assert.strictEqual(onFilesChange.mock.calls.length, 1); }); it("sets loading state during async onFilesChange", async () => { const file = createFile("test.pdf"); let resolveChange: () => void; const onFilesChange = async () => { await new Promise((resolve) => { resolveChange = resolve; }); }; const { result } = renderHook(() => useFilesUploader([], false, [ "pdf" ], 10, 5, null, null, onFilesChange) ); act(() => { result.current.handleDrop(createDragEvent([ file ])); }); assert.strictEqual(result.current.isLoading, true); await act(async () => { resolveChange(); }); assert.strictEqual(result.current.isLoading, false); }); it("awaits a non-async callback that returns a promise", async () => { let resolveChange: () => void; const onFilesChange = () => new Promise((resolve) => { resolveChange = resolve; }); const { result } = renderHook(() => useFilesUploader([], false, [ "pdf" ], 10, 5, null, null, onFilesChange) ); act(() => result.current.handleDrop(createDragEvent([ createFile("test.pdf") ]))); assert.strictEqual(result.current.isLoading, true); await act(async () => resolveChange()); assert.strictEqual(result.current.isLoading, false); }); it("sets error when async onFilesChange fails", async () => { const onFilesChange = async () => { throw new Error("Delete failed"); }; const consoleErrorSpy = mock.method(console, "error", () => {}); const { result } = renderHook(() => useFilesUploader([], false, [ "pdf" ], 10, 5, null, null, onFilesChange) ); const file = createFile("test.pdf"); await act(async () => { result.current.handleDrop(createDragEvent([ file ])); }); assert.strictEqual(result.current.isError, true); assert.strictEqual(consoleErrorSpy.mock.calls.length, 1); }); }); });