Skip to content

Commit 30a6b9b

Browse files
[Test Improver] Add unit tests for Poller and PollerSequential utility classes (rancher#17748)
* Add unit tests for Poller and PollerSequential utility classes Test coverage for shell/utils/poller.js and shell/utils/poller-sequential.js: - 17 tests for Poller (constructor, start, stop, _intervalMethod) - 15 tests for PollerSequential (constructor, start, stop, _poll, _intervalMethod) - Uses Jest fake timers to test interval/timeout behaviour deterministically Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: trigger checks --------- 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: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 030a453 commit 30a6b9b

2 files changed

Lines changed: 347 additions & 0 deletions

File tree

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import PollerSequential from '@shell/utils/poller-sequential';
2+
3+
describe('pollerSequential', () => {
4+
let fn: jest.Mock;
5+
6+
beforeEach(() => {
7+
jest.useFakeTimers();
8+
fn = jest.fn().mockResolvedValue(undefined);
9+
});
10+
11+
afterEach(() => {
12+
jest.useRealTimers();
13+
jest.restoreAllMocks();
14+
});
15+
16+
describe('constructor', () => {
17+
it('should store fn, pollRateMs and maxRetries', () => {
18+
const poller = new PollerSequential(fn, 1000, 5);
19+
20+
expect(poller.fn).toBe(fn);
21+
expect(poller.pollRateMs).toBe(1000);
22+
expect(poller.maxRetries).toBe(5);
23+
});
24+
25+
it('should default maxRetries to POSITIVE_INFINITY', () => {
26+
const poller = new PollerSequential(fn, 500);
27+
28+
expect(poller.maxRetries).toBe(Number.POSITIVE_INFINITY);
29+
});
30+
31+
it('should substitute a no-op when fn is falsy', () => {
32+
const poller = new PollerSequential(null as any, 500);
33+
34+
expect(typeof poller.fn).toBe('function');
35+
});
36+
37+
it('should initialise tryCount to 0', () => {
38+
const poller = new PollerSequential(fn, 1000);
39+
40+
expect(poller.tryCount).toBe(0);
41+
});
42+
43+
it('should initialise timeoutId to undefined', () => {
44+
const poller = new PollerSequential(fn, 1000);
45+
46+
expect(poller.timeoutId).toBeUndefined();
47+
});
48+
});
49+
50+
describe('start', () => {
51+
it('should set timeoutId after start', () => {
52+
const poller = new PollerSequential(fn, 1000);
53+
54+
poller.start();
55+
56+
expect(poller.timeoutId).toBeDefined();
57+
58+
poller.stop();
59+
});
60+
61+
it('should not call fn immediately (delayed unlike Poller)', () => {
62+
const poller = new PollerSequential(fn, 1000);
63+
64+
poller.start();
65+
66+
expect(fn).not.toHaveBeenCalled();
67+
68+
poller.stop();
69+
});
70+
71+
it('should stop and restart when start is called while already running', () => {
72+
const poller = new PollerSequential(fn, 1000);
73+
const stopSpy = jest.spyOn(poller, 'stop');
74+
75+
poller.start();
76+
poller.start();
77+
78+
expect(stopSpy).toHaveBeenCalledWith();
79+
80+
poller.stop();
81+
});
82+
});
83+
84+
describe('stop', () => {
85+
it('should clear timeoutId after stop', () => {
86+
const poller = new PollerSequential(fn, 1000);
87+
88+
poller.start();
89+
poller.stop();
90+
91+
expect(poller.timeoutId).toBeUndefined();
92+
});
93+
94+
it('should not call fn after stop', async() => {
95+
const poller = new PollerSequential(fn, 1000);
96+
97+
poller.start();
98+
poller.stop();
99+
jest.advanceTimersByTime(10000);
100+
101+
expect(fn).not.toHaveBeenCalled();
102+
});
103+
104+
it('should be safe to call stop when not running', () => {
105+
const poller = new PollerSequential(fn, 1000);
106+
107+
expect(() => poller.stop()).not.toThrow();
108+
});
109+
});
110+
111+
describe('_poll', () => {
112+
it('should not continue polling if timeoutId has been cleared', async() => {
113+
const poller = new PollerSequential(fn, 1000);
114+
115+
poller.timeoutId = undefined;
116+
poller._poll();
117+
118+
jest.advanceTimersByTime(5000);
119+
120+
expect(fn).not.toHaveBeenCalled();
121+
});
122+
123+
it('should clear timeoutId and schedule next poll', () => {
124+
const poller = new PollerSequential(fn, 1000);
125+
126+
poller.start();
127+
expect(poller.timeoutId).toBeDefined();
128+
129+
const firstId = poller.timeoutId;
130+
131+
// Advance past the initial delay to trigger _poll
132+
jest.advanceTimersByTime(1000);
133+
134+
// _poll should have set a new timeout (different from the first)
135+
expect(poller.timeoutId).toBeDefined();
136+
// timeoutId should have changed since _poll calls stop then sets a new one
137+
expect(poller.timeoutId).not.toBe(firstId);
138+
139+
poller.stop();
140+
});
141+
});
142+
143+
describe('_intervalMethod', () => {
144+
it('should reset tryCount to 0 after a successful call', async() => {
145+
const poller = new PollerSequential(fn, 1000);
146+
147+
poller.tryCount = 3;
148+
await poller._intervalMethod();
149+
150+
expect(poller.tryCount).toBe(0);
151+
});
152+
153+
it('should increment tryCount on each error', async() => {
154+
fn.mockRejectedValue(new Error('fail'));
155+
const poller = new PollerSequential(fn, 1000, 5);
156+
157+
await poller._intervalMethod();
158+
expect(poller.tryCount).toBe(1);
159+
160+
await poller._intervalMethod();
161+
expect(poller.tryCount).toBe(2);
162+
});
163+
164+
it('should stop when tryCount reaches maxRetries', async() => {
165+
fn.mockRejectedValue(new Error('fail'));
166+
const poller = new PollerSequential(fn, 1000, 3);
167+
const stopSpy = jest.spyOn(poller, 'stop');
168+
169+
await poller._intervalMethod();
170+
await poller._intervalMethod();
171+
expect(stopSpy).not.toHaveBeenCalled();
172+
173+
await poller._intervalMethod();
174+
expect(stopSpy).toHaveBeenCalledTimes(1);
175+
});
176+
});
177+
});
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import Poller from '@shell/utils/poller';
2+
3+
describe('poller', () => {
4+
let fn: jest.Mock;
5+
6+
beforeEach(() => {
7+
jest.useFakeTimers();
8+
fn = jest.fn().mockResolvedValue(undefined);
9+
});
10+
11+
afterEach(() => {
12+
jest.useRealTimers();
13+
jest.restoreAllMocks();
14+
});
15+
16+
describe('constructor', () => {
17+
it('should store fn, pollRateMs and maxRetries', () => {
18+
const poller = new Poller(fn, 1000, 5);
19+
20+
expect(poller.fn).toBe(fn);
21+
expect(poller.pollRateMs).toBe(1000);
22+
expect(poller.maxRetries).toBe(5);
23+
});
24+
25+
it('should default maxRetries to POSITIVE_INFINITY', () => {
26+
const poller = new Poller(fn, 500);
27+
28+
expect(poller.maxRetries).toBe(Number.POSITIVE_INFINITY);
29+
});
30+
31+
it('should substitute a no-op when fn is falsy', () => {
32+
const poller = new Poller(null as any, 500);
33+
34+
expect(typeof poller.fn).toBe('function');
35+
});
36+
37+
it('should initialise tryCount to 0', () => {
38+
const poller = new Poller(fn, 1000);
39+
40+
expect(poller.tryCount).toBe(0);
41+
});
42+
43+
it('should initialise intervalId to undefined', () => {
44+
const poller = new Poller(fn, 1000);
45+
46+
expect(poller.intervalId).toBeUndefined();
47+
});
48+
});
49+
50+
describe('start', () => {
51+
it('should call fn immediately when started', () => {
52+
const poller = new Poller(fn, 1000);
53+
54+
poller.start();
55+
56+
expect(fn).toHaveBeenCalledTimes(1);
57+
});
58+
59+
it('should set intervalId after start', () => {
60+
const poller = new Poller(fn, 1000);
61+
62+
poller.start();
63+
64+
expect(poller.intervalId).toBeDefined();
65+
});
66+
67+
it('should call fn again after each poll interval', async() => {
68+
const poller = new Poller(fn, 1000);
69+
70+
poller.start();
71+
expect(fn).toHaveBeenCalledTimes(1);
72+
73+
jest.advanceTimersByTime(1000);
74+
expect(fn).toHaveBeenCalledTimes(2);
75+
76+
jest.advanceTimersByTime(1000);
77+
expect(fn).toHaveBeenCalledTimes(3);
78+
79+
poller.stop();
80+
});
81+
82+
it('should stop and restart when start is called while already running', () => {
83+
const poller = new Poller(fn, 1000);
84+
const stopSpy = jest.spyOn(poller, 'stop');
85+
86+
poller.start();
87+
poller.start();
88+
89+
expect(stopSpy).toHaveBeenCalledWith();
90+
91+
poller.stop();
92+
});
93+
});
94+
95+
describe('stop', () => {
96+
it('should clear intervalId after stop', () => {
97+
const poller = new Poller(fn, 1000);
98+
99+
poller.start();
100+
poller.stop();
101+
102+
expect(poller.intervalId).toBeUndefined();
103+
});
104+
105+
it('should not call fn after stop', () => {
106+
const poller = new Poller(fn, 1000);
107+
108+
poller.start();
109+
expect(fn).toHaveBeenCalledTimes(1);
110+
111+
poller.stop();
112+
jest.advanceTimersByTime(5000);
113+
114+
expect(fn).toHaveBeenCalledTimes(1);
115+
});
116+
117+
it('should be safe to call stop when not running', () => {
118+
const poller = new Poller(fn, 1000);
119+
120+
expect(() => poller.stop()).not.toThrow();
121+
});
122+
});
123+
124+
describe('_intervalMethod', () => {
125+
it('should reset tryCount to 0 after a successful call', async() => {
126+
const poller = new Poller(fn, 1000);
127+
128+
poller.tryCount = 3;
129+
await poller._intervalMethod();
130+
131+
expect(poller.tryCount).toBe(0);
132+
});
133+
134+
it('should increment tryCount on each error', async() => {
135+
fn.mockRejectedValue(new Error('fail'));
136+
const poller = new Poller(fn, 1000, 5);
137+
138+
await poller._intervalMethod();
139+
expect(poller.tryCount).toBe(1);
140+
141+
await poller._intervalMethod();
142+
expect(poller.tryCount).toBe(2);
143+
});
144+
145+
it('should stop when tryCount reaches maxRetries', async() => {
146+
fn.mockRejectedValue(new Error('fail'));
147+
const poller = new Poller(fn, 1000, 3);
148+
const stopSpy = jest.spyOn(poller, 'stop');
149+
150+
await poller._intervalMethod();
151+
await poller._intervalMethod();
152+
expect(stopSpy).not.toHaveBeenCalled();
153+
154+
await poller._intervalMethod();
155+
expect(stopSpy).toHaveBeenCalledTimes(1);
156+
});
157+
158+
it('should not stop before maxRetries is reached', async() => {
159+
fn.mockRejectedValue(new Error('fail'));
160+
const poller = new Poller(fn, 1000, 5);
161+
const stopSpy = jest.spyOn(poller, 'stop');
162+
163+
for (let i = 0; i < 4; i++) {
164+
await poller._intervalMethod();
165+
}
166+
167+
expect(stopSpy).not.toHaveBeenCalled();
168+
});
169+
});
170+
});

0 commit comments

Comments
 (0)