Skip to content

Commit 2b30c5e

Browse files
committed
test: add UTs to improve test coverage
1 parent 1416f4e commit 2b30c5e

4 files changed

Lines changed: 1145 additions & 33 deletions

File tree

tests/utils/async.utils.test.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ describe('debounce', () => {
7777
expect(fn).toHaveBeenCalledWith('z');
7878
expect(fn).toHaveBeenCalledTimes(1);
7979
});
80+
81+
it('handles flush when no pending args', () => {
82+
const fn = jest.fn();
83+
const deb = debounce(fn, 100);
84+
deb.flush(); // Should not call fn when no pending args
85+
expect(fn).not.toHaveBeenCalled();
86+
});
8087
});
8188

8289
describe('throttle', () => {
@@ -109,6 +116,32 @@ describe('throttle', () => {
109116
expect(fn).toHaveBeenCalledWith('b');
110117
expect(fn).toHaveBeenCalledTimes(1);
111118
});
119+
120+
it('handles timer cleanup when remaining <= 0', () => {
121+
const fn = jest.fn();
122+
const thr = throttle(fn, 100, { leading: true, trailing: true });
123+
124+
// First call should execute immediately
125+
thr('first');
126+
expect(fn).toHaveBeenCalledWith('first');
127+
expect(fn).toHaveBeenCalledTimes(1);
128+
129+
// Wait for throttle period to expire
130+
jest.advanceTimersByTime(100);
131+
132+
// This call should execute immediately since enough time has passed
133+
thr('second');
134+
expect(fn).toHaveBeenCalledWith('second');
135+
expect(fn).toHaveBeenCalledTimes(2);
136+
});
137+
138+
it('does nothing when both leading and trailing are false', () => {
139+
const fn = jest.fn();
140+
const thr = throttle(fn, 100, { leading: false, trailing: false });
141+
thr('test');
142+
jest.advanceTimersByTime(200);
143+
expect(fn).not.toHaveBeenCalled();
144+
});
112145
});
113146

114147
describe('retry', () => {
@@ -164,6 +197,11 @@ describe('withTimeout', () => {
164197
const slow = new Promise<void>(() => {});
165198
await expect(withTimeout(slow, 10, 'timeout!')).rejects.toThrow('timeout!');
166199
});
200+
201+
it('rejects if promise rejects before timeout', async () => {
202+
const failing = Promise.reject(new Error('original error'));
203+
await expect(withTimeout(failing, 100)).rejects.toThrow('original error');
204+
});
167205
});
168206

169207
describe('runInBatches', () => {
@@ -255,6 +293,16 @@ describe('createTaskQueue', () => {
255293
expect(q.length).toBe(1);
256294
expect(q['isPaused']).toBe(true);
257295
});
296+
297+
it('handles task rejection properly', async () => {
298+
const q = createTaskQueue(1);
299+
const error = new Error('task failed');
300+
await expect(
301+
q(async () => {
302+
throw error;
303+
})
304+
).rejects.toBe(error);
305+
});
258306
});
259307

260308
describe('runInSeries', () => {
@@ -294,6 +342,29 @@ describe('memoizeAsync', () => {
294342
await expect(memo(1, 2)).resolves.toBe(3);
295343
expect(fn).toHaveBeenCalledTimes(1);
296344
});
345+
346+
it('cleans up expired cache entries properly', async () => {
347+
jest.useFakeTimers();
348+
let callCount = 0;
349+
const fn = jest.fn(async (x: number) => {
350+
callCount++;
351+
return x + callCount;
352+
});
353+
const memo = memoizeAsync(fn, { ttl: 100 });
354+
355+
// First call
356+
await expect(memo(1)).resolves.toBe(2); // 1 + 1
357+
expect(fn).toHaveBeenCalledTimes(1);
358+
359+
// Expire the cache
360+
jest.advanceTimersByTime(101);
361+
362+
// Second call should not use cache
363+
await expect(memo(1)).resolves.toBe(3); // 1 + 2
364+
expect(fn).toHaveBeenCalledTimes(2);
365+
366+
jest.useRealTimers();
367+
});
297368
});
298369

299370
describe('abortable', () => {
@@ -353,6 +424,25 @@ describe('rateLimit', () => {
353424
expect(fn).toHaveBeenCalledTimes(4);
354425
jest.useRealTimers();
355426
});
427+
428+
it('handles edge case where delay calculation results in very small values', async () => {
429+
jest.useFakeTimers();
430+
const fn = jest.fn(async (x: number) => x);
431+
const limited = rateLimit(fn, 1, 100);
432+
433+
// Make first call
434+
const p1 = limited(1);
435+
436+
// Advance time to make delay calculation very small
437+
jest.advanceTimersByTime(99);
438+
const p2 = limited(2);
439+
440+
jest.advanceTimersByTime(2); // Should use Math.max(1, delay)
441+
442+
await expect(Promise.all([p1, p2])).resolves.toEqual([1, 2]);
443+
expect(fn).toHaveBeenCalledTimes(2);
444+
jest.useRealTimers();
445+
});
356446
});
357447

358448
describe('circuitBreaker', () => {
@@ -471,4 +561,20 @@ describe('runWithConcurrency', () => {
471561
ctrl.abort();
472562
await expect(runWithConcurrency([async () => 1], { signal: ctrl.signal })).rejects.toThrow('Aborted');
473563
});
564+
565+
it('handles error in onProgress callback gracefully', async () => {
566+
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
567+
const tasks = [1, 2].map(n => async () => n);
568+
569+
await expect(
570+
runWithConcurrency(tasks, {
571+
concurrency: 2,
572+
onProgress: () => {
573+
throw new Error('Progress callback error');
574+
}
575+
})
576+
).resolves.toEqual([1, 2]);
577+
578+
consoleSpy.mockRestore();
579+
});
474580
});

0 commit comments

Comments
 (0)