Files
readest/apps/readest-app/src/__tests__/utils/debounce.test.ts
T
Huang Xin 0e516f6e56 chore(test): add unit tests and enforce dash-case naming for test files (#3715)
Add ~290 new unit tests covering utils (lru, diff, css, a11y, walk,
usage, txt-worker), services (EdgeTTSClient, transformers), and
suppress noisy console output across all test files. Rename 27
camelCase test files to dash-case for consistency.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 09:14:00 +02:00

191 lines
6.0 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { debounce } from '@/utils/debounce';
describe('debounce', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
// -----------------------------------------------------------------------
// Basic debounce behavior (emitLast: true, default)
// -----------------------------------------------------------------------
describe('emitLast: true (default)', () => {
it('does not call the function immediately', () => {
const fn = vi.fn();
const debounced = debounce(fn, 100);
debounced();
expect(fn).not.toHaveBeenCalled();
});
it('calls the function after the delay', () => {
const fn = vi.fn();
const debounced = debounce(fn, 100);
debounced('a');
vi.advanceTimersByTime(100);
expect(fn).toHaveBeenCalledOnce();
expect(fn).toHaveBeenCalledWith('a');
});
it('resets the timer on subsequent calls and uses the last args', () => {
const fn = vi.fn();
const debounced = debounce(fn, 100);
debounced('first');
vi.advanceTimersByTime(50);
debounced('second');
vi.advanceTimersByTime(50);
// Only 50ms have passed since second call, should not fire yet
expect(fn).not.toHaveBeenCalled();
vi.advanceTimersByTime(50);
expect(fn).toHaveBeenCalledOnce();
expect(fn).toHaveBeenCalledWith('second');
});
it('calls function only once when invoked many times within delay', () => {
const fn = vi.fn();
const debounced = debounce(fn, 100);
for (let i = 0; i < 10; i++) {
debounced(i);
}
vi.advanceTimersByTime(100);
expect(fn).toHaveBeenCalledOnce();
expect(fn).toHaveBeenCalledWith(9);
});
});
// -----------------------------------------------------------------------
// emitLast: false
// -----------------------------------------------------------------------
describe('emitLast: false', () => {
it('calls function with the args from each call (not the last)', () => {
const fn = vi.fn();
const debounced = debounce(fn, 100, { emitLast: false });
debounced('first');
vi.advanceTimersByTime(50);
debounced('second');
vi.advanceTimersByTime(100);
// With emitLast: false, each call sets a new timeout with its own args
expect(fn).toHaveBeenCalledOnce();
expect(fn).toHaveBeenCalledWith('second');
});
it('fires the original args, not the latest, after delay', () => {
const fn = vi.fn();
const debounced = debounce(fn, 100, { emitLast: false });
debounced('only');
vi.advanceTimersByTime(100);
expect(fn).toHaveBeenCalledWith('only');
});
});
// -----------------------------------------------------------------------
// flush
// -----------------------------------------------------------------------
describe('flush', () => {
it('immediately invokes the pending call with latest args', () => {
const fn = vi.fn();
const debounced = debounce(fn, 100);
debounced('a');
debounced('b');
debounced.flush();
expect(fn).toHaveBeenCalledOnce();
expect(fn).toHaveBeenCalledWith('b');
});
it('clears the pending timer after flushing', () => {
const fn = vi.fn();
const debounced = debounce(fn, 100);
debounced('a');
debounced.flush();
vi.advanceTimersByTime(200);
// Should not fire again after flush
expect(fn).toHaveBeenCalledOnce();
});
it('does nothing when there is no pending call', () => {
const fn = vi.fn();
const debounced = debounce(fn, 100);
debounced.flush();
expect(fn).not.toHaveBeenCalled();
});
});
// -----------------------------------------------------------------------
// cancel
// -----------------------------------------------------------------------
describe('cancel', () => {
it('prevents the pending call from firing', () => {
const fn = vi.fn();
const debounced = debounce(fn, 100);
debounced('a');
debounced.cancel();
vi.advanceTimersByTime(200);
expect(fn).not.toHaveBeenCalled();
});
it('clears lastArgs so a subsequent flush does nothing', () => {
const fn = vi.fn();
const debounced = debounce(fn, 100);
debounced('a');
debounced.cancel();
debounced.flush();
expect(fn).not.toHaveBeenCalled();
});
it('does nothing when there is no pending call', () => {
const fn = vi.fn();
const debounced = debounce(fn, 100);
debounced.cancel(); // should not throw
expect(fn).not.toHaveBeenCalled();
});
});
// -----------------------------------------------------------------------
// Edge cases
// -----------------------------------------------------------------------
describe('edge cases', () => {
it('works with zero delay', () => {
const fn = vi.fn();
const debounced = debounce(fn, 0);
debounced('instant');
vi.advanceTimersByTime(0);
expect(fn).toHaveBeenCalledOnce();
expect(fn).toHaveBeenCalledWith('instant');
});
it('works with multiple arguments', () => {
const fn = vi.fn();
const debounced = debounce(fn, 50);
debounced('a', 'b', 'c');
vi.advanceTimersByTime(50);
expect(fn).toHaveBeenCalledWith('a', 'b', 'c');
});
it('can be called again after the debounced function fires', () => {
const fn = vi.fn();
const debounced = debounce(fn, 100);
debounced('first');
vi.advanceTimersByTime(100);
expect(fn).toHaveBeenCalledOnce();
debounced('second');
vi.advanceTimersByTime(100);
expect(fn).toHaveBeenCalledTimes(2);
expect(fn).toHaveBeenLastCalledWith('second');
});
it('handles async functions as the debounced callback', () => {
const fn = vi.fn(async () => {
/* noop */
});
const debounced = debounce(fn, 50);
debounced();
vi.advanceTimersByTime(50);
expect(fn).toHaveBeenCalledOnce();
});
});
});