1- import { describe , it , expect , beforeEach , afterEach , jest } from "@jest/globals " ;
1+ import { describe , it , expect , afterEach , jest , setSystemTime } from "bun:test " ;
22import { RefreshController } from "./RefreshController" ;
33
4- describe ( "RefreshController" , ( ) => {
5- beforeEach ( ( ) => {
6- jest . useFakeTimers ( ) ;
7- } ) ;
4+ async function sleep ( ms : number ) : Promise < void > {
5+ await new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
6+ }
87
8+ describe ( "RefreshController" , ( ) => {
99 afterEach ( ( ) => {
10- jest . useRealTimers ( ) ;
10+ // Some tests manipulate Date.now via setSystemTime(); always restore.
11+ setSystemTime ( ) ;
1112 } ) ;
1213
13- it ( "rate-limits multiple schedule() calls (doesn't reset timer)" , ( ) => {
14+ it ( "rate-limits multiple schedule() calls (doesn't reset timer)" , async ( ) => {
1415 const onRefresh = jest . fn < ( ) => void > ( ) ;
15- const controller = new RefreshController ( { onRefresh, debounceMs : 100 } ) ;
16+ const controller = new RefreshController ( { onRefresh, debounceMs : 50 } ) ;
1617
1718 controller . schedule ( ) ;
18- jest . advanceTimersByTime ( 50 ) ;
19+ await sleep ( 20 ) ;
1920 controller . schedule ( ) ; // Shouldn't reset timer
20- jest . advanceTimersByTime ( 50 ) ;
2121
22- // Should fire at 100ms from first call, not 150ms
22+ // Should fire at 50ms from first call, not 70ms
23+ await sleep ( 40 ) ;
2324 expect ( onRefresh ) . toHaveBeenCalledTimes ( 1 ) ;
2425
2526 controller . dispose ( ) ;
2627 } ) ;
2728
28- it ( "coalesces calls during rate-limit window" , ( ) => {
29+ it ( "coalesces calls during rate-limit window" , async ( ) => {
2930 const onRefresh = jest . fn < ( ) => void > ( ) ;
30- const controller = new RefreshController ( { onRefresh, debounceMs : 100 } ) ;
31+ const controller = new RefreshController ( { onRefresh, debounceMs : 50 } ) ;
3132
3233 controller . schedule ( ) ;
3334 controller . schedule ( ) ;
3435 controller . schedule ( ) ;
3536
3637 expect ( onRefresh ) . not . toHaveBeenCalled ( ) ;
3738
38- jest . advanceTimersByTime ( 100 ) ;
39+ await sleep ( 60 ) ;
3940
4041 expect ( onRefresh ) . toHaveBeenCalledTimes ( 1 ) ;
4142
4243 controller . dispose ( ) ;
4344 } ) ;
4445
45- it ( "requestImmediate() bypasses rate-limit timer" , ( ) => {
46+ it ( "requestImmediate() bypasses rate-limit timer" , async ( ) => {
4647 const onRefresh = jest . fn < ( ) => void > ( ) ;
47- const controller = new RefreshController ( { onRefresh, debounceMs : 100 } ) ;
48+ const controller = new RefreshController ( { onRefresh, debounceMs : 50 } ) ;
4849
4950 controller . schedule ( ) ;
5051 expect ( onRefresh ) . not . toHaveBeenCalled ( ) ;
@@ -53,7 +54,7 @@ describe("RefreshController", () => {
5354 expect ( onRefresh ) . toHaveBeenCalledTimes ( 1 ) ;
5455
5556 // Original timer should be cleared
56- jest . advanceTimersByTime ( 100 ) ;
57+ await sleep ( 60 ) ;
5758 expect ( onRefresh ) . toHaveBeenCalledTimes ( 1 ) ;
5859
5960 controller . dispose ( ) ;
@@ -80,33 +81,45 @@ describe("RefreshController", () => {
8081 } ) ;
8182
8283 it ( "schedule() during in-flight queues refresh for after completion" , async ( ) => {
83- let resolveRefresh : ( ) => void ;
84+ setSystemTime ( 0 ) ;
85+
86+ const resolvers : Array < ( ) => void > = [ ] ;
8487 const onRefresh = jest . fn (
8588 ( ) =>
8689 new Promise < void > ( ( resolve ) => {
87- resolveRefresh = resolve ;
90+ resolvers . push ( resolve ) ;
8891 } )
8992 ) ;
9093
91- const controller = new RefreshController ( { onRefresh, debounceMs : 100 } ) ;
94+ const controller = new RefreshController ( { onRefresh, debounceMs : 50 } ) ;
9295
9396 // Start first refresh
9497 controller . requestImmediate ( ) ;
9598 expect ( onRefresh ) . toHaveBeenCalledTimes ( 1 ) ;
99+ expect ( resolvers ) . toHaveLength ( 1 ) ;
96100
97101 // schedule() while in-flight should queue, not start timer
98102 controller . schedule ( ) ;
99103
100104 // Complete the first refresh and let .finally() run
101- resolveRefresh ! ( ) ;
105+ resolvers [ 0 ] ( ) ;
102106 await Promise . resolve ( ) ;
103107 await Promise . resolve ( ) ; // Extra tick for .finally()
104108
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 ) ;
109+ // Make the follow-up refresh eligible immediately (skip MIN_REFRESH_INTERVAL_MS wait)
110+ setSystemTime ( 1000 ) ;
111+
112+ // Allow post-flight setTimeout(0) to run
113+ await sleep ( 0 ) ;
114+ await sleep ( 10 ) ;
115+
109116 expect ( onRefresh ) . toHaveBeenCalledTimes ( 2 ) ;
117+ expect ( resolvers ) . toHaveLength ( 2 ) ;
118+
119+ // Resolve the follow-up refresh promise to avoid leaving it in-flight.
120+ resolvers [ 1 ] ( ) ;
121+ await Promise . resolve ( ) ;
122+ await Promise . resolve ( ) ;
110123
111124 controller . dispose ( ) ;
112125 } ) ;
@@ -131,43 +144,43 @@ describe("RefreshController", () => {
131144 controller . dispose ( ) ;
132145 } ) ;
133146
134- it ( "dispose() cleans up debounce timer" , ( ) => {
147+ it ( "dispose() cleans up debounce timer" , async ( ) => {
135148 const onRefresh = jest . fn < ( ) => void > ( ) ;
136- const controller = new RefreshController ( { onRefresh, debounceMs : 100 } ) ;
149+ const controller = new RefreshController ( { onRefresh, debounceMs : 50 } ) ;
137150
138151 controller . schedule ( ) ;
139152 controller . dispose ( ) ;
140153
141- jest . advanceTimersByTime ( 100 ) ;
154+ await sleep ( 60 ) ;
142155
143156 expect ( onRefresh ) . not . toHaveBeenCalled ( ) ;
144157 } ) ;
145158
146- it ( "does not refresh after dispose" , ( ) => {
159+ it ( "does not refresh after dispose" , async ( ) => {
147160 const onRefresh = jest . fn < ( ) => void > ( ) ;
148- const controller = new RefreshController ( { onRefresh, debounceMs : 100 } ) ;
161+ const controller = new RefreshController ( { onRefresh, debounceMs : 50 } ) ;
149162
150163 controller . dispose ( ) ;
151164 controller . schedule ( ) ;
152165 controller . requestImmediate ( ) ;
153166
154- jest . advanceTimersByTime ( 100 ) ;
167+ await sleep ( 60 ) ;
155168
156169 expect ( onRefresh ) . not . toHaveBeenCalled ( ) ;
157170 } ) ;
158171
159- it ( "requestImmediate() bypasses isPaused check (for manual refresh)" , ( ) => {
172+ it ( "requestImmediate() bypasses isPaused check (for manual refresh)" , async ( ) => {
160173 const onRefresh = jest . fn < ( ) => void > ( ) ;
161174 const paused = true ;
162175 const controller = new RefreshController ( {
163176 onRefresh,
164- debounceMs : 100 ,
177+ debounceMs : 50 ,
165178 isPaused : ( ) => paused ,
166179 } ) ;
167180
168181 // schedule() should be blocked by isPaused
169182 controller . schedule ( ) ;
170- jest . advanceTimersByTime ( 100 ) ;
183+ await sleep ( 60 ) ;
171184 expect ( onRefresh ) . not . toHaveBeenCalled ( ) ;
172185
173186 // requestImmediate() should bypass isPaused (manual refresh)
@@ -177,18 +190,18 @@ describe("RefreshController", () => {
177190 controller . dispose ( ) ;
178191 } ) ;
179192
180- it ( "schedule() respects isPaused and flushes on notifyUnpaused" , ( ) => {
193+ it ( "schedule() respects isPaused and flushes on notifyUnpaused" , async ( ) => {
181194 const onRefresh = jest . fn < ( ) => void > ( ) ;
182195 let paused = true ;
183196 const controller = new RefreshController ( {
184197 onRefresh,
185- debounceMs : 100 ,
198+ debounceMs : 50 ,
186199 isPaused : ( ) => paused ,
187200 } ) ;
188201
189202 // schedule() should queue but not execute while paused
190203 controller . schedule ( ) ;
191- jest . advanceTimersByTime ( 100 ) ;
204+ await sleep ( 60 ) ;
192205 expect ( onRefresh ) . not . toHaveBeenCalled ( ) ;
193206
194207 // Unpausing should flush the pending refresh
@@ -199,9 +212,12 @@ describe("RefreshController", () => {
199212 controller . dispose ( ) ;
200213 } ) ;
201214
202- it ( "lastRefreshInfo tracks trigger and timestamp" , ( ) => {
215+ it ( "lastRefreshInfo tracks trigger and timestamp" , async ( ) => {
216+ // This test needs to avoid MIN_REFRESH_INTERVAL_MS; use setSystemTime() to simulate time passing.
217+ setSystemTime ( 0 ) ;
218+
203219 const onRefresh = jest . fn < ( ) => void > ( ) ;
204- const controller = new RefreshController ( { onRefresh, debounceMs : 100 } ) ;
220+ const controller = new RefreshController ( { onRefresh, debounceMs : 20 } ) ;
205221
206222 expect ( controller . lastRefreshInfo ) . toBeNull ( ) ;
207223
@@ -213,37 +229,44 @@ describe("RefreshController", () => {
213229 expect ( controller . lastRefreshInfo ! . timestamp ) . toBeGreaterThanOrEqual ( beforeManual ) ;
214230
215231 // Scheduled refresh should record "scheduled" trigger
232+ setSystemTime ( 1000 ) ;
216233 controller . schedule ( ) ;
217- jest . advanceTimersByTime ( 500 ) ;
234+ await sleep ( 30 ) ;
218235 expect ( controller . lastRefreshInfo ! . trigger ) . toBe ( "scheduled" ) ;
219236
220237 // Priority refresh should record "priority" trigger
238+ setSystemTime ( 2000 ) ;
221239 controller . schedulePriority ( ) ;
222- jest . advanceTimersByTime ( 500 ) ;
240+ await sleep ( 30 ) ;
223241 expect ( controller . lastRefreshInfo ! . trigger ) . toBe ( "priority" ) ;
224242
225243 controller . dispose ( ) ;
226244 } ) ;
227245
228- it ( "onRefreshComplete callback is called with refresh info" , ( ) => {
246+ it ( "onRefreshComplete callback is called with refresh info" , async ( ) => {
247+ // This test needs to avoid MIN_REFRESH_INTERVAL_MS; use setSystemTime() to simulate time passing.
248+ setSystemTime ( 0 ) ;
249+
229250 const onRefresh = jest . fn < ( ) => void > ( ) ;
230251 const onRefreshComplete = jest . fn < ( info : { trigger : string ; timestamp : number } ) => void > ( ) ;
231252 const controller = new RefreshController ( {
232253 onRefresh,
233254 onRefreshComplete,
234- debounceMs : 100 ,
255+ debounceMs : 20 ,
235256 } ) ;
236257
237258 expect ( onRefreshComplete ) . not . toHaveBeenCalled ( ) ;
238259
239260 controller . requestImmediate ( ) ;
240261 expect ( onRefreshComplete ) . toHaveBeenCalledTimes ( 1 ) ;
241262 expect ( onRefreshComplete ) . toHaveBeenCalledWith (
263+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
242264 expect . objectContaining ( { trigger : "manual" , timestamp : expect . any ( Number ) } )
243265 ) ;
244266
267+ setSystemTime ( 1000 ) ;
245268 controller . schedule ( ) ;
246- jest . advanceTimersByTime ( 500 ) ;
269+ await sleep ( 30 ) ;
247270 expect ( onRefreshComplete ) . toHaveBeenCalledTimes ( 2 ) ;
248271 expect ( onRefreshComplete ) . toHaveBeenLastCalledWith (
249272 expect . objectContaining ( { trigger : "scheduled" } )
0 commit comments