Add tests for DebouncedCache

This commit is contained in:
MareStare 2025-03-04 03:41:33 +00:00
parent 69eff89886
commit 97ca6d8846

View file

@ -0,0 +1,150 @@
import { DebouncedCache } from '../debounced-cache';
describe('DebouncedCache', () => {
beforeAll(() => {
vi.useFakeTimers();
});
const consoleSpy = {
debug: vi.spyOn(console, 'debug'),
error: vi.spyOn(console, 'error'),
};
afterEach(() => {
consoleSpy.debug.mockClear();
consoleSpy.error.mockClear();
});
it('should call the function after a debounce threshold and cache the result', async () => {
const { producer, cache } = createTestCache();
const consumer = vi.fn();
cache.schedule({ a: 1, b: 2 }, consumer);
await vi.runAllTimersAsync();
expect(producer).toHaveBeenCalledWith({ a: 1, b: 2 });
expect(consumer).toHaveBeenCalledWith(3);
cache.schedule({ a: 1, b: 2 }, consumer);
await vi.runAllTimersAsync();
expect(producer).toHaveBeenCalledTimes(1);
expect(consumer).toHaveBeenCalledTimes(2);
expect(consoleSpy.debug).not.toHaveBeenCalled();
expect(consoleSpy.error).not.toHaveBeenCalled();
});
describe('should abort the last scheduled call when a new one is scheduled', () => {
test('scheduling before the debounce threshold is reached', async () => {
const { producer, cache } = createTestCache();
const consumer1 = vi.fn();
const consumer2 = vi.fn();
cache.schedule({ a: 1, b: 2 }, consumer1);
cache.schedule({ a: 1, b: 2 }, consumer2);
await vi.runAllTimersAsync();
expect(consumer1).not.toHaveBeenCalled();
expect(consumer2).toHaveBeenCalledWith(3);
expect(producer).toHaveBeenCalledOnce();
// No logs should be emitted because the `setTimeout` call itself should have been aborted.
expect(consoleSpy.debug.mock.calls).toMatchInlineSnapshot(`[]`);
expect(consoleSpy.error.mock.calls).toMatchInlineSnapshot(`[]`);
});
test('scheduling after the debounce threshold is reached', async () => {
const threshold = 300;
const { producer, cache } = createTestCache(threshold);
const consumer1 = vi.fn();
const consumer2 = vi.fn();
cache.schedule({ a: 1, b: 2 }, consumer1);
vi.advanceTimersByTime(threshold);
cache.schedule({ a: 1, b: 2 }, consumer2);
await vi.runAllTimersAsync();
expect(consumer1).not.toHaveBeenCalled();
expect(consumer2).toHaveBeenCalledWith(3);
expect(producer).toHaveBeenCalledOnce();
expect(consoleSpy.debug.mock.calls).toMatchInlineSnapshot(`
[
[
"A call was aborted after the debounce threshold was reached",
DOMException {},
],
]
`);
expect(consoleSpy.error.mock.calls).toMatchInlineSnapshot(`[]`);
});
});
describe('should handle errors by logging them', () => {
test('error in producer', async () => {
const producer = vi.fn(() => Promise.reject(new Error('producer error')));
const cache = new DebouncedCache(producer);
const consumer = vi.fn();
cache.schedule(undefined, consumer);
await vi.runAllTimersAsync();
expect(consumer).not.toHaveBeenCalled();
expect(consoleSpy.debug).not.toHaveBeenCalled();
expect(consoleSpy.error.mock.calls).toMatchInlineSnapshot(`
[
[
"An error occurred while calling 'spy'.",
[Error: producer error],
],
]
`);
});
test('error in consumer', async () => {
const { producer, cache } = createTestCache();
const consumer = vi.fn(() => {
throw new Error('consumer error');
});
cache.schedule({ a: 1, b: 2 }, consumer);
await vi.runAllTimersAsync();
expect(producer).toHaveBeenCalledOnce();
expect(consoleSpy.debug).not.toHaveBeenCalled();
expect(consoleSpy.error.mock.calls).toMatchInlineSnapshot(`
[
[
"An error occurred while processing the result of 'producerImpl'.",
[Error: consumer error],
],
]
`);
});
});
});
function createTestCache(thresholdMs?: number) {
const producer = vi.fn(producerImpl);
const cache = new DebouncedCache(producer, { thresholdMs });
return { producer, cache };
}
interface ProducerParams {
a: number;
b: number;
}
async function producerImpl(params: ProducerParams): Promise<number> {
return params.a + params.b;
}