Skip to content

Commit f917d27

Browse files
committed
fix: make RefreshController tests bun-compatible
Change-Id: I73e70a106d775fe476864725b484c74a210e0775 Signed-off-by: Thomas Kosiewski <[email protected]>
1 parent 4d9c69a commit f917d27

File tree

1 file changed

+72
-75
lines changed

1 file changed

+72
-75
lines changed

src/browser/utils/RefreshController.test.ts

Lines changed: 72 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,49 @@
1-
import { describe, it, expect, beforeEach, afterEach, jest } from "bun:test";
2-
import { RefreshController } from "./RefreshController";
1+
import { describe, it, expect, mock } from "bun:test";
32

4-
describe("RefreshController", () => {
5-
beforeEach(() => {
6-
jest.useFakeTimers();
7-
});
3+
import { RefreshController, type LastRefreshInfo } from "./RefreshController";
84

9-
afterEach(() => {
10-
jest.useRealTimers();
11-
});
5+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
126

13-
it("rate-limits multiple schedule() calls (doesn't reset timer)", () => {
14-
const onRefresh = jest.fn<() => void>();
15-
const controller = new RefreshController({ onRefresh, debounceMs: 100 });
7+
// NOTE: Bun's Jest-compat layer does not currently expose timer controls like
8+
// jest.advanceTimersByTime(), so these tests use real timers.
9+
10+
describe("RefreshController", () => {
11+
it("rate-limits multiple schedule() calls (doesn't reset timer)", async () => {
12+
const onRefresh = mock<() => void>(() => undefined);
13+
const controller = new RefreshController({ onRefresh, debounceMs: 20 });
1614

1715
controller.schedule();
18-
jest.advanceTimersByTime(50);
16+
await sleep(10);
1917
controller.schedule(); // Shouldn't reset timer
20-
jest.advanceTimersByTime(50);
2118

22-
// Should fire at 100ms from first call, not 150ms
19+
await sleep(40);
20+
21+
// Should fire ~20ms after first call, not ~30ms after second.
2322
expect(onRefresh).toHaveBeenCalledTimes(1);
2423

2524
controller.dispose();
2625
});
2726

28-
it("coalesces calls during rate-limit window", () => {
29-
const onRefresh = jest.fn<() => void>();
30-
const controller = new RefreshController({ onRefresh, debounceMs: 100 });
27+
it("coalesces calls during rate-limit window", async () => {
28+
const onRefresh = mock<() => void>(() => undefined);
29+
const controller = new RefreshController({ onRefresh, debounceMs: 20 });
3130

3231
controller.schedule();
3332
controller.schedule();
3433
controller.schedule();
3534

3635
expect(onRefresh).not.toHaveBeenCalled();
3736

38-
jest.advanceTimersByTime(100);
37+
await sleep(60);
3938

4039
expect(onRefresh).toHaveBeenCalledTimes(1);
4140

4241
controller.dispose();
4342
});
4443

45-
it("requestImmediate() bypasses rate-limit timer", () => {
46-
const onRefresh = jest.fn<() => void>();
47-
const controller = new RefreshController({ onRefresh, debounceMs: 100 });
44+
it("requestImmediate() bypasses rate-limit timer", async () => {
45+
const onRefresh = mock<() => void>(() => undefined);
46+
const controller = new RefreshController({ onRefresh, debounceMs: 50 });
4847

4948
controller.schedule();
5049
expect(onRefresh).not.toHaveBeenCalled();
@@ -53,7 +52,7 @@ describe("RefreshController", () => {
5352
expect(onRefresh).toHaveBeenCalledTimes(1);
5453

5554
// Original timer should be cleared
56-
jest.advanceTimersByTime(100);
55+
await sleep(80);
5756
expect(onRefresh).toHaveBeenCalledTimes(1);
5857

5958
controller.dispose();
@@ -62,17 +61,14 @@ describe("RefreshController", () => {
6261
it("guards against concurrent sync refreshes (in-flight queuing)", () => {
6362
// Track if refresh is currently in-flight
6463
let inFlight = false;
65-
const onRefresh = jest.fn(() => {
66-
// Simulate sync operation that takes time
64+
const onRefresh = mock(() => {
6765
expect(inFlight).toBe(false); // Should never be called while already in-flight
6866
inFlight = true;
69-
// Immediately complete (sync)
7067
inFlight = false;
7168
});
7269

73-
const controller = new RefreshController({ onRefresh, debounceMs: 100 });
70+
const controller = new RefreshController({ onRefresh, debounceMs: 20 });
7471

75-
// Multiple immediate requests should only call once (queued ones execute after)
7672
controller.requestImmediate();
7773
expect(onRefresh).toHaveBeenCalledTimes(1);
7874

@@ -81,169 +77,170 @@ describe("RefreshController", () => {
8177

8278
it("schedule() during in-flight queues refresh for after completion", async () => {
8379
let resolveRefresh: () => void;
84-
const onRefresh = jest.fn(
80+
const onRefresh = mock(
8581
() =>
8682
new Promise<void>((resolve) => {
8783
resolveRefresh = resolve;
8884
})
8985
);
9086

91-
const controller = new RefreshController({ onRefresh, debounceMs: 100 });
87+
const controller = new RefreshController({ onRefresh, debounceMs: 20 });
9288

93-
// Start first refresh
9489
controller.requestImmediate();
9590
expect(onRefresh).toHaveBeenCalledTimes(1);
9691

97-
// schedule() while in-flight should queue, not start timer
92+
// schedule() while in-flight should queue for after completion.
9893
controller.schedule();
9994

100-
// Complete the first refresh and let .finally() run
10195
resolveRefresh!();
10296
await Promise.resolve();
103-
await Promise.resolve(); // Extra tick for .finally()
10497

105-
// Should trigger follow-up refresh, but never more frequently than the minimum interval.
106-
// First tick runs the post-flight setTimeout(0), then we wait out the min interval.
107-
jest.advanceTimersByTime(0);
108-
jest.advanceTimersByTime(500);
98+
// Follow-up refresh is subject to MIN_REFRESH_INTERVAL_MS (500ms).
99+
await sleep(650);
100+
109101
expect(onRefresh).toHaveBeenCalledTimes(2);
110102

111103
controller.dispose();
112104
});
113105

114-
it("isRefreshing reflects in-flight state", () => {
106+
it("isRefreshing reflects in-flight state", async () => {
115107
let resolveRefresh: () => void;
116108
const refreshPromise = new Promise<void>((resolve) => {
117109
resolveRefresh = resolve;
118110
});
119111

120-
const onRefresh = jest.fn(() => refreshPromise);
121-
const controller = new RefreshController({ onRefresh, debounceMs: 100 });
112+
const onRefresh = mock(() => refreshPromise);
113+
const controller = new RefreshController({ onRefresh, debounceMs: 20 });
122114

123115
expect(controller.isRefreshing).toBe(false);
124116

125117
controller.requestImmediate();
126118
expect(controller.isRefreshing).toBe(true);
127119

128-
// Complete the promise
129120
resolveRefresh!();
121+
await Promise.resolve();
122+
123+
expect(controller.isRefreshing).toBe(false);
130124

131125
controller.dispose();
132126
});
133127

134-
it("dispose() cleans up debounce timer", () => {
135-
const onRefresh = jest.fn<() => void>();
136-
const controller = new RefreshController({ onRefresh, debounceMs: 100 });
128+
it("dispose() cleans up debounce timer", async () => {
129+
const onRefresh = mock<() => void>(() => undefined);
130+
const controller = new RefreshController({ onRefresh, debounceMs: 20 });
137131

138132
controller.schedule();
139133
controller.dispose();
140134

141-
jest.advanceTimersByTime(100);
135+
await sleep(80);
142136

143137
expect(onRefresh).not.toHaveBeenCalled();
144138
});
145139

146-
it("does not refresh after dispose", () => {
147-
const onRefresh = jest.fn<() => void>();
148-
const controller = new RefreshController({ onRefresh, debounceMs: 100 });
140+
it("does not refresh after dispose", async () => {
141+
const onRefresh = mock<() => void>(() => undefined);
142+
const controller = new RefreshController({ onRefresh, debounceMs: 20 });
149143

150144
controller.dispose();
151145
controller.schedule();
152146
controller.requestImmediate();
153147

154-
jest.advanceTimersByTime(100);
148+
await sleep(80);
155149

156150
expect(onRefresh).not.toHaveBeenCalled();
157151
});
158152

159-
it("requestImmediate() bypasses isPaused check (for manual refresh)", () => {
160-
const onRefresh = jest.fn<() => void>();
153+
it("requestImmediate() bypasses isPaused check (for manual refresh)", async () => {
154+
const onRefresh = mock<() => void>(() => undefined);
161155
const paused = true;
162156
const controller = new RefreshController({
163157
onRefresh,
164-
debounceMs: 100,
158+
debounceMs: 20,
165159
isPaused: () => paused,
166160
});
167161

168-
// schedule() should be blocked by isPaused
169162
controller.schedule();
170-
jest.advanceTimersByTime(100);
163+
await sleep(80);
171164
expect(onRefresh).not.toHaveBeenCalled();
172165

173-
// requestImmediate() should bypass isPaused (manual refresh)
174166
controller.requestImmediate();
175167
expect(onRefresh).toHaveBeenCalledTimes(1);
176168

177169
controller.dispose();
178170
});
179171

180-
it("schedule() respects isPaused and flushes on notifyUnpaused", () => {
181-
const onRefresh = jest.fn<() => void>();
172+
it("schedule() respects isPaused and flushes on notifyUnpaused", async () => {
173+
const onRefresh = mock<() => void>(() => undefined);
182174
let paused = true;
183175
const controller = new RefreshController({
184176
onRefresh,
185-
debounceMs: 100,
177+
debounceMs: 20,
186178
isPaused: () => paused,
187179
});
188180

189-
// schedule() should queue but not execute while paused
190181
controller.schedule();
191-
jest.advanceTimersByTime(100);
182+
await sleep(80);
192183
expect(onRefresh).not.toHaveBeenCalled();
193184

194-
// Unpausing should flush the pending refresh
195185
paused = false;
196186
controller.notifyUnpaused();
187+
197188
expect(onRefresh).toHaveBeenCalledTimes(1);
198189

199190
controller.dispose();
200191
});
201192

202-
it("lastRefreshInfo tracks trigger and timestamp", () => {
203-
const onRefresh = jest.fn<() => void>();
204-
const controller = new RefreshController({ onRefresh, debounceMs: 100 });
193+
it("lastRefreshInfo tracks trigger and timestamp", async () => {
194+
const onRefresh = mock<() => void>(() => undefined);
195+
const controller = new RefreshController({
196+
onRefresh,
197+
debounceMs: 20,
198+
priorityDebounceMs: 20,
199+
});
205200

206201
expect(controller.lastRefreshInfo).toBeNull();
207202

208-
// Manual refresh should record "manual" trigger
209203
const beforeManual = Date.now();
210204
controller.requestImmediate();
205+
206+
expect(onRefresh).toHaveBeenCalledTimes(1);
211207
expect(controller.lastRefreshInfo).not.toBeNull();
212208
expect(controller.lastRefreshInfo!.trigger).toBe("manual");
213209
expect(controller.lastRefreshInfo!.timestamp).toBeGreaterThanOrEqual(beforeManual);
214210

215-
// Scheduled refresh should record "scheduled" trigger
216211
controller.schedule();
217-
jest.advanceTimersByTime(500);
212+
await sleep(650);
213+
expect(onRefresh).toHaveBeenCalledTimes(2);
218214
expect(controller.lastRefreshInfo!.trigger).toBe("scheduled");
219215

220-
// Priority refresh should record "priority" trigger
221216
controller.schedulePriority();
222-
jest.advanceTimersByTime(500);
217+
await sleep(650);
218+
expect(onRefresh).toHaveBeenCalledTimes(3);
223219
expect(controller.lastRefreshInfo!.trigger).toBe("priority");
224220

225221
controller.dispose();
226222
});
227223

228-
it("onRefreshComplete callback is called with refresh info", () => {
229-
const onRefresh = jest.fn<() => void>();
230-
const onRefreshComplete = jest.fn<(info: { trigger: string; timestamp: number }) => void>();
224+
it("onRefreshComplete callback is called with refresh info", async () => {
225+
const onRefresh = mock<() => void>(() => undefined);
226+
const onRefreshComplete = mock<(info: LastRefreshInfo) => void>(() => undefined);
231227
const controller = new RefreshController({
232228
onRefresh,
233229
onRefreshComplete,
234-
debounceMs: 100,
230+
debounceMs: 20,
235231
});
236232

237233
expect(onRefreshComplete).not.toHaveBeenCalled();
238234

239235
controller.requestImmediate();
240236
expect(onRefreshComplete).toHaveBeenCalledTimes(1);
241237
expect(onRefreshComplete).toHaveBeenCalledWith(
238+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
242239
expect.objectContaining({ trigger: "manual", timestamp: expect.any(Number) })
243240
);
244241

245242
controller.schedule();
246-
jest.advanceTimersByTime(500);
243+
await sleep(650);
247244
expect(onRefreshComplete).toHaveBeenCalledTimes(2);
248245
expect(onRefreshComplete).toHaveBeenLastCalledWith(
249246
expect.objectContaining({ trigger: "scheduled" })

0 commit comments

Comments
 (0)