298 lines
8.0 KiB
TypeScript
298 lines
8.0 KiB
TypeScript
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<T extends (...args: any[]) => any>(
|
|
fn: T,
|
|
delayMs: number = 300
|
|
) {
|
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
return (...args: Parameters<T>) => {
|
|
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);
|
|
});
|
|
});
|
|
});
|