1
1
import { fetchWithRetry } from '../src/fetch-retry' ;
2
- import { describe , it , expect , vi , beforeEach } from 'vitest' ;
2
+ import { describe , it , expect , vi , beforeEach , afterEach } from 'vitest' ;
3
3
4
- // Mock the DOM environment
5
4
beforeEach ( ( ) => {
6
5
vi . clearAllMocks ( ) ;
7
6
vi . useFakeTimers ( ) ;
@@ -20,7 +19,9 @@ vi.spyOn(document, 'createElement').mockImplementation(() => {
20
19
return mockScriptElement as unknown as HTMLScriptElement ;
21
20
} ) ;
22
21
23
- vi . spyOn ( document . head , 'appendChild' ) . mockImplementation ( ( ) => { } ) ;
22
+ vi . spyOn ( document . head , 'appendChild' ) . mockImplementation (
23
+ ( ) => mockScriptElement as any ,
24
+ ) ;
24
25
25
26
const mockGlobalFetch = ( mockData ) => {
26
27
const mockFetch = vi . fn ( ) . mockResolvedValueOnce ( mockResponse ( 200 , mockData ) ) ;
@@ -65,7 +66,7 @@ describe('fetchWithRetry', () => {
65
66
const mockData = { success : true } ;
66
67
mockGlobalFetch ( mockData ) ;
67
68
const response = await fetchWithRetry ( {
68
- url : 'https://example.com' ,
69
+ manifestUrl : 'https://example.com' ,
69
70
retryDelay : 0 ,
70
71
} ) ;
71
72
expect ( await response . json ( ) ) . toEqual ( mockData ) ;
@@ -76,7 +77,7 @@ describe('fetchWithRetry', () => {
76
77
mockGlobalFetch ( mockData ) ;
77
78
78
79
const response = await fetchWithRetry ( {
79
- url : 'https://example.com' ,
80
+ manifestUrl : 'https://example.com' ,
80
81
retryDelay : 0 ,
81
82
} ) ;
82
83
@@ -88,7 +89,7 @@ describe('fetchWithRetry', () => {
88
89
mockErrorFetch ( ) ;
89
90
const retryTimes = 3 ;
90
91
const responsePromise = fetchWithRetry ( {
91
- url : 'https://example.com' ,
92
+ manifestUrl : 'https://example.com' ,
92
93
retryTimes,
93
94
retryDelay : 0 ,
94
95
} ) ;
@@ -97,14 +98,14 @@ describe('fetchWithRetry', () => {
97
98
await expect ( responsePromise ) . rejects . toThrow (
98
99
'The request failed three times and has now been abandoned' ,
99
100
) ;
100
- expect ( fetch ) . toHaveBeenCalledTimes ( 4 ) ; //first fetch + retryTimes fetch
101
+ expect ( fetch ) . toHaveBeenCalledTimes ( 4 ) ;
101
102
} ) ;
102
103
103
104
it ( 'should fall back to the fallback URL after retries fail' , async ( ) => {
104
105
mockErrorFetch ( ) ;
105
106
const retryTimes = 3 ;
106
107
const responsePromise = fetchWithRetry ( {
107
- url : 'https://example.com' ,
108
+ manifestUrl : 'https://example.com' ,
108
109
retryTimes,
109
110
retryDelay : 0 ,
110
111
fallback : ( ) => 'https://fallback.com' ,
@@ -114,15 +115,15 @@ describe('fetchWithRetry', () => {
114
115
await expect ( responsePromise ) . rejects . toThrow (
115
116
'The request failed three times and has now been abandoned' ,
116
117
) ;
117
- expect ( fetch ) . toHaveBeenCalledTimes ( 5 ) ; //first fetch + retryTimes fetch
118
+ expect ( fetch ) . toHaveBeenCalledTimes ( 5 ) ;
118
119
expect ( fetch ) . toHaveBeenLastCalledWith ( 'https://fallback.com' , { } ) ;
119
120
} ) ;
120
121
121
122
it ( 'should build fallback URL from remote after retries fail' , async ( ) => {
122
123
mockErrorFetch ( ) ;
123
124
const retryTimes = 3 ;
124
125
const responsePromise = fetchWithRetry ( {
125
- url : 'https://example.com' ,
126
+ manifestUrl : 'https://example.com' ,
126
127
retryTimes,
127
128
retryDelay : 0 ,
128
129
fallback : ( url ) => `${ url } /fallback` ,
@@ -132,7 +133,7 @@ describe('fetchWithRetry', () => {
132
133
await expect ( responsePromise ) . rejects . toThrow (
133
134
'The request failed three times and has now been abandoned' ,
134
135
) ;
135
- expect ( fetch ) . toHaveBeenCalledTimes ( 5 ) ; //first fetch + retryTimes fetch
136
+ expect ( fetch ) . toHaveBeenCalledTimes ( 5 ) ;
136
137
expect ( fetch ) . toHaveBeenLastCalledWith ( 'https://example.com/fallback' , { } ) ;
137
138
} ) ;
138
139
@@ -148,28 +149,180 @@ describe('fetchWithRetry', () => {
148
149
global . fetch = mockFetch ;
149
150
await expect (
150
151
fetchWithRetry ( {
151
- url : 'https://example.com' ,
152
+ manifestUrl : 'https://example.com' ,
152
153
retryTimes : 0 ,
153
154
retryDelay : 0 ,
154
155
} ) ,
155
156
) . rejects . toThrow ( 'Json parse error' ) ;
156
157
} ) ;
157
158
158
- it ( 'should build fallback URL from remote after retries fail' , async ( ) => {
159
- mockErrorFetch ( ) ;
160
- const retryTimes = 3 ;
161
- const responsePromise = fetchWithRetry ( {
162
- url : 'https://example.com' ,
163
- retryTimes,
164
- retryDelay : 0 ,
165
- fallback : ( url ) => `${ url } /fallback` ,
159
+ describe ( 'getRetryPath functionality' , ( ) => {
160
+ it ( 'should use original URL for first attempt and getRetryPath for retries' , async ( ) => {
161
+ const mockFetch = vi
162
+ . fn ( )
163
+ . mockRejectedValueOnce ( new Error ( 'Network error' ) )
164
+ . mockResolvedValueOnce ( mockResponse ( 200 , { success : true } ) ) ;
165
+
166
+ global . fetch = mockFetch ;
167
+ const getRetryPath = vi . fn ( ) . mockReturnValue ( 'https://retry.example.com' ) ;
168
+
169
+ const response = await fetchWithRetry ( {
170
+ manifestUrl : 'https://example.com' ,
171
+ retryTimes : 3 ,
172
+ retryDelay : 0 ,
173
+ getRetryPath,
174
+ } ) ;
175
+
176
+ expect ( fetch ) . toHaveBeenCalledTimes ( 2 ) ;
177
+ expect ( fetch ) . toHaveBeenNthCalledWith ( 1 , 'https://example.com' , { } ) ;
178
+ expect ( fetch ) . toHaveBeenNthCalledWith ( 2 , 'https://retry.example.com' , { } ) ;
179
+ expect ( getRetryPath ) . toHaveBeenCalledWith ( 'https://example.com' ) ;
166
180
} ) ;
167
- vi . advanceTimersByTime ( 2000 * retryTimes ) ;
168
181
169
- await expect ( responsePromise ) . rejects . toThrow (
170
- 'The request failed three times and has now been abandoned' ,
171
- ) ;
172
- expect ( fetch ) . toHaveBeenCalledTimes ( 5 ) ; //first fetch + retryTimes fetch
173
- expect ( fetch ) . toHaveBeenLastCalledWith ( 'https://example.com/fallback' , { } ) ;
182
+ it ( 'should not call getRetryPath on first attempt' , async ( ) => {
183
+ const mockData = { success : true } ;
184
+ mockGlobalFetch ( mockData ) ;
185
+ const getRetryPath = vi . fn ( ) . mockReturnValue ( 'https://retry.example.com' ) ;
186
+ const response = await fetchWithRetry ( {
187
+ manifestUrl : 'https://example.com' ,
188
+ retryDelay : 0 ,
189
+ getRetryPath,
190
+ } ) ;
191
+
192
+ expect ( fetch ) . toHaveBeenCalledTimes ( 1 ) ;
193
+ expect ( fetch ) . toHaveBeenCalledWith ( 'https://example.com' , { } ) ;
194
+ expect ( getRetryPath ) . not . toHaveBeenCalled ( ) ;
195
+ } ) ;
196
+
197
+ it ( 'should use getRetryPath for all retry attempts' , async ( ) => {
198
+ const mockFetch = vi
199
+ . fn ( )
200
+ . mockRejectedValueOnce ( new Error ( 'Network error' ) )
201
+ . mockRejectedValueOnce ( new Error ( 'Network error' ) )
202
+ . mockRejectedValueOnce ( new Error ( 'Network error' ) )
203
+ . mockResolvedValueOnce ( mockResponse ( 200 , { success : true } ) ) ;
204
+
205
+ global . fetch = mockFetch ;
206
+ const getRetryPath = vi . fn ( ) . mockReturnValue ( 'https://retry.example.com' ) ;
207
+ const response = await fetchWithRetry ( {
208
+ manifestUrl : 'https://example.com' ,
209
+ retryTimes : 3 ,
210
+ retryDelay : 0 ,
211
+ getRetryPath,
212
+ } ) ;
213
+
214
+ expect ( fetch ) . toHaveBeenCalledTimes ( 4 ) ;
215
+ expect ( fetch ) . toHaveBeenNthCalledWith ( 1 , 'https://example.com' , { } ) ;
216
+ expect ( fetch ) . toHaveBeenNthCalledWith ( 2 , 'https://retry.example.com' , { } ) ;
217
+ expect ( fetch ) . toHaveBeenNthCalledWith ( 3 , 'https://retry.example.com' , { } ) ;
218
+ expect ( fetch ) . toHaveBeenNthCalledWith ( 4 , 'https://retry.example.com' , { } ) ;
219
+ expect ( getRetryPath ) . toHaveBeenCalledTimes ( 3 ) ;
220
+ } ) ;
221
+
222
+ it ( 'should handle getRetryPath returning different URLs for each retry' , async ( ) => {
223
+ const mockFetch = vi
224
+ . fn ( )
225
+ . mockRejectedValueOnce ( new Error ( 'Network error' ) )
226
+ . mockRejectedValueOnce ( new Error ( 'Network error' ) )
227
+ . mockResolvedValueOnce ( mockResponse ( 200 , { success : true } ) ) ;
228
+
229
+ global . fetch = mockFetch ;
230
+
231
+ const getRetryPath = vi
232
+ . fn ( )
233
+ . mockReturnValueOnce ( 'https://retry1.example.com' )
234
+ . mockReturnValueOnce ( 'https://retry2.example.com' ) ;
235
+
236
+ const response = await fetchWithRetry ( {
237
+ manifestUrl : 'https://example.com' ,
238
+ retryTimes : 2 ,
239
+ retryDelay : 0 ,
240
+ getRetryPath,
241
+ } ) ;
242
+
243
+ expect ( fetch ) . toHaveBeenCalledTimes ( 3 ) ;
244
+ expect ( fetch ) . toHaveBeenNthCalledWith ( 1 , 'https://example.com' , { } ) ;
245
+ expect ( fetch ) . toHaveBeenNthCalledWith (
246
+ 2 ,
247
+ 'https://retry1.example.com' ,
248
+ { } ,
249
+ ) ;
250
+ expect ( fetch ) . toHaveBeenNthCalledWith (
251
+ 3 ,
252
+ 'https://retry2.example.com' ,
253
+ { } ,
254
+ ) ;
255
+ expect ( getRetryPath ) . toHaveBeenCalledTimes ( 2 ) ;
256
+ expect ( getRetryPath ) . toHaveBeenNthCalledWith ( 1 , 'https://example.com' ) ;
257
+ expect ( getRetryPath ) . toHaveBeenNthCalledWith ( 2 , 'https://example.com' ) ;
258
+ } ) ;
259
+
260
+ it ( 'should handle getRetryPath returning undefined or null' , async ( ) => {
261
+ const mockFetch = vi
262
+ . fn ( )
263
+ . mockRejectedValueOnce ( new Error ( 'Network error' ) )
264
+ . mockResolvedValueOnce ( mockResponse ( 200 , { success : true } ) ) ;
265
+
266
+ global . fetch = mockFetch ;
267
+ const getRetryPath = vi . fn ( ) . mockReturnValue ( undefined ) ;
268
+ const response = await fetchWithRetry ( {
269
+ manifestUrl : 'https://example.com' ,
270
+ retryTimes : 1 ,
271
+ retryDelay : 0 ,
272
+ getRetryPath,
273
+ } ) ;
274
+
275
+ expect ( fetch ) . toHaveBeenCalledTimes ( 2 ) ;
276
+ expect ( fetch ) . toHaveBeenNthCalledWith ( 1 , 'https://example.com' , { } ) ;
277
+ expect ( fetch ) . toHaveBeenNthCalledWith ( 2 , 'https://example.com' , { } ) ;
278
+ expect ( getRetryPath ) . toHaveBeenCalledWith ( 'https://example.com' ) ;
279
+ } ) ;
280
+ } ) ;
281
+
282
+ describe ( 'retry count and timing' , ( ) => {
283
+ it ( 'should execute exactly retryTimes + 1 attempts when retryTimes = 3' , async ( ) => {
284
+ const mockFetch = vi
285
+ . fn ( )
286
+ . mockRejectedValueOnce ( new Error ( 'Network error' ) )
287
+ . mockRejectedValueOnce ( new Error ( 'Network error' ) )
288
+ . mockRejectedValueOnce ( new Error ( 'Network error' ) )
289
+ . mockRejectedValueOnce ( new Error ( 'Network error' ) ) ;
290
+
291
+ global . fetch = mockFetch ;
292
+
293
+ const responsePromise = fetchWithRetry ( {
294
+ manifestUrl : 'https://example.com' ,
295
+ retryTimes : 3 ,
296
+ retryDelay : 0 ,
297
+ } ) ;
298
+
299
+ vi . advanceTimersByTime ( 1000 ) ;
300
+
301
+ await expect ( responsePromise ) . rejects . toThrow (
302
+ 'The request failed three times and has now been abandoned' ,
303
+ ) ;
304
+ expect ( fetch ) . toHaveBeenCalledTimes ( 4 ) ;
305
+ } ) ;
306
+
307
+ it . skip ( 'should respect retryDelay timing' , async ( ) => {
308
+ const mockFetch = vi
309
+ . fn ( )
310
+ . mockRejectedValueOnce ( new Error ( 'Network error' ) )
311
+ . mockResolvedValueOnce ( mockResponse ( 200 , { success : true } ) ) ;
312
+
313
+ global . fetch = mockFetch ;
314
+
315
+ const response = await fetchWithRetry ( {
316
+ manifestUrl : 'https://example.com' ,
317
+ retryTimes : 1 ,
318
+ retryDelay : 100 ,
319
+ } ) ;
320
+
321
+ expect ( fetch ) . toHaveBeenCalledTimes ( 2 ) ;
322
+ expect ( fetch ) . toHaveBeenNthCalledWith ( 1 , 'https://example.com' , { } ) ;
323
+ expect ( fetch ) . toHaveBeenNthCalledWith ( 2 , 'https://example.com' , { } ) ;
324
+
325
+ expect ( response ) . toBeDefined ( ) ;
326
+ } , 10000 ) ;
174
327
} ) ;
175
328
} ) ;
0 commit comments