import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; /** * Utility function to create debounced functions * Used in GenericMediaListPage for search input debouncing */ export function createDebouncedFunction any>( fn: T, delayMs: number = 300 ) { let timeout: ReturnType | null = null; return (...args: Parameters) => { if (timeout) clearTimeout(timeout); timeout = setTimeout(() => { fn(...args); timeout = null; }, delayMs); }; } describe("Debounce Utility", () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); describe("Basic Debouncing", () => { it("should delay function execution", () => { const mockFn = vi.fn(); const debouncedFn = createDebouncedFunction(mockFn, 300); debouncedFn("test"); // Should not be called immediately expect(mockFn).not.toHaveBeenCalled(); // Advance time by 300ms vi.advanceTimersByTime(300); // Now it should be called expect(mockFn).toHaveBeenCalledWith("test"); expect(mockFn).toHaveBeenCalledTimes(1); }); it("should not call function if timer is cleared before delay", () => { const mockFn = vi.fn(); const debouncedFn = createDebouncedFunction(mockFn, 300); debouncedFn("test"); vi.advanceTimersByTime(150); // Call again before delay completes debouncedFn("updated"); // First timeout should be cleared vi.advanceTimersByTime(150); // Should still not have been called expect(mockFn).not.toHaveBeenCalled(); // Complete the second timeout vi.advanceTimersByTime(300); // Should be called once with latest value expect(mockFn).toHaveBeenCalledWith("updated"); expect(mockFn).toHaveBeenCalledTimes(1); }); it("should handle multiple rapid calls", () => { const mockFn = vi.fn(); const debouncedFn = createDebouncedFunction(mockFn, 300); // Rapid calls debouncedFn("a"); vi.advanceTimersByTime(100); debouncedFn("b"); vi.advanceTimersByTime(100); debouncedFn("c"); vi.advanceTimersByTime(100); // Should not be called yet expect(mockFn).not.toHaveBeenCalled(); // Complete the final timeout vi.advanceTimersByTime(300); // Should be called once with the last value expect(mockFn).toHaveBeenCalledWith("c"); expect(mockFn).toHaveBeenCalledTimes(1); }); it("should call multiple times if calls are spaced out", () => { const mockFn = vi.fn(); const debouncedFn = createDebouncedFunction(mockFn, 300); debouncedFn("first"); vi.advanceTimersByTime(300); // Should be called expect(mockFn).toHaveBeenCalledWith("first"); expect(mockFn).toHaveBeenCalledTimes(1); // Wait enough time and call again vi.advanceTimersByTime(200); debouncedFn("second"); vi.advanceTimersByTime(300); // Should be called again expect(mockFn).toHaveBeenCalledWith("second"); expect(mockFn).toHaveBeenCalledTimes(2); }); }); describe("Custom Delay", () => { it("should respect custom delay values", () => { const mockFn = vi.fn(); const debouncedFn = createDebouncedFunction(mockFn, 500); debouncedFn("test"); // 300ms shouldn't trigger vi.advanceTimersByTime(300); expect(mockFn).not.toHaveBeenCalled(); // But 500ms should vi.advanceTimersByTime(200); expect(mockFn).toHaveBeenCalledWith("test"); }); it("should handle zero delay", () => { const mockFn = vi.fn(); const debouncedFn = createDebouncedFunction(mockFn, 0); debouncedFn("test"); vi.advanceTimersByTime(0); expect(mockFn).toHaveBeenCalledWith("test"); }); }); describe("Search Use Case", () => { it("should debounce search queries correctly", () => { const mockSearch = vi.fn(); const debouncedSearch = createDebouncedFunction(mockSearch, 300); // User types "t" debouncedSearch("t"); expect(mockSearch).not.toHaveBeenCalled(); // User types "te" quickly vi.advanceTimersByTime(100); debouncedSearch("te"); expect(mockSearch).not.toHaveBeenCalled(); // User types "tes" vi.advanceTimersByTime(100); debouncedSearch("tes"); expect(mockSearch).not.toHaveBeenCalled(); // User types "test" vi.advanceTimersByTime(100); debouncedSearch("test"); expect(mockSearch).not.toHaveBeenCalled(); // Wait for debounce delay vi.advanceTimersByTime(300); // Should only call once with final value expect(mockSearch).toHaveBeenCalledWith("test"); expect(mockSearch).toHaveBeenCalledTimes(1); }); it("should cancel pending search if input clears quickly", () => { const mockSearch = vi.fn(); const debouncedSearch = createDebouncedFunction(mockSearch, 300); // User types "test" debouncedSearch("test"); vi.advanceTimersByTime(100); // User clears input debouncedSearch(""); vi.advanceTimersByTime(100); // User types again debouncedSearch("new"); vi.advanceTimersByTime(300); // Should only call with final value expect(mockSearch).toHaveBeenCalledWith("new"); expect(mockSearch).toHaveBeenCalledTimes(1); }); it("should work with async search functions", () => { const mockAsyncSearch = vi.fn().mockResolvedValue([]); const debouncedSearch = createDebouncedFunction(mockAsyncSearch, 300); debouncedSearch("query"); vi.advanceTimersByTime(300); expect(mockAsyncSearch).toHaveBeenCalledWith("query"); }); }); describe("Generic Parameter Handling", () => { it("should preserve function parameters", () => { const mockFn = vi.fn(); const debouncedFn = createDebouncedFunction(mockFn, 300); const obj = { id: "123", name: "test" }; debouncedFn("string", 42, obj); vi.advanceTimersByTime(300); expect(mockFn).toHaveBeenCalledWith("string", 42, obj); }); it("should handle functions with no parameters", () => { const mockFn = vi.fn(); const debouncedFn = createDebouncedFunction(mockFn, 300); debouncedFn(); vi.advanceTimersByTime(300); expect(mockFn).toHaveBeenCalledWith(); }); it("should handle complex object parameters", () => { const mockFn = vi.fn(); const debouncedFn = createDebouncedFunction(mockFn, 300); const options = { query: "test", filters: { type: "Audio", limit: 100 }, sort: { by: "SortName", order: "Ascending" }, }; debouncedFn(options); vi.advanceTimersByTime(300); expect(mockFn).toHaveBeenCalledWith(options); }); }); describe("Memory Management", () => { it("should clean up timeout after execution", () => { const mockFn = vi.fn(); const debouncedFn = createDebouncedFunction(mockFn, 300); debouncedFn("test"); vi.advanceTimersByTime(300); expect(mockFn).toHaveBeenCalled(); const callCount = mockFn.mock.calls.length; // Call again shortly after debouncedFn("test2"); vi.advanceTimersByTime(100); // Additional calls within delay shouldn't cause multiple executions debouncedFn("test3"); vi.advanceTimersByTime(300); // Should only have been called 2 times total expect(mockFn.mock.calls.length).toBe(2); }); it("should handle repeated debouncing without memory leaks", () => { const mockFn = vi.fn(); const debouncedFn = createDebouncedFunction(mockFn, 50); // Simulate 100 rapid calls for (let i = 0; i < 100; i++) { debouncedFn(`call${i}`); vi.advanceTimersByTime(10); } // Complete final timeout vi.advanceTimersByTime(50); // Should only be called once with the last value expect(mockFn).toHaveBeenCalledWith("call99"); expect(mockFn).toHaveBeenCalledTimes(1); }); }); });