@@ -74,7 +74,7 @@ describe("useWithRetry", () => {
74
74
} ) ;
75
75
76
76
expect ( mockFn ) . toHaveBeenCalledTimes ( 1 ) ;
77
- expect ( result . current . isLoading ) . toBe ( true ) ;
77
+ expect ( result . current . isLoading ) . toBe ( false ) ;
78
78
expect ( result . current . retryAt ) . not . toBe ( null ) ;
79
79
80
80
// Fast-forward to first retry (1 second)
@@ -83,7 +83,7 @@ describe("useWithRetry", () => {
83
83
} ) ;
84
84
85
85
expect ( mockFn ) . toHaveBeenCalledTimes ( 2 ) ;
86
- expect ( result . current . isLoading ) . toBe ( true ) ;
86
+ expect ( result . current . isLoading ) . toBe ( false ) ;
87
87
expect ( result . current . retryAt ) . not . toBe ( null ) ;
88
88
89
89
// Fast-forward to second retry (2 seconds)
@@ -96,96 +96,69 @@ describe("useWithRetry", () => {
96
96
expect ( result . current . retryAt ) . toBe ( null ) ;
97
97
} ) ;
98
98
99
- it ( "should stop retrying after max attempts" , async ( ) => {
99
+ it ( "should stop retrying after max attempts (10) " , async ( ) => {
100
100
mockFn . mockRejectedValue ( new Error ( "Always fails" ) ) ;
101
101
102
- const { result } = renderHook ( ( ) =>
103
- useWithRetry ( mockFn , { maxAttempts : 2 } ) ,
104
- ) ;
102
+ const { result } = renderHook ( ( ) => useWithRetry ( mockFn ) ) ;
105
103
106
104
// Start the call
107
105
await act ( async ( ) => {
108
106
await result . current . call ( ) ;
109
107
} ) ;
110
108
111
109
expect ( mockFn ) . toHaveBeenCalledTimes ( 1 ) ;
112
- expect ( result . current . isLoading ) . toBe ( true ) ;
113
-
114
- // Fast-forward to first retry
115
- await act ( async ( ) => {
116
- jest . advanceTimersByTime ( 1000 ) ;
117
- } ) ;
118
-
119
- expect ( mockFn ) . toHaveBeenCalledTimes ( 2 ) ;
120
110
expect ( result . current . isLoading ) . toBe ( false ) ;
121
- expect ( result . current . retryAt ) . toBe ( null ) ;
122
- } ) ;
123
-
124
- it ( "should use custom retry options" , async ( ) => {
125
- mockFn
126
- . mockRejectedValueOnce ( new Error ( "First failure" ) )
127
- . mockResolvedValueOnce ( undefined ) ;
128
-
129
- const { result } = renderHook ( ( ) =>
130
- useWithRetry ( mockFn , {
131
- initialDelay : 500 ,
132
- multiplier : 3 ,
133
- maxAttempts : 2 ,
134
- } ) ,
135
- ) ;
136
-
137
- // Start the call
138
- await act ( async ( ) => {
139
- await result . current . call ( ) ;
140
- } ) ;
141
-
142
- expect ( mockFn ) . toHaveBeenCalledTimes ( 1 ) ;
143
- expect ( result . current . isLoading ) . toBe ( true ) ;
144
111
expect ( result . current . retryAt ) . not . toBe ( null ) ;
145
112
146
- // Fast-forward by custom initial delay (500ms)
147
- await act ( async ( ) => {
148
- jest . advanceTimersByTime ( 500 ) ;
149
- } ) ;
113
+ // Fast-forward through all retries
114
+ for ( let i = 1 ; i < 10 ; i ++ ) {
115
+ const delay = Math . min ( 1000 * 2 ** ( i - 1 ) , 600000 ) ; // exponential backoff with max delay
116
+ await act ( async ( ) => {
117
+ jest . advanceTimersByTime ( delay ) ;
118
+ } ) ;
119
+ expect ( mockFn ) . toHaveBeenCalledTimes ( i + 1 ) ;
120
+ }
150
121
151
- expect ( mockFn ) . toHaveBeenCalledTimes ( 2 ) ;
122
+ // After 10 attempts, should stop retrying
152
123
expect ( result . current . isLoading ) . toBe ( false ) ;
153
124
expect ( result . current . retryAt ) . toBe ( null ) ;
154
125
} ) ;
155
126
156
- it ( "should respect max delay" , async ( ) => {
127
+ it ( "should respect max delay of 10 minutes " , async ( ) => {
157
128
mockFn . mockRejectedValue ( new Error ( "Always fails" ) ) ;
158
129
159
- const { result } = renderHook ( ( ) =>
160
- useWithRetry ( mockFn , {
161
- initialDelay : 1000 ,
162
- multiplier : 10 ,
163
- maxDelay : 2000 ,
164
- maxAttempts : 3 ,
165
- } ) ,
166
- ) ;
130
+ const { result } = renderHook ( ( ) => useWithRetry ( mockFn ) ) ;
167
131
168
132
// Start the call
169
133
await act ( async ( ) => {
170
134
await result . current . call ( ) ;
171
135
} ) ;
172
136
173
- expect ( result . current . isLoading ) . toBe ( true ) ;
174
-
175
- // First retry should be at 1000ms (initial delay)
176
- await act ( async ( ) => {
177
- jest . advanceTimersByTime ( 1000 ) ;
178
- } ) ;
137
+ expect ( result . current . isLoading ) . toBe ( false ) ;
179
138
180
- expect ( mockFn ) . toHaveBeenCalledTimes ( 2 ) ;
139
+ // Fast-forward through several retries to reach max delay
140
+ // After attempt 9, delay would be 1000 * 2^9 = 512000ms, which is less than 600000ms (10 min)
141
+ // After attempt 10, delay would be 1000 * 2^10 = 1024000ms, which should be capped at 600000ms
142
+
143
+ // Skip to attempt 9 (delay calculation: 1000 * 2^8 = 256000ms)
144
+ for ( let i = 1 ; i < 9 ; i ++ ) {
145
+ const delay = 1000 * 2 ** ( i - 1 ) ;
146
+ await act ( async ( ) => {
147
+ jest . advanceTimersByTime ( delay ) ;
148
+ } ) ;
149
+ }
150
+
151
+ expect ( mockFn ) . toHaveBeenCalledTimes ( 9 ) ;
152
+ expect ( result . current . retryAt ) . not . toBe ( null ) ;
181
153
182
- // Second retry should be at 2000ms (max delay, not 10000ms )
154
+ // The 9th retry should use max delay (600000ms = 10 minutes )
183
155
await act ( async ( ) => {
184
- jest . advanceTimersByTime ( 2000 ) ;
156
+ jest . advanceTimersByTime ( 600000 ) ;
185
157
} ) ;
186
158
187
- expect ( mockFn ) . toHaveBeenCalledTimes ( 3 ) ;
159
+ expect ( mockFn ) . toHaveBeenCalledTimes ( 10 ) ;
188
160
expect ( result . current . isLoading ) . toBe ( false ) ;
161
+ expect ( result . current . retryAt ) . toBe ( null ) ;
189
162
} ) ;
190
163
191
164
it ( "should cancel previous retry when call is invoked again" , async ( ) => {
@@ -201,7 +174,7 @@ describe("useWithRetry", () => {
201
174
} ) ;
202
175
203
176
expect ( mockFn ) . toHaveBeenCalledTimes ( 1 ) ;
204
- expect ( result . current . isLoading ) . toBe ( true ) ;
177
+ expect ( result . current . isLoading ) . toBe ( false ) ;
205
178
expect ( result . current . retryAt ) . not . toBe ( null ) ;
206
179
207
180
// Call again before retry happens
@@ -221,61 +194,50 @@ describe("useWithRetry", () => {
221
194
expect ( mockFn ) . toHaveBeenCalledTimes ( 2 ) ; // Should not have been called again
222
195
} ) ;
223
196
224
- it ( "should update retryAt countdown " , async ( ) => {
197
+ it ( "should set retryAt when scheduling retry " , async ( ) => {
225
198
mockFn . mockRejectedValue ( new Error ( "Failure" ) ) ;
226
199
227
- const { result } = renderHook ( ( ) =>
228
- useWithRetry ( mockFn , { initialDelay : 1000 } ) ,
229
- ) ;
200
+ const { result } = renderHook ( ( ) => useWithRetry ( mockFn ) ) ;
230
201
231
202
// Start the call
232
203
await act ( async ( ) => {
233
204
await result . current . call ( ) ;
234
205
} ) ;
235
206
236
- const initialRetryAt = result . current . retryAt ;
237
- expect ( initialRetryAt ) . not . toBe ( null ) ;
238
-
239
- // Advance time by 100ms (countdown update interval)
240
- await act ( async ( ) => {
241
- jest . advanceTimersByTime ( 100 ) ;
242
- } ) ;
243
-
244
- // retryAt should still be set but countdown should be updating
245
- expect ( result . current . retryAt ) . not . toBe ( null ) ;
246
-
247
- // Advance to just before retry time
248
- await act ( async ( ) => {
249
- jest . advanceTimersByTime ( 850 ) ;
250
- } ) ;
207
+ const retryAt = result . current . retryAt ;
208
+ expect ( retryAt ) . not . toBe ( null ) ;
209
+ expect ( retryAt ) . toBeInstanceOf ( Date ) ;
251
210
252
- expect ( result . current . retryAt ) . not . toBe ( null ) ;
211
+ // retryAt should be approximately 1 second in the future
212
+ const expectedTime = Date . now ( ) + 1000 ;
213
+ const actualTime = retryAt ! . getTime ( ) ;
214
+ expect ( Math . abs ( actualTime - expectedTime ) ) . toBeLessThan ( 100 ) ; // Allow 100ms tolerance
253
215
254
216
// Advance past retry time
255
217
await act ( async ( ) => {
256
- jest . advanceTimersByTime ( 100 ) ;
218
+ jest . advanceTimersByTime ( 1000 ) ;
257
219
} ) ;
258
220
259
221
expect ( result . current . retryAt ) . toBe ( null ) ;
260
222
} ) ;
261
223
262
- it ( "should cleanup timers on unmount" , async ( ) => {
224
+ it ( "should cleanup timer on unmount" , async ( ) => {
263
225
mockFn . mockRejectedValue ( new Error ( "Failure" ) ) ;
264
226
265
227
const { result, unmount } = renderHook ( ( ) => useWithRetry ( mockFn ) ) ;
266
228
267
- // Start the call to create timers
229
+ // Start the call to create timer
268
230
await act ( async ( ) => {
269
231
await result . current . call ( ) ;
270
232
} ) ;
271
233
272
- expect ( result . current . isLoading ) . toBe ( true ) ;
234
+ expect ( result . current . isLoading ) . toBe ( false ) ;
273
235
expect ( result . current . retryAt ) . not . toBe ( null ) ;
274
236
275
- // Unmount should cleanup timers
237
+ // Unmount should cleanup timer
276
238
unmount ( ) ;
277
239
278
- // Advance time to ensure timers were cleared
240
+ // Advance time to ensure timer was cleared
279
241
await act ( async ( ) => {
280
242
jest . advanceTimersByTime ( 5000 ) ;
281
243
} ) ;
0 commit comments