Skip to content

Commit eb1201f

Browse files
github-actions[bot]Copilotmarcelofukumoto
authored
[Test Improver] test: add unit tests for promise.js and queue.js (#17590)
* test: add unit tests for promise.js and queue.js Add 34 unit tests covering: - Queue: enqueue/dequeue FIFO ordering, peek, getLength, isEmpty, clear - allHash: key-mapped Promise.all, empty hash, rejection propagation - allHashSettled: fulfilled/rejected status mapping, never rejects - eachLimit: concurrency limit enforcement, index ordering, empty input, single-item limit, rejection handling, index argument passing - deferred: promise/resolve/reject object creation, resolve/reject behavior - setPromiseResult: property assignment on resolve, warning on reject Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Reviewed and improved tests --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Marcelo Fukumoto <marcelo.fukumoto@suse.com>
1 parent 2dcde67 commit eb1201f

1 file changed

Lines changed: 346 additions & 0 deletions

File tree

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
import Queue from '@shell/utils/queue';
2+
import {
3+
allHash,
4+
allHashSettled,
5+
deferred,
6+
eachLimit,
7+
setPromiseResult,
8+
} from '@shell/utils/promise';
9+
10+
describe('queue', () => {
11+
describe('getLength', () => {
12+
it.each([
13+
{
14+
desc: '0 for an empty queue', enqueueCount: 0, dequeueCount: 0, expected: 0
15+
},
16+
{
17+
desc: '1 after one enqueue', enqueueCount: 1, dequeueCount: 0, expected: 1
18+
},
19+
{
20+
desc: 'decrements after dequeue', enqueueCount: 2, dequeueCount: 1, expected: 1
21+
},
22+
])('returns $desc', ({ enqueueCount, dequeueCount, expected }) => {
23+
const q = new Queue();
24+
25+
for (let i = 0; i < enqueueCount; i++) {
26+
q.enqueue(`item-${ i }`);
27+
}
28+
for (let i = 0; i < dequeueCount; i++) {
29+
q.dequeue();
30+
}
31+
32+
expect(q.getLength()).toStrictEqual(expected);
33+
});
34+
});
35+
36+
describe('isEmpty', () => {
37+
it.each([
38+
{
39+
desc: 'true when queue is empty', enqueueCount: 0, dequeueCount: 0, expected: true
40+
},
41+
{
42+
desc: 'false after enqueueing an item', enqueueCount: 1, dequeueCount: 0, expected: false
43+
},
44+
{
45+
desc: 'true after all items are dequeued', enqueueCount: 1, dequeueCount: 1, expected: true
46+
},
47+
])('returns $desc', ({ enqueueCount, dequeueCount, expected }) => {
48+
const q = new Queue();
49+
50+
for (let i = 0; i < enqueueCount; i++) {
51+
q.enqueue('x');
52+
}
53+
for (let i = 0; i < dequeueCount; i++) {
54+
q.dequeue();
55+
}
56+
57+
expect(q.isEmpty()).toStrictEqual(expected);
58+
});
59+
});
60+
61+
describe('enqueue / dequeue', () => {
62+
it('dequeues items in FIFO order', () => {
63+
const q = new Queue();
64+
65+
q.enqueue('first');
66+
q.enqueue('second');
67+
q.enqueue('third');
68+
69+
expect(q.dequeue()).toStrictEqual('first');
70+
expect(q.dequeue()).toStrictEqual('second');
71+
expect(q.dequeue()).toStrictEqual('third');
72+
});
73+
74+
it('returns undefined when dequeuing from an empty queue', () => {
75+
const q = new Queue();
76+
77+
expect(q.dequeue()).toBeUndefined();
78+
});
79+
80+
it('works with objects as items', () => {
81+
const q = new Queue();
82+
const item = { key: 'value' };
83+
84+
q.enqueue(item);
85+
expect(q.dequeue()).toStrictEqual(item);
86+
});
87+
});
88+
89+
describe('peek', () => {
90+
it.each([
91+
{
92+
desc: 'the front item without removing it', items: ['a', 'b'], expected: 'a'
93+
},
94+
{
95+
desc: 'undefined when queue is empty', items: [] as string[], expected: undefined
96+
},
97+
])('returns $desc', ({ items, expected }) => {
98+
const q = new Queue();
99+
100+
items.forEach((item) => q.enqueue(item));
101+
102+
expect(q.peek()).toStrictEqual(expected);
103+
expect(q.getLength()).toStrictEqual(items.length);
104+
});
105+
});
106+
107+
describe('clear', () => {
108+
it('empties the queue', () => {
109+
const q = new Queue();
110+
111+
q.enqueue('a');
112+
q.enqueue('b');
113+
q.clear();
114+
115+
expect(q.isEmpty()).toStrictEqual(true);
116+
expect(q.getLength()).toStrictEqual(0);
117+
});
118+
119+
it('allows re-use after clear', () => {
120+
const q = new Queue();
121+
122+
q.enqueue('a');
123+
q.clear();
124+
q.enqueue('b');
125+
126+
expect(q.dequeue()).toStrictEqual('b');
127+
});
128+
});
129+
});
130+
131+
describe('allHash', () => {
132+
it('resolves a hash of resolved promises', async() => {
133+
const result = await allHash({
134+
a: Promise.resolve(1),
135+
b: Promise.resolve(2),
136+
c: Promise.resolve(3),
137+
});
138+
139+
expect(result).toStrictEqual({
140+
a: 1,
141+
b: 2,
142+
c: 3,
143+
});
144+
});
145+
146+
it('rejects if any promise in the hash rejects', async() => {
147+
await expect(
148+
allHash({
149+
a: Promise.resolve(1),
150+
b: Promise.reject(new Error('fail')),
151+
})
152+
).rejects.toThrow('fail');
153+
});
154+
155+
it('returns an empty object for an empty hash', async() => {
156+
const result = await allHash({});
157+
158+
expect(result).toStrictEqual({});
159+
});
160+
161+
it('preserves key ordering from the input hash', async() => {
162+
const result = await allHash({
163+
z: Promise.resolve('z-val'),
164+
a: Promise.resolve('a-val'),
165+
});
166+
167+
expect(Object.keys(result)).toStrictEqual(['z', 'a']);
168+
expect(result).toStrictEqual({ z: 'z-val', a: 'a-val' });
169+
});
170+
171+
it('resolves non-promise values in the hash', async() => {
172+
const result = await allHash({ x: 42 });
173+
174+
expect(result).toStrictEqual({ x: 42 });
175+
});
176+
});
177+
178+
describe('allHashSettled', () => {
179+
it('resolves with fulfilled/rejected statuses for each key', async() => {
180+
const result = await allHashSettled({
181+
ok: Promise.resolve('success'),
182+
err: Promise.reject(new Error('oops')),
183+
});
184+
185+
expect(result.ok).toStrictEqual({ status: 'fulfilled', value: 'success' });
186+
expect(result.err.status).toStrictEqual('rejected');
187+
expect((result.err as PromiseRejectedResult).reason.message).toStrictEqual('oops');
188+
});
189+
190+
it('returns an empty object for an empty hash', async() => {
191+
const result = await allHashSettled({});
192+
193+
expect(result).toStrictEqual({});
194+
});
195+
196+
it('never rejects — always resolves even when all promises fail', async() => {
197+
await expect(
198+
allHashSettled({
199+
a: Promise.reject(new Error('a-fail')),
200+
b: Promise.reject(new Error('b-fail')),
201+
})
202+
).resolves.toBeDefined();
203+
});
204+
});
205+
206+
describe('eachLimit', () => {
207+
it('processes all items and returns results in index order', async() => {
208+
const items = [1, 2, 3, 4, 5];
209+
const result = await eachLimit(items, 2, (item: number) => Promise.resolve(item * 10));
210+
211+
expect(result).toStrictEqual([10, 20, 30, 40, 50]);
212+
});
213+
214+
it('returns an empty array for empty input', async() => {
215+
const result = await eachLimit([], 3, () => Promise.resolve('x'));
216+
217+
expect(result).toStrictEqual([]);
218+
});
219+
220+
it('rejects if any iterator rejects', async() => {
221+
await expect(
222+
eachLimit([1, 2, 3], 2, (item: number) => {
223+
if (item === 2) {
224+
return Promise.reject(new Error('item-2-failed'));
225+
}
226+
227+
return Promise.resolve(item);
228+
})
229+
).rejects.toThrow('item-2-failed');
230+
});
231+
232+
it('handles limit larger than the number of items', async() => {
233+
const items = ['a', 'b'];
234+
const result = await eachLimit(items, 100, (item: string) => Promise.resolve(`done-${ item }`));
235+
236+
expect(result).toStrictEqual(['done-a', 'done-b']);
237+
});
238+
239+
it('handles limit of 1 (sequential processing)', async() => {
240+
const order: number[] = [];
241+
const items = [1, 2, 3];
242+
243+
await eachLimit(items, 1, (item: number) => {
244+
order.push(item);
245+
246+
return Promise.resolve(item);
247+
});
248+
249+
expect(order).toStrictEqual([1, 2, 3]);
250+
});
251+
252+
it('respects the concurrency limit', async() => {
253+
let concurrent = 0;
254+
let maxConcurrent = 0;
255+
const limit = 2;
256+
const items = [1, 2, 3, 4, 5];
257+
258+
await eachLimit(items, limit, (item: number) => {
259+
concurrent++;
260+
if (concurrent > maxConcurrent) {
261+
maxConcurrent = concurrent;
262+
}
263+
264+
return new Promise<number>((resolve) => {
265+
setTimeout(() => {
266+
concurrent--;
267+
resolve(item);
268+
}, 5);
269+
});
270+
});
271+
272+
expect(maxConcurrent).toStrictEqual(limit);
273+
});
274+
275+
it('passes the item index as the second argument to the iterator', async() => {
276+
const indices: number[] = [];
277+
278+
await eachLimit(['x', 'y', 'z'], 3, (_item: string, idx: number) => {
279+
indices.push(idx);
280+
281+
return Promise.resolve(idx);
282+
});
283+
284+
expect(indices).toStrictEqual([0, 1, 2]);
285+
});
286+
});
287+
288+
describe('deferred', () => {
289+
it('returns an object with promise, resolve, and reject', () => {
290+
const d = deferred('test');
291+
292+
expect(d.promise).toBeInstanceOf(Promise);
293+
expect(typeof d.resolve).toStrictEqual('function');
294+
expect(typeof d.reject).toStrictEqual('function');
295+
});
296+
297+
it('resolves the promise when resolve is called', async() => {
298+
const d = deferred('resolve-test');
299+
300+
d.resolve('resolved-value');
301+
302+
await expect(d.promise).resolves.toStrictEqual('resolved-value');
303+
});
304+
305+
it('rejects the promise when reject is called', async() => {
306+
const d = deferred('reject-test');
307+
308+
d.reject(new Error('deferred-error'));
309+
310+
await expect(d.promise).rejects.toThrow('deferred-error');
311+
});
312+
});
313+
314+
describe('setPromiseResult', () => {
315+
it('sets the resolved value on the target object property', async() => {
316+
const obj: { result?: string } = {};
317+
318+
setPromiseResult(Promise.resolve('hello'), obj, 'result', 'test label');
319+
await new Promise((resolve) => setTimeout(resolve, 0));
320+
321+
expect(obj.result).toStrictEqual('hello');
322+
});
323+
324+
it('does not throw when the promise rejects — logs a warning instead', async() => {
325+
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
326+
const obj: { value?: string } = {};
327+
328+
setPromiseResult(Promise.reject(new Error('boom')), obj, 'value', 'failing label');
329+
await new Promise((resolve) => setTimeout(resolve, 0));
330+
331+
expect(obj.value).toBeUndefined();
332+
expect(warnSpy).toHaveBeenCalledWith('Failed to: ', 'failing label', expect.any(Error));
333+
warnSpy.mockRestore();
334+
});
335+
336+
it('leaves the object property unchanged when the promise rejects', async() => {
337+
jest.spyOn(console, 'warn').mockImplementation(() => {});
338+
const obj: { data: string } = { data: 'original' };
339+
340+
setPromiseResult(Promise.reject(new Error('err')), obj, 'data', 'test');
341+
await new Promise((resolve) => setTimeout(resolve, 0));
342+
343+
expect(obj.data).toStrictEqual('original');
344+
jest.restoreAllMocks();
345+
});
346+
});

0 commit comments

Comments
 (0)