Skip to content

Commit 4777bef

Browse files
committed
test: update UTs for new methods
1 parent 2cb7186 commit 4777bef

17 files changed

Lines changed: 2047 additions & 175 deletions

tests/config.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ describe("Config", () => {
1010
Env: {
1111
get: jest.fn((key: string, fallback: string) => fallback),
1212
getBoolean: jest.fn(() => false),
13+
getNumber: jest.fn((key: string, fallback: number) => fallback),
1314
},
1415
}));
1516

@@ -39,6 +40,7 @@ describe("Config", () => {
3940
getBoolean: jest.fn((key: string, fallback: boolean) =>
4041
key === "LOGGER_ISO_TIMESTAMP" ? true : fallback,
4142
),
43+
getNumber: jest.fn((key: string, fallback: number) => fallback),
4244
},
4345
}));
4446

@@ -60,6 +62,7 @@ describe("Config", () => {
6062
return key in values ? values[key] : fallback;
6163
}),
6264
getBoolean: jest.fn(() => false),
65+
getNumber: jest.fn((key: string, fallback: number) => fallback),
6366
},
6467
}));
6568

@@ -75,6 +78,7 @@ describe("Config", () => {
7578
Env: {
7679
get: jest.fn(() => "nonsense"),
7780
getBoolean: jest.fn(() => false),
81+
getNumber: jest.fn((key: string, fallback: number) => fallback),
7882
},
7983
}));
8084

tests/utils/array.utils.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ import {
99
difference,
1010
intersect,
1111
mergeSort,
12+
zip,
13+
partition,
14+
range,
15+
take,
16+
takeWhile,
17+
compact,
18+
countBy,
1219
} from "../../src/utils/array.utils";
1320

1421
jest.mock("../../src/utils/obj.utils", () => ({
@@ -197,4 +204,90 @@ describe("ArrayUtils", () => {
197204
expect(() => mergeSort(null, "x")).toThrow("Expected array");
198205
});
199206
});
207+
208+
describe("zip", () => {
209+
it("zips arrays together", () => {
210+
expect(zip([1, 2], ["a", "b"] as any)).toEqual([
211+
[1, "a"],
212+
[2, "b"],
213+
]);
214+
expect(zip([1, 2, 3], ["a"] as any)).toEqual([[1, "a"]]);
215+
expect(zip()).toEqual([]);
216+
expect(() => zip([1], null as any)).toThrow(
217+
"All arguments must be arrays",
218+
);
219+
});
220+
});
221+
222+
describe("partition", () => {
223+
it("splits array by predicate", () => {
224+
expect(partition([1, 2, 3, 4], (x) => x % 2 === 0)).toEqual([
225+
[2, 4],
226+
[1, 3],
227+
]);
228+
});
229+
it("returns [[], []] for non-array", () => {
230+
// @ts-expect-error
231+
expect(partition(null, () => true)).toEqual([[], []]);
232+
});
233+
});
234+
235+
describe("range", () => {
236+
it("generates a range of numbers", () => {
237+
expect(range(0, 5)).toEqual([0, 1, 2, 3, 4]);
238+
expect(range(5, 0, -2)).toEqual([5, 3, 1]);
239+
expect(range(0, 0)).toEqual([]);
240+
});
241+
it("throws for invalid args", () => {
242+
expect(() => range(NaN, 1)).toThrow();
243+
expect(() => range(0, 1, 0)).toThrow();
244+
});
245+
});
246+
247+
describe("take", () => {
248+
it("returns first n elements", () => {
249+
expect(take([1, 2, 3], 2)).toEqual([1, 2]);
250+
expect(take([1, 2, 3], 0)).toEqual([]);
251+
expect(take([1, 2, 3])).toEqual([1]);
252+
expect(take([], 2)).toEqual([]);
253+
// @ts-expect-error
254+
expect(take(null, 2)).toEqual([]);
255+
});
256+
});
257+
258+
describe("takeWhile", () => {
259+
it("takes elements while predicate is true", () => {
260+
expect(takeWhile([1, 2, 3, 0, 4], (x) => x > 0)).toEqual([1, 2, 3]);
261+
expect(takeWhile([1, 2, 3], () => false)).toEqual([]);
262+
// @ts-expect-error
263+
expect(takeWhile(null, () => true)).toEqual([]);
264+
});
265+
});
266+
267+
describe("compact", () => {
268+
it("removes falsy values", () => {
269+
expect(compact([0, 1, false, 2, "", 3, null, undefined])).toEqual([
270+
1, 2, 3,
271+
]);
272+
expect(compact([])).toEqual([]);
273+
// @ts-expect-error
274+
expect(compact(null)).toEqual([]);
275+
});
276+
});
277+
278+
describe("countBy", () => {
279+
it("counts elements by key function", () => {
280+
expect(countBy(["a", "b", "a"], (x) => x)).toEqual({ a: 2, b: 1 });
281+
expect(
282+
countBy([1, 2, 3, 2], (x) => (x % 2 === 0 ? "even" : "odd")),
283+
).toEqual({
284+
odd: 2,
285+
even: 2,
286+
});
287+
});
288+
it("returns {} for non-array", () => {
289+
// @ts-expect-error
290+
expect(countBy(null, () => "x")).toEqual({});
291+
});
292+
});
200293
});

tests/utils/async.utils.test.ts

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// asyncUtils.spec.ts
21
import {
32
sleep,
43
debounce,
@@ -10,13 +9,18 @@ import {
109
settleAll,
1110
createTaskQueue,
1211
runInSeries,
12+
memoizeAsync,
13+
abortable,
14+
createDeferred,
15+
waterfall,
16+
rateLimit,
1317
} from "../../src/utils/async.utils";
1418

1519
describe("sleep", () => {
1620
it("delays for at least the given ms", async () => {
1721
const start = Date.now();
1822
await sleep(30);
19-
expect(Date.now() - start).toBeGreaterThanOrEqual(30);
23+
expect(Date.now() - start).toBeGreaterThanOrEqual(30 - 5); // allow some margin for test environment
2024
});
2125
});
2226

@@ -261,3 +265,105 @@ describe("runInSeries", () => {
261265
expect(result).toEqual([1, 2, 3]);
262266
});
263267
});
268+
269+
describe("memoizeAsync", () => {
270+
it("caches results for identical arguments", async () => {
271+
const fn = jest.fn(async (x: number) => x * 2);
272+
const memo = memoizeAsync(fn);
273+
expect(await memo(2)).toBe(4);
274+
expect(await memo(2)).toBe(4);
275+
expect(fn).toHaveBeenCalledTimes(1);
276+
});
277+
278+
it("respects TTL option", async () => {
279+
jest.useFakeTimers();
280+
const fn = jest.fn(async (x: number) => x + 1);
281+
const memo = memoizeAsync(fn, { ttl: 100 });
282+
await expect(memo(1)).resolves.toBe(2);
283+
jest.advanceTimersByTime(99);
284+
await expect(memo(1)).resolves.toBe(2);
285+
jest.advanceTimersByTime(2);
286+
await expect(memo(1)).resolves.toBe(2);
287+
expect(fn).toHaveBeenCalledTimes(2);
288+
jest.useRealTimers();
289+
});
290+
291+
it("supports custom keyFn", async () => {
292+
const fn = jest.fn(async (a: number, b: number) => a + b);
293+
const memo = memoizeAsync(fn, { keyFn: ([a, b]) => `${a}-${b}` });
294+
await expect(memo(1, 2)).resolves.toBe(3);
295+
await expect(memo(1, 2)).resolves.toBe(3);
296+
expect(fn).toHaveBeenCalledTimes(1);
297+
});
298+
});
299+
300+
describe("abortable", () => {
301+
it("resolves if not aborted", async () => {
302+
const ctrl = new AbortController();
303+
await expect(abortable(Promise.resolve("ok"), ctrl.signal)).resolves.toBe(
304+
"ok",
305+
);
306+
});
307+
308+
it("rejects if aborted before promise resolves", async () => {
309+
const ctrl = new AbortController();
310+
const p = abortable(
311+
new Promise((r) => setTimeout(() => r("late"), 50)),
312+
ctrl.signal,
313+
"aborted",
314+
);
315+
ctrl.abort();
316+
await expect(p).rejects.toBe("aborted");
317+
});
318+
319+
it("rejects immediately if already aborted", async () => {
320+
const ctrl = new AbortController();
321+
ctrl.abort();
322+
await expect(
323+
abortable(Promise.resolve("x"), ctrl.signal, "gone"),
324+
).rejects.toBe("gone");
325+
});
326+
});
327+
328+
describe("createDeferred", () => {
329+
it("resolves externally", async () => {
330+
const [p, resolve] = createDeferred<number>();
331+
setTimeout(() => resolve(42), 10);
332+
await expect(p).resolves.toBe(42);
333+
});
334+
335+
it("rejects externally", async () => {
336+
const [p, , reject] = createDeferred<number>();
337+
setTimeout(() => reject("fail"), 10);
338+
await expect(p).rejects.toBe("fail");
339+
});
340+
});
341+
342+
describe("waterfall", () => {
343+
it("chains async functions in order", async () => {
344+
const fns = [
345+
async (x: number) => x + 1,
346+
async (x: number) => x * 2,
347+
async (x: number) => `Result: ${x}`,
348+
];
349+
const wf = waterfall<string>(fns);
350+
await expect(wf(3)).resolves.toBe("Result: 8");
351+
});
352+
});
353+
354+
describe("rateLimit", () => {
355+
it("limits calls per interval", async () => {
356+
jest.useFakeTimers();
357+
const fn = jest.fn(async (x: number) => x);
358+
const limited = rateLimit(fn, 2, 100);
359+
const p1 = limited(1);
360+
const p2 = limited(2);
361+
const p3 = limited(3);
362+
jest.advanceTimersByTime(101);
363+
const p4 = limited(4);
364+
jest.advanceTimersByTime(101);
365+
await expect(Promise.all([p1, p2, p3, p4])).resolves.toEqual([1, 2, 3, 4]);
366+
expect(fn).toHaveBeenCalledTimes(4);
367+
jest.useRealTimers();
368+
});
369+
});

tests/utils/cache.utils.test.ts

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ describe("CacheUtils", () => {
66
afterEach(() => jest.useRealTimers());
77

88
it("set() + get() returns value before TTL expires", () => {
9-
const cache = new TTLCache<string, number>(1000);
9+
const cache = new TTLCache<string, number>({ ttlMs: 1000 });
1010
cache.set("foo", 42);
1111
expect(cache.get("foo")).toBe(42);
1212
});
1313

1414
it("get() returns undefined after TTL expires and deletes key", () => {
15-
const cache = new TTLCache<string, number>(1000);
15+
const cache = new TTLCache<string, number>({ ttlMs: 1000 });
1616
cache.set("foo", 1);
1717
jest.advanceTimersByTime(1001);
1818
expect(cache.get("foo")).toBeUndefined();
@@ -33,7 +33,7 @@ describe("CacheUtils", () => {
3333
});
3434

3535
it("has() only true for fresh entry, false after expiry", () => {
36-
const cache = new TTLCache<string, number>(1000);
36+
const cache = new TTLCache<string, number>({ ttlMs: 1000 });
3737
expect(cache.has("x")).toBe(false);
3838
cache.set("x", 500);
3939
expect(cache.has("x")).toBe(true);
@@ -60,7 +60,7 @@ describe("CacheUtils", () => {
6060
});
6161

6262
it("size() returns current Map size (includes expired until cleanup/get)", () => {
63-
const cache = new TTLCache<string, number>(100);
63+
const cache = new TTLCache<string, number>({ ttlMs: 100 });
6464
cache.set("a", 1);
6565
cache.set("b", 2);
6666
expect(cache.size()).toBe(2);
@@ -124,4 +124,71 @@ describe("CacheUtils", () => {
124124
cache.set(99, "great");
125125
expect(Array.from(cache.keys()).sort()).toEqual([77, 99]);
126126
});
127+
128+
it("getOrCompute returns cached value or computes and caches if missing", async () => {
129+
const cache = new TTLCache<string, number>({ ttlMs: 100 });
130+
const producer = jest.fn(async () => 123);
131+
// Not present, should call producer
132+
await expect(cache.getOrCompute("a", producer)).resolves.toBe(123);
133+
expect(producer).toHaveBeenCalledTimes(1);
134+
// Present, should not call producer again
135+
await expect(cache.getOrCompute("a", producer)).resolves.toBe(123);
136+
expect(producer).toHaveBeenCalledTimes(1);
137+
// After expiry, should call producer again
138+
jest.advanceTimersByTime(101);
139+
await expect(cache.getOrCompute("a", producer)).resolves.toBe(123);
140+
expect(producer).toHaveBeenCalledTimes(2);
141+
});
142+
143+
it("setMany and getMany work as expected", () => {
144+
const cache = new TTLCache<string, number>();
145+
cache.setMany([
146+
["a", 1],
147+
["b", 2],
148+
["c", 3],
149+
]);
150+
expect(cache.getMany(["a", "b", "c", "d"])).toEqual([1, 2, 3, undefined]);
151+
});
152+
153+
it("refresh extends the TTL of an entry", () => {
154+
const cache = new TTLCache<string, number>({ ttlMs: 100 });
155+
cache.set("foo", 1);
156+
jest.advanceTimersByTime(90);
157+
expect(cache.refresh("foo")).toBe(true);
158+
jest.advanceTimersByTime(90);
159+
// Should still be valid after refresh
160+
expect(cache.get("foo")).toBe(1);
161+
jest.advanceTimersByTime(101);
162+
// Now expired
163+
expect(cache.get("foo")).toBeUndefined();
164+
});
165+
166+
it("refresh returns false for missing or expired keys", () => {
167+
const cache = new TTLCache<string, number>({ ttlMs: 50 });
168+
expect(cache.refresh("nope")).toBe(false);
169+
cache.set("x", 1);
170+
jest.advanceTimersByTime(51);
171+
expect(cache.refresh("x")).toBe(false);
172+
});
173+
174+
it("stats returns correct cache statistics", () => {
175+
const cache = new TTLCache<string, number>({ ttlMs: 100, maxSize: 10 });
176+
cache.set("a", 1);
177+
cache.set("b", 2);
178+
jest.advanceTimersByTime(101);
179+
cache.set("c", 3);
180+
const stats = cache.stats();
181+
expect(stats.size).toBe(3);
182+
expect(stats.validEntries).toBe(1);
183+
expect(stats.expiredEntries).toBe(2);
184+
expect(stats.maxSize).toBe(10);
185+
});
186+
187+
it("destroy stops auto-cleanup interval", () => {
188+
const cache = new TTLCache<string, number>({ autoCleanupMs: 100 });
189+
const spy = jest.spyOn(global, "clearInterval");
190+
cache.destroy();
191+
expect(spy).toHaveBeenCalled();
192+
spy.mockRestore();
193+
});
127194
});

0 commit comments

Comments
 (0)