feat(utils): add debounce utility with full test coverage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
69
frontend/src/lib/utils/debounce.spec.ts
Normal file
69
frontend/src/lib/utils/debounce.spec.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { debounce } from './debounce';
|
||||||
|
|
||||||
|
describe('debounce', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not fire before the delay has elapsed', () => {
|
||||||
|
const fn = vi.fn();
|
||||||
|
const debounced = debounce(fn, 200);
|
||||||
|
|
||||||
|
debounced();
|
||||||
|
vi.advanceTimersByTime(199);
|
||||||
|
|
||||||
|
expect(fn).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fires exactly once after the delay', () => {
|
||||||
|
const fn = vi.fn();
|
||||||
|
const debounced = debounce(fn, 200);
|
||||||
|
|
||||||
|
debounced();
|
||||||
|
vi.advanceTimersByTime(200);
|
||||||
|
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets the timer on each call — fires only once after inactivity', () => {
|
||||||
|
const fn = vi.fn();
|
||||||
|
const debounced = debounce(fn, 200);
|
||||||
|
|
||||||
|
debounced();
|
||||||
|
vi.advanceTimersByTime(100);
|
||||||
|
debounced();
|
||||||
|
vi.advanceTimersByTime(100);
|
||||||
|
debounced();
|
||||||
|
vi.advanceTimersByTime(200);
|
||||||
|
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('passes the latest arguments to the callback', () => {
|
||||||
|
const fn = vi.fn();
|
||||||
|
const debounced = debounce(fn, 200);
|
||||||
|
|
||||||
|
debounced('first');
|
||||||
|
debounced('second');
|
||||||
|
vi.advanceTimersByTime(200);
|
||||||
|
|
||||||
|
expect(fn).toHaveBeenCalledWith('second');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can fire again after the first invocation settles', () => {
|
||||||
|
const fn = vi.fn();
|
||||||
|
const debounced = debounce(fn, 200);
|
||||||
|
|
||||||
|
debounced();
|
||||||
|
vi.advanceTimersByTime(200);
|
||||||
|
debounced();
|
||||||
|
vi.advanceTimersByTime(200);
|
||||||
|
|
||||||
|
expect(fn).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
12
frontend/src/lib/utils/debounce.ts
Normal file
12
frontend/src/lib/utils/debounce.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* Returns a debounced version of fn that delays invocation until after
|
||||||
|
* `delay` ms have elapsed since the last call.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function debounce<T extends (...args: any[]) => void>(fn: T, delay: number): T {
|
||||||
|
let timer: ReturnType<typeof setTimeout>;
|
||||||
|
return ((...args: Parameters<T>) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(() => fn(...args), delay);
|
||||||
|
}) as T;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user