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