feat(utils): add debounce utility with full test coverage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-06 13:23:14 +02:00
parent d1ad4d834c
commit 2c0748d60e
2 changed files with 81 additions and 0 deletions

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

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