many changes
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
/**
|
||||
* Duration formatting utility tests
|
||||
*
|
||||
* TRACES: UR-005 | DR-028
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/**
|
||||
* Input validation utility tests
|
||||
*
|
||||
* TRACES: UR-009, UR-025 | DR-015
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
Reference in New Issue
Block a user