Files
jellytau/src/lib/utils/debounce.test.ts
T
dtourolle e3797f32ca
Traceability Validation / Check Requirement Traces (push) Failing after 1m18s
🏗️ Build and Test JellyTau / Build APK and Run Tests (push) Has been cancelled
many changes
2026-02-14 00:09:47 +01:00

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);
});
});
});