@uploadista/client-core 0.0.20-beta.6 → 0.0.20-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +909 -909
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/client/__tests__/flow-inputs.test.ts +43 -11
- package/src/client/create-uploadista-client.ts +27 -13
- package/src/index.ts +2 -2
- package/src/managers/__tests__/upload-manager.test.ts +557 -545
- package/src/testing/index.ts +14 -14
- package/src/testing/mock-service-container.ts +4 -5
- package/src/types/index.ts +2 -2
- package/src/upload/index.ts +1 -1
- package/src/utils/__tests__/flow-inputs-builder.test.ts +5 -8
- package/src/utils/__tests__/input-validation.test.ts +1 -1
- package/src/utils/flow-inputs-builder.ts +2 -1
- package/src/utils/index.ts +1 -1
|
@@ -1,588 +1,600 @@
|
|
|
1
1
|
import type { UploadFile } from "@uploadista/core/types";
|
|
2
2
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
type UploadAbortController,
|
|
5
|
+
type UploadFunction,
|
|
6
|
+
UploadManager,
|
|
7
|
+
type UploadManagerCallbacks,
|
|
8
|
+
type UploadOptions,
|
|
9
|
+
type UploadState,
|
|
10
10
|
} from "../upload-manager";
|
|
11
11
|
|
|
12
12
|
describe("UploadManager", () => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
13
|
+
let mockUploadFn: ReturnType<typeof vi.fn<UploadFunction>>;
|
|
14
|
+
let mockCallbacks: UploadManagerCallbacks;
|
|
15
|
+
let mockAbortController: UploadAbortController;
|
|
16
|
+
let stateChanges: UploadState[];
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
stateChanges = [];
|
|
20
|
+
|
|
21
|
+
mockAbortController = {
|
|
22
|
+
abort: vi.fn(),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
mockUploadFn = vi
|
|
26
|
+
.fn<UploadFunction>()
|
|
27
|
+
.mockResolvedValue(mockAbortController);
|
|
28
|
+
|
|
29
|
+
mockCallbacks = {
|
|
30
|
+
onStateChange: vi.fn((state) => stateChanges.push({ ...state })),
|
|
31
|
+
onProgress: vi.fn(),
|
|
32
|
+
onChunkComplete: vi.fn(),
|
|
33
|
+
onSuccess: vi.fn(),
|
|
34
|
+
onError: vi.fn(),
|
|
35
|
+
onAbort: vi.fn(),
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("constructor", () => {
|
|
40
|
+
it("should initialize with idle state", () => {
|
|
41
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
42
|
+
const state = manager.getState();
|
|
43
|
+
|
|
44
|
+
expect(state).toEqual({
|
|
45
|
+
status: "idle",
|
|
46
|
+
progress: 0,
|
|
47
|
+
bytesUploaded: 0,
|
|
48
|
+
totalBytes: null,
|
|
49
|
+
error: null,
|
|
50
|
+
result: null,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should not call onStateChange during initialization", () => {
|
|
55
|
+
new UploadManager(mockUploadFn, mockCallbacks);
|
|
56
|
+
expect(mockCallbacks.onStateChange).not.toHaveBeenCalled();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("getState", () => {
|
|
61
|
+
it("should return a copy of the current state", () => {
|
|
62
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
63
|
+
const state1 = manager.getState();
|
|
64
|
+
const state2 = manager.getState();
|
|
65
|
+
|
|
66
|
+
expect(state1).toEqual(state2);
|
|
67
|
+
expect(state1).not.toBe(state2); // Different objects
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("isUploading", () => {
|
|
72
|
+
it("should return false when idle", () => {
|
|
73
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
74
|
+
expect(manager.isUploading()).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should return true when uploading", async () => {
|
|
78
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
79
|
+
const uploadPromise = manager.upload({ size: 1000 });
|
|
80
|
+
|
|
81
|
+
expect(manager.isUploading()).toBe(true);
|
|
82
|
+
await uploadPromise;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("should return false after upload completes", async () => {
|
|
86
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
87
|
+
const uploadPromise = manager.upload({ size: 1000 });
|
|
88
|
+
|
|
89
|
+
// Simulate success
|
|
90
|
+
const uploadOptions = mockUploadFn.mock.calls[0][1];
|
|
91
|
+
uploadOptions.onSuccess?.({
|
|
92
|
+
id: "file-1",
|
|
93
|
+
offset: 1000,
|
|
94
|
+
storage: "s3",
|
|
95
|
+
} as UploadFile);
|
|
96
|
+
|
|
97
|
+
await uploadPromise;
|
|
98
|
+
expect(manager.isUploading()).toBe(false);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("canRetry", () => {
|
|
103
|
+
it("should return false when idle", () => {
|
|
104
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
105
|
+
expect(manager.canRetry()).toBe(false);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should return false while uploading", async () => {
|
|
109
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
110
|
+
const uploadPromise = manager.upload({ size: 1000 });
|
|
111
|
+
|
|
112
|
+
expect(manager.canRetry()).toBe(false);
|
|
113
|
+
await uploadPromise;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should return true after error", async () => {
|
|
117
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
118
|
+
const uploadPromise = manager.upload({ size: 1000 });
|
|
119
|
+
|
|
120
|
+
// Simulate error
|
|
121
|
+
const uploadOptions = mockUploadFn.mock.calls[0][1];
|
|
122
|
+
uploadOptions.onError?.(new Error("Upload failed"));
|
|
123
|
+
|
|
124
|
+
await uploadPromise;
|
|
125
|
+
expect(manager.canRetry()).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("should return true after abort", async () => {
|
|
129
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
130
|
+
const uploadPromise = manager.upload({ size: 1000 });
|
|
131
|
+
|
|
132
|
+
// Simulate abort
|
|
133
|
+
const uploadOptions = mockUploadFn.mock.calls[0][1];
|
|
134
|
+
uploadOptions.onAbort?.();
|
|
135
|
+
|
|
136
|
+
await uploadPromise;
|
|
137
|
+
expect(manager.canRetry()).toBe(true);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("should return false after successful upload", async () => {
|
|
141
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
142
|
+
const uploadPromise = manager.upload({ size: 1000 });
|
|
143
|
+
|
|
144
|
+
// Simulate success
|
|
145
|
+
const uploadOptions = mockUploadFn.mock.calls[0][1];
|
|
146
|
+
uploadOptions.onSuccess?.({
|
|
147
|
+
id: "file-1",
|
|
148
|
+
offset: 1000,
|
|
149
|
+
storage: "s3",
|
|
150
|
+
} as UploadFile);
|
|
151
|
+
|
|
152
|
+
await uploadPromise;
|
|
153
|
+
expect(manager.canRetry()).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("should return false after reset", async () => {
|
|
157
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
158
|
+
const uploadPromise = manager.upload({ size: 1000 });
|
|
159
|
+
|
|
160
|
+
// Simulate error
|
|
161
|
+
const uploadOptions = mockUploadFn.mock.calls[0][1];
|
|
162
|
+
uploadOptions.onError?.(new Error("Upload failed"));
|
|
163
|
+
|
|
164
|
+
await uploadPromise;
|
|
165
|
+
expect(manager.canRetry()).toBe(true);
|
|
166
|
+
|
|
167
|
+
manager.reset();
|
|
168
|
+
expect(manager.canRetry()).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("upload", () => {
|
|
173
|
+
it("should update state to uploading", async () => {
|
|
174
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
175
|
+
await manager.upload({ size: 1000 });
|
|
176
|
+
|
|
177
|
+
expect(stateChanges[0]).toMatchObject({
|
|
178
|
+
status: "uploading",
|
|
179
|
+
progress: 0,
|
|
180
|
+
bytesUploaded: 0,
|
|
181
|
+
totalBytes: 1000,
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("should extract totalBytes from File/Blob-like input", async () => {
|
|
186
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
187
|
+
await manager.upload({ size: 5000 });
|
|
188
|
+
|
|
189
|
+
expect(stateChanges[0].totalBytes).toBe(5000);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("should call uploadFn with input and options", async () => {
|
|
193
|
+
const options: UploadOptions = {
|
|
194
|
+
metadata: { key: "value" },
|
|
195
|
+
};
|
|
196
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks, options);
|
|
197
|
+
const input = { size: 1000 };
|
|
198
|
+
|
|
199
|
+
await manager.upload(input);
|
|
200
|
+
|
|
201
|
+
expect(mockUploadFn).toHaveBeenCalledWith(input, expect.any(Object));
|
|
202
|
+
const uploadOptions = mockUploadFn.mock.calls[0][1];
|
|
203
|
+
expect(uploadOptions.metadata).toEqual({ key: "value" });
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should handle successful upload", async () => {
|
|
207
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
208
|
+
const uploadPromise = manager.upload({ size: 1000 });
|
|
209
|
+
|
|
210
|
+
// Simulate success
|
|
211
|
+
const uploadOptions = mockUploadFn.mock.calls[0][1];
|
|
212
|
+
const result: UploadFile = {
|
|
213
|
+
id: "file-1",
|
|
214
|
+
offset: 1000,
|
|
215
|
+
storage: "s3",
|
|
216
|
+
size: 1000,
|
|
217
|
+
};
|
|
218
|
+
uploadOptions.onSuccess?.(result);
|
|
219
|
+
|
|
220
|
+
await uploadPromise;
|
|
221
|
+
|
|
222
|
+
expect(mockCallbacks.onSuccess).toHaveBeenCalledWith(result);
|
|
223
|
+
expect(stateChanges).toContainEqual(
|
|
224
|
+
expect.objectContaining({
|
|
225
|
+
status: "success",
|
|
226
|
+
result,
|
|
227
|
+
progress: 100,
|
|
228
|
+
bytesUploaded: 1000,
|
|
229
|
+
}),
|
|
230
|
+
);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should handle upload error", async () => {
|
|
234
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
235
|
+
const uploadPromise = manager.upload({ size: 1000 });
|
|
236
|
+
|
|
237
|
+
// Simulate error
|
|
238
|
+
const uploadOptions = mockUploadFn.mock.calls[0][1];
|
|
239
|
+
const error = new Error("Upload failed");
|
|
240
|
+
uploadOptions.onError?.(error);
|
|
241
|
+
|
|
242
|
+
await uploadPromise;
|
|
243
|
+
|
|
244
|
+
expect(mockCallbacks.onError).toHaveBeenCalledWith(error);
|
|
245
|
+
expect(stateChanges).toContainEqual(
|
|
246
|
+
expect.objectContaining({
|
|
247
|
+
status: "error",
|
|
248
|
+
error,
|
|
249
|
+
}),
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("should handle upload abort", async () => {
|
|
254
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
255
|
+
const uploadPromise = manager.upload({ size: 1000 });
|
|
256
|
+
|
|
257
|
+
// Simulate abort
|
|
258
|
+
const uploadOptions = mockUploadFn.mock.calls[0][1];
|
|
259
|
+
uploadOptions.onAbort?.();
|
|
260
|
+
|
|
261
|
+
await uploadPromise;
|
|
262
|
+
|
|
263
|
+
expect(mockCallbacks.onAbort).toHaveBeenCalled();
|
|
264
|
+
expect(stateChanges).toContainEqual(
|
|
265
|
+
expect.objectContaining({
|
|
266
|
+
status: "aborted",
|
|
267
|
+
}),
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("should track progress updates", async () => {
|
|
272
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
273
|
+
const uploadPromise = manager.upload({ size: 1000 });
|
|
274
|
+
|
|
275
|
+
// Simulate progress
|
|
276
|
+
const uploadOptions = mockUploadFn.mock.calls[0][1];
|
|
277
|
+
uploadOptions.onProgress?.(0, 0, 1000);
|
|
278
|
+
uploadOptions.onProgress?.(500, 500, 1000);
|
|
279
|
+
uploadOptions.onProgress?.(1000, 1000, 1000);
|
|
280
|
+
|
|
281
|
+
await uploadPromise;
|
|
282
|
+
|
|
283
|
+
expect(mockCallbacks.onProgress).toHaveBeenCalledTimes(3);
|
|
284
|
+
expect(mockCallbacks.onProgress).toHaveBeenNthCalledWith(1, 0, 0, 1000);
|
|
285
|
+
expect(mockCallbacks.onProgress).toHaveBeenNthCalledWith(
|
|
286
|
+
2,
|
|
287
|
+
50,
|
|
288
|
+
500,
|
|
289
|
+
1000,
|
|
290
|
+
);
|
|
291
|
+
expect(mockCallbacks.onProgress).toHaveBeenNthCalledWith(
|
|
292
|
+
3,
|
|
293
|
+
100,
|
|
294
|
+
1000,
|
|
295
|
+
1000,
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("should handle chunk completion", async () => {
|
|
300
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
301
|
+
const uploadPromise = manager.upload({ size: 1000 });
|
|
302
|
+
|
|
303
|
+
// Simulate chunk completion
|
|
304
|
+
const uploadOptions = mockUploadFn.mock.calls[0][1];
|
|
305
|
+
uploadOptions.onChunkComplete?.(256, 256, 1000);
|
|
306
|
+
uploadOptions.onChunkComplete?.(256, 512, 1000);
|
|
307
|
+
|
|
308
|
+
await uploadPromise;
|
|
309
|
+
|
|
310
|
+
expect(mockCallbacks.onChunkComplete).toHaveBeenCalledTimes(2);
|
|
311
|
+
expect(mockCallbacks.onChunkComplete).toHaveBeenNthCalledWith(
|
|
312
|
+
1,
|
|
313
|
+
256,
|
|
314
|
+
256,
|
|
315
|
+
1000,
|
|
316
|
+
);
|
|
317
|
+
expect(mockCallbacks.onChunkComplete).toHaveBeenNthCalledWith(
|
|
318
|
+
2,
|
|
319
|
+
256,
|
|
320
|
+
512,
|
|
321
|
+
1000,
|
|
322
|
+
);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it("should handle uploadFn throwing error", async () => {
|
|
326
|
+
mockUploadFn.mockRejectedValue(new Error("Connection failed"));
|
|
327
|
+
|
|
328
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
329
|
+
await manager.upload({ size: 1000 });
|
|
330
|
+
|
|
331
|
+
expect(mockCallbacks.onError).toHaveBeenCalledWith(
|
|
332
|
+
expect.objectContaining({
|
|
333
|
+
message: "Connection failed",
|
|
334
|
+
}),
|
|
335
|
+
);
|
|
336
|
+
expect(stateChanges).toContainEqual(
|
|
337
|
+
expect.objectContaining({
|
|
338
|
+
status: "error",
|
|
339
|
+
error: expect.any(Error),
|
|
340
|
+
}),
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("should handle non-Error exceptions", async () => {
|
|
345
|
+
mockUploadFn.mockRejectedValue("Network timeout");
|
|
346
|
+
|
|
347
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
348
|
+
await manager.upload({ size: 1000 });
|
|
349
|
+
|
|
350
|
+
expect(mockCallbacks.onError).toHaveBeenCalledWith(
|
|
351
|
+
expect.objectContaining({
|
|
352
|
+
message: "Network timeout",
|
|
353
|
+
}),
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("should invoke both manager and options callbacks", async () => {
|
|
358
|
+
const optionCallbacks: Partial<UploadOptions> = {
|
|
359
|
+
onProgress: vi.fn(),
|
|
360
|
+
onSuccess: vi.fn(),
|
|
361
|
+
onChunkComplete: vi.fn(),
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const manager = new UploadManager(
|
|
365
|
+
mockUploadFn,
|
|
366
|
+
mockCallbacks,
|
|
367
|
+
optionCallbacks,
|
|
368
|
+
);
|
|
369
|
+
const uploadPromise = manager.upload({ size: 1000 });
|
|
370
|
+
|
|
371
|
+
// Simulate callbacks
|
|
372
|
+
const uploadOptions = mockUploadFn.mock.calls[0][1];
|
|
373
|
+
uploadOptions.onProgress?.(500, 500, 1000);
|
|
374
|
+
uploadOptions.onChunkComplete?.(500, 500, 1000);
|
|
375
|
+
uploadOptions.onSuccess?.({
|
|
376
|
+
id: "file-1",
|
|
377
|
+
offset: 1000,
|
|
378
|
+
storage: "s3",
|
|
379
|
+
} as UploadFile);
|
|
380
|
+
|
|
381
|
+
await uploadPromise;
|
|
382
|
+
|
|
383
|
+
// Both sets of callbacks should be called
|
|
384
|
+
expect(mockCallbacks.onProgress).toHaveBeenCalled();
|
|
385
|
+
expect(optionCallbacks.onProgress).toHaveBeenCalled();
|
|
386
|
+
expect(mockCallbacks.onChunkComplete).toHaveBeenCalled();
|
|
387
|
+
expect(optionCallbacks.onChunkComplete).toHaveBeenCalled();
|
|
388
|
+
expect(mockCallbacks.onSuccess).toHaveBeenCalled();
|
|
389
|
+
expect(optionCallbacks.onSuccess).toHaveBeenCalled();
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe("abort", () => {
|
|
394
|
+
it("should call abort on abort controller", async () => {
|
|
395
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
396
|
+
await manager.upload({ size: 1000 });
|
|
397
|
+
|
|
398
|
+
manager.abort();
|
|
399
|
+
|
|
400
|
+
expect(mockAbortController.abort).toHaveBeenCalled();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("should do nothing if no upload is active", () => {
|
|
404
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
405
|
+
|
|
406
|
+
expect(() => manager.abort()).not.toThrow();
|
|
407
|
+
expect(mockAbortController.abort).not.toHaveBeenCalled();
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe("reset", () => {
|
|
412
|
+
it("should reset state to idle", async () => {
|
|
413
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
414
|
+
const uploadPromise = manager.upload({ size: 1000 });
|
|
415
|
+
|
|
416
|
+
// Simulate error
|
|
417
|
+
const uploadOptions = mockUploadFn.mock.calls[0][1];
|
|
418
|
+
uploadOptions.onError?.(new Error("Upload failed"));
|
|
419
|
+
|
|
420
|
+
await uploadPromise;
|
|
421
|
+
|
|
422
|
+
// Reset
|
|
423
|
+
stateChanges = [];
|
|
424
|
+
manager.reset();
|
|
425
|
+
|
|
426
|
+
expect(stateChanges[0]).toEqual({
|
|
427
|
+
status: "idle",
|
|
428
|
+
progress: 0,
|
|
429
|
+
bytesUploaded: 0,
|
|
430
|
+
totalBytes: null,
|
|
431
|
+
error: null,
|
|
432
|
+
result: null,
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("should abort active upload before resetting", async () => {
|
|
437
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
438
|
+
await manager.upload({ size: 1000 });
|
|
439
|
+
|
|
440
|
+
manager.reset();
|
|
441
|
+
|
|
442
|
+
expect(mockAbortController.abort).toHaveBeenCalled();
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("should clear lastInput so retry is not possible", async () => {
|
|
446
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
447
|
+
const uploadPromise = manager.upload({ size: 1000 });
|
|
448
|
+
|
|
449
|
+
const uploadOptions = mockUploadFn.mock.calls[0][1];
|
|
450
|
+
uploadOptions.onError?.(new Error("Upload failed"));
|
|
451
|
+
|
|
452
|
+
await uploadPromise;
|
|
453
|
+
expect(manager.canRetry()).toBe(true);
|
|
454
|
+
|
|
455
|
+
manager.reset();
|
|
456
|
+
expect(manager.canRetry()).toBe(false);
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
describe("retry", () => {
|
|
461
|
+
it("should retry with same input after error", async () => {
|
|
462
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
463
|
+
const input = { size: 1000 };
|
|
464
|
+
|
|
465
|
+
// First upload fails
|
|
466
|
+
const uploadPromise = manager.upload(input);
|
|
467
|
+
const uploadOptions = mockUploadFn.mock.calls[0][1];
|
|
468
|
+
uploadOptions.onError?.(new Error("Upload failed"));
|
|
469
|
+
await uploadPromise;
|
|
458
470
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
471
|
+
// Retry
|
|
472
|
+
mockUploadFn.mockClear();
|
|
473
|
+
manager.retry();
|
|
462
474
|
|
|
463
|
-
|
|
464
|
-
|
|
475
|
+
expect(mockUploadFn).toHaveBeenCalledWith(input, expect.any(Object));
|
|
476
|
+
});
|
|
465
477
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
478
|
+
it("should retry with same input after abort", async () => {
|
|
479
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
480
|
+
const input = { size: 1000 };
|
|
469
481
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
482
|
+
// First upload aborted
|
|
483
|
+
const uploadPromise = manager.upload(input);
|
|
484
|
+
const uploadOptions = mockUploadFn.mock.calls[0][1];
|
|
485
|
+
uploadOptions.onAbort?.();
|
|
486
|
+
await uploadPromise;
|
|
475
487
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
488
|
+
// Retry
|
|
489
|
+
mockUploadFn.mockClear();
|
|
490
|
+
manager.retry();
|
|
479
491
|
|
|
480
|
-
|
|
481
|
-
|
|
492
|
+
expect(mockUploadFn).toHaveBeenCalledWith(input, expect.any(Object));
|
|
493
|
+
});
|
|
482
494
|
|
|
483
|
-
|
|
484
|
-
|
|
495
|
+
it("should do nothing if canRetry is false", () => {
|
|
496
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
485
497
|
|
|
486
|
-
|
|
498
|
+
manager.retry();
|
|
487
499
|
|
|
488
|
-
|
|
489
|
-
|
|
500
|
+
expect(mockUploadFn).not.toHaveBeenCalled();
|
|
501
|
+
});
|
|
490
502
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
503
|
+
it("should do nothing after successful upload", async () => {
|
|
504
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
505
|
+
const uploadPromise = manager.upload({ size: 1000 });
|
|
494
506
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
507
|
+
const uploadOptions = mockUploadFn.mock.calls[0][1];
|
|
508
|
+
uploadOptions.onSuccess?.({
|
|
509
|
+
id: "file-1",
|
|
510
|
+
offset: 1000,
|
|
511
|
+
storage: "s3",
|
|
512
|
+
} as UploadFile);
|
|
501
513
|
|
|
502
|
-
|
|
514
|
+
await uploadPromise;
|
|
503
515
|
|
|
504
|
-
|
|
505
|
-
|
|
516
|
+
mockUploadFn.mockClear();
|
|
517
|
+
manager.retry();
|
|
506
518
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
519
|
+
expect(mockUploadFn).not.toHaveBeenCalled();
|
|
520
|
+
});
|
|
521
|
+
});
|
|
510
522
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
523
|
+
describe("cleanup", () => {
|
|
524
|
+
it("should abort active upload", async () => {
|
|
525
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
526
|
+
await manager.upload({ size: 1000 });
|
|
515
527
|
|
|
516
|
-
|
|
528
|
+
manager.cleanup();
|
|
517
529
|
|
|
518
|
-
|
|
519
|
-
|
|
530
|
+
expect(mockAbortController.abort).toHaveBeenCalled();
|
|
531
|
+
});
|
|
520
532
|
|
|
521
|
-
|
|
522
|
-
|
|
533
|
+
it("should do nothing if no upload is active", () => {
|
|
534
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
523
535
|
|
|
524
|
-
|
|
525
|
-
|
|
536
|
+
expect(() => manager.cleanup()).not.toThrow();
|
|
537
|
+
});
|
|
526
538
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
539
|
+
it("should allow creating new upload after cleanup", async () => {
|
|
540
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
541
|
+
await manager.upload({ size: 1000 });
|
|
530
542
|
|
|
531
|
-
|
|
543
|
+
manager.cleanup();
|
|
532
544
|
|
|
533
|
-
|
|
534
|
-
|
|
545
|
+
// Should be able to upload again
|
|
546
|
+
await manager.upload({ size: 2000 });
|
|
535
547
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
548
|
+
expect(mockUploadFn).toHaveBeenCalledTimes(2);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
539
551
|
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
552
|
+
describe("edge cases", () => {
|
|
553
|
+
it("should handle calling upload multiple times", async () => {
|
|
554
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
543
555
|
|
|
544
|
-
|
|
545
|
-
|
|
556
|
+
await manager.upload({ size: 1000 });
|
|
557
|
+
await manager.upload({ size: 2000 });
|
|
546
558
|
|
|
547
|
-
|
|
548
|
-
|
|
559
|
+
expect(mockUploadFn).toHaveBeenCalledTimes(2);
|
|
560
|
+
});
|
|
549
561
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
562
|
+
it("should handle abort before upload function resolves", async () => {
|
|
563
|
+
let resolveUpload: (value: UploadAbortController) => void;
|
|
564
|
+
const uploadPromise = new Promise<UploadAbortController>((resolve) => {
|
|
565
|
+
resolveUpload = resolve;
|
|
566
|
+
});
|
|
567
|
+
mockUploadFn.mockReturnValue(uploadPromise);
|
|
556
568
|
|
|
557
|
-
|
|
558
|
-
|
|
569
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
570
|
+
const upload = manager.upload({ size: 1000 });
|
|
559
571
|
|
|
560
|
-
|
|
561
|
-
|
|
572
|
+
// Abort before upload function resolves
|
|
573
|
+
manager.abort();
|
|
562
574
|
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
575
|
+
// Now resolve
|
|
576
|
+
resolveUpload!(mockAbortController);
|
|
577
|
+
await upload;
|
|
566
578
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
579
|
+
// Abort should have been attempted (though controller wasn't available yet)
|
|
580
|
+
expect(manager.getState().status).toBe("uploading");
|
|
581
|
+
});
|
|
570
582
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
583
|
+
it("should handle input without size property", async () => {
|
|
584
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
585
|
+
await manager.upload("string-input");
|
|
574
586
|
|
|
575
|
-
|
|
576
|
-
|
|
587
|
+
expect(stateChanges[0].totalBytes).toBe(null);
|
|
588
|
+
});
|
|
577
589
|
|
|
578
|
-
|
|
579
|
-
|
|
590
|
+
it("should handle null/undefined input", async () => {
|
|
591
|
+
const manager = new UploadManager(mockUploadFn, mockCallbacks);
|
|
580
592
|
|
|
581
|
-
|
|
582
|
-
|
|
593
|
+
await manager.upload(null);
|
|
594
|
+
expect(stateChanges[0].totalBytes).toBe(null);
|
|
583
595
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
596
|
+
await manager.upload(undefined);
|
|
597
|
+
expect(stateChanges[1].totalBytes).toBe(null);
|
|
598
|
+
});
|
|
599
|
+
});
|
|
588
600
|
});
|