@uploadista/react 0.0.20 → 0.1.0

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.
@@ -0,0 +1,317 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import type { ReactNode } from "react";
3
+ import { describe, expect, it, vi, beforeEach, type MockInstance } from "vitest";
4
+ import { UploadistaProvider } from "../components/uploadista-provider";
5
+ import { useUpload } from "../hooks/use-upload";
6
+
7
+ // Create mock manager instance
8
+ const mockManagerInstance = {
9
+ upload: vi.fn(),
10
+ abort: vi.fn(),
11
+ reset: vi.fn(),
12
+ retry: vi.fn(),
13
+ cleanup: vi.fn(),
14
+ canRetry: vi.fn(() => false),
15
+ };
16
+
17
+ // Keep track of UploadManager constructor calls
18
+ let uploadManagerConstructorCalls: any[] = [];
19
+
20
+ // Mock dependencies
21
+ vi.mock("@uploadista/client-browser", () => ({
22
+ createUploadistaClient: vi.fn(() => ({
23
+ upload: vi.fn(),
24
+ executeFlow: vi.fn(),
25
+ discoverFlowInputs: vi.fn(),
26
+ uploadWithFlow: vi.fn(),
27
+ multiInputFlowUpload: vi.fn(),
28
+ getChunkingInsights: vi.fn(() => ({
29
+ currentChunkSize: 1024 * 1024,
30
+ recommendedChunkSize: 1024 * 1024,
31
+ networkCondition: "good",
32
+ })),
33
+ exportMetrics: vi.fn(() => ({})),
34
+ getNetworkMetrics: vi.fn(() => ({
35
+ averageSpeed: 1024 * 1024,
36
+ currentSpeed: 1024 * 1024,
37
+ estimatedTimeRemaining: 0,
38
+ })),
39
+ getNetworkCondition: vi.fn(() => "good"),
40
+ resetMetrics: vi.fn(),
41
+ })),
42
+ }));
43
+
44
+ vi.mock("@uploadista/client-core", async (importOriginal) => {
45
+ const actual = await importOriginal<typeof import("@uploadista/client-core")>();
46
+
47
+ // Use a proper class mock
48
+ class MockUploadManager {
49
+ constructor(...args: any[]) {
50
+ uploadManagerConstructorCalls.push(args);
51
+ Object.assign(this, mockManagerInstance);
52
+ }
53
+
54
+ upload = mockManagerInstance.upload;
55
+ abort = mockManagerInstance.abort;
56
+ reset = mockManagerInstance.reset;
57
+ retry = mockManagerInstance.retry;
58
+ cleanup = mockManagerInstance.cleanup;
59
+ canRetry = mockManagerInstance.canRetry;
60
+ }
61
+
62
+ return {
63
+ ...actual,
64
+ UploadManager: MockUploadManager,
65
+ FlowManager: class MockFlowManager {
66
+ handleFlowEvent = vi.fn();
67
+ handleUploadProgress = vi.fn();
68
+ cleanup = vi.fn();
69
+ },
70
+ };
71
+ });
72
+
73
+ // Wrapper component that provides the context
74
+ const wrapper = ({ children }: { children: ReactNode }) => (
75
+ <UploadistaProvider
76
+ baseUrl="https://api.example.com"
77
+ storageId="test"
78
+ chunkSize={1024 * 1024}
79
+ storeFingerprintForResuming={true}
80
+ >
81
+ {children}
82
+ </UploadistaProvider>
83
+ );
84
+
85
+ describe("useUpload", () => {
86
+ beforeEach(() => {
87
+ vi.clearAllMocks();
88
+ uploadManagerConstructorCalls = [];
89
+ // Reset mock functions
90
+ mockManagerInstance.upload.mockClear();
91
+ mockManagerInstance.abort.mockClear();
92
+ mockManagerInstance.reset.mockClear();
93
+ mockManagerInstance.retry.mockClear();
94
+ mockManagerInstance.cleanup.mockClear();
95
+ mockManagerInstance.canRetry.mockReturnValue(false);
96
+ });
97
+
98
+ describe("initial state", () => {
99
+ it("should have correct initial state", () => {
100
+ const { result } = renderHook(() => useUpload(), { wrapper });
101
+
102
+ expect(result.current.state).toEqual({
103
+ status: "idle",
104
+ progress: 0,
105
+ bytesUploaded: 0,
106
+ totalBytes: null,
107
+ error: null,
108
+ result: null,
109
+ });
110
+ });
111
+
112
+ it("should return isUploading as false initially", () => {
113
+ const { result } = renderHook(() => useUpload(), { wrapper });
114
+
115
+ expect(result.current.isUploading).toBe(false);
116
+ });
117
+
118
+ it("should return canRetry as false initially", () => {
119
+ const { result } = renderHook(() => useUpload(), { wrapper });
120
+
121
+ expect(result.current.canRetry).toBe(false);
122
+ });
123
+
124
+ it("should return control methods", () => {
125
+ const { result } = renderHook(() => useUpload(), { wrapper });
126
+
127
+ expect(typeof result.current.upload).toBe("function");
128
+ expect(typeof result.current.abort).toBe("function");
129
+ expect(typeof result.current.reset).toBe("function");
130
+ expect(typeof result.current.retry).toBe("function");
131
+ });
132
+
133
+ it("should return metrics object", () => {
134
+ const { result } = renderHook(() => useUpload(), { wrapper });
135
+
136
+ expect(result.current.metrics).toBeDefined();
137
+ expect(typeof result.current.metrics.getInsights).toBe("function");
138
+ expect(typeof result.current.metrics.exportMetrics).toBe("function");
139
+ expect(typeof result.current.metrics.getNetworkMetrics).toBe("function");
140
+ expect(typeof result.current.metrics.getNetworkCondition).toBe("function");
141
+ expect(typeof result.current.metrics.resetMetrics).toBe("function");
142
+ });
143
+ });
144
+
145
+ describe("UploadManager initialization", () => {
146
+ it("should create UploadManager on mount", () => {
147
+ renderHook(() => useUpload(), { wrapper });
148
+
149
+ expect(uploadManagerConstructorCalls.length).toBeGreaterThan(0);
150
+ });
151
+
152
+ it("should pass upload function as first argument to UploadManager", () => {
153
+ renderHook(() => useUpload(), { wrapper });
154
+
155
+ expect(uploadManagerConstructorCalls.length).toBeGreaterThan(0);
156
+ expect(typeof uploadManagerConstructorCalls[0][0]).toBe("function");
157
+ });
158
+
159
+ it("should pass callbacks to UploadManager", () => {
160
+ const onProgress = vi.fn();
161
+ const onChunkComplete = vi.fn();
162
+ const onSuccess = vi.fn();
163
+ const onError = vi.fn();
164
+ const onAbort = vi.fn();
165
+
166
+ renderHook(
167
+ () =>
168
+ useUpload({
169
+ onProgress,
170
+ onChunkComplete,
171
+ onSuccess,
172
+ onError,
173
+ onAbort,
174
+ }),
175
+ { wrapper },
176
+ );
177
+
178
+ expect(uploadManagerConstructorCalls.length).toBeGreaterThan(0);
179
+ const callbacks = uploadManagerConstructorCalls[0][1];
180
+ expect(callbacks.onProgress).toBe(onProgress);
181
+ expect(callbacks.onChunkComplete).toBe(onChunkComplete);
182
+ expect(callbacks.onSuccess).toBe(onSuccess);
183
+ expect(callbacks.onError).toBe(onError);
184
+ expect(callbacks.onAbort).toBe(onAbort);
185
+ });
186
+
187
+ it("should pass options to UploadManager", () => {
188
+ const metadata = { key: "value" };
189
+ const onShouldRetry = vi.fn();
190
+
191
+ renderHook(
192
+ () =>
193
+ useUpload({
194
+ metadata,
195
+ uploadLengthDeferred: true,
196
+ uploadSize: 1000,
197
+ onShouldRetry,
198
+ }),
199
+ { wrapper },
200
+ );
201
+
202
+ expect(uploadManagerConstructorCalls.length).toBeGreaterThan(0);
203
+ const options = uploadManagerConstructorCalls[0][2];
204
+ expect(options.metadata).toEqual(metadata);
205
+ expect(options.uploadLengthDeferred).toBe(true);
206
+ expect(options.uploadSize).toBe(1000);
207
+ expect(options.onShouldRetry).toBe(onShouldRetry);
208
+ });
209
+
210
+ it("should cleanup UploadManager on unmount", () => {
211
+ const { unmount } = renderHook(() => useUpload(), { wrapper });
212
+
213
+ unmount();
214
+
215
+ expect(mockManagerInstance.cleanup).toHaveBeenCalled();
216
+ });
217
+ });
218
+
219
+ describe("upload method", () => {
220
+ it("should call manager.upload when upload is called", () => {
221
+ const { result } = renderHook(() => useUpload(), { wrapper });
222
+
223
+ const mockFile = new File(["test"], "test.txt", { type: "text/plain" });
224
+
225
+ act(() => {
226
+ result.current.upload(mockFile);
227
+ });
228
+
229
+ expect(mockManagerInstance.upload).toHaveBeenCalledWith(mockFile);
230
+ });
231
+ });
232
+
233
+ describe("abort method", () => {
234
+ it("should call manager.abort when abort is called", () => {
235
+ const { result } = renderHook(() => useUpload(), { wrapper });
236
+
237
+ act(() => {
238
+ result.current.abort();
239
+ });
240
+
241
+ expect(mockManagerInstance.abort).toHaveBeenCalled();
242
+ });
243
+ });
244
+
245
+ describe("reset method", () => {
246
+ it("should call manager.reset when reset is called", () => {
247
+ const { result } = renderHook(() => useUpload(), { wrapper });
248
+
249
+ act(() => {
250
+ result.current.reset();
251
+ });
252
+
253
+ expect(mockManagerInstance.reset).toHaveBeenCalled();
254
+ });
255
+ });
256
+
257
+ describe("retry method", () => {
258
+ it("should call manager.retry when retry is called", () => {
259
+ const { result } = renderHook(() => useUpload(), { wrapper });
260
+
261
+ act(() => {
262
+ result.current.retry();
263
+ });
264
+
265
+ expect(mockManagerInstance.retry).toHaveBeenCalled();
266
+ });
267
+ });
268
+
269
+ describe("canRetry", () => {
270
+ it("should return manager canRetry value after re-render", () => {
271
+ mockManagerInstance.canRetry.mockReturnValue(true);
272
+
273
+ const { result, rerender } = renderHook(() => useUpload(), { wrapper });
274
+
275
+ // Initial render - manager is created in useEffect, so canRetry is computed
276
+ // After re-render, canRetry should reflect manager's value
277
+ rerender();
278
+
279
+ expect(result.current.canRetry).toBe(true);
280
+ });
281
+
282
+ it("should return false when manager.canRetry returns false", () => {
283
+ mockManagerInstance.canRetry.mockReturnValue(false);
284
+
285
+ const { result, rerender } = renderHook(() => useUpload(), { wrapper });
286
+ rerender();
287
+
288
+ expect(result.current.canRetry).toBe(false);
289
+ });
290
+ });
291
+
292
+ describe("error handling", () => {
293
+ it("should throw when used outside provider", () => {
294
+ expect(() => {
295
+ renderHook(() => useUpload());
296
+ }).toThrow("useUploadistaContext must be used within an UploadistaProvider");
297
+ });
298
+ });
299
+
300
+ describe("method stability", () => {
301
+ it("should return stable method references", () => {
302
+ const { result, rerender } = renderHook(() => useUpload(), { wrapper });
303
+
304
+ const firstUpload = result.current.upload;
305
+ const firstAbort = result.current.abort;
306
+ const firstReset = result.current.reset;
307
+ const firstRetry = result.current.retry;
308
+
309
+ rerender();
310
+
311
+ expect(result.current.upload).toBe(firstUpload);
312
+ expect(result.current.abort).toBe(firstAbort);
313
+ expect(result.current.reset).toBe(firstReset);
314
+ expect(result.current.retry).toBe(firstRetry);
315
+ });
316
+ });
317
+ });
@@ -0,0 +1,208 @@
1
+ import { renderHook } from "@testing-library/react";
2
+ import { createUploadistaClient } from "@uploadista/client-browser";
3
+ import { beforeEach, describe, expect, it, vi } from "vitest";
4
+ import { useUploadistaClient } from "../hooks/use-uploadista-client";
5
+
6
+ vi.mock("@uploadista/client-browser", () => ({
7
+ createUploadistaClient: vi.fn(() => ({
8
+ upload: vi.fn(),
9
+ executeFlow: vi.fn(),
10
+ discoverFlowInputs: vi.fn(),
11
+ })),
12
+ }));
13
+
14
+ describe("useUploadistaClient", () => {
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+ });
18
+
19
+ describe("client creation", () => {
20
+ it("should create a client with provided options", () => {
21
+ const options = {
22
+ baseUrl: "https://api.example.com",
23
+ storageId: "test-storage",
24
+ chunkSize: 1024 * 1024,
25
+ storeFingerprintForResuming: true,
26
+ };
27
+
28
+ renderHook(() => useUploadistaClient(options));
29
+
30
+ expect(createUploadistaClient).toHaveBeenCalledWith(
31
+ expect.objectContaining({
32
+ baseUrl: "https://api.example.com",
33
+ storageId: "test-storage",
34
+ chunkSize: 1024 * 1024,
35
+ storeFingerprintForResuming: true,
36
+ }),
37
+ );
38
+ });
39
+
40
+ it("should return client and config", () => {
41
+ const options = {
42
+ baseUrl: "https://api.example.com",
43
+ storageId: "test-storage",
44
+ chunkSize: 1024 * 1024,
45
+ storeFingerprintForResuming: true,
46
+ };
47
+
48
+ const { result } = renderHook(() => useUploadistaClient(options));
49
+
50
+ expect(result.current.client).toBeDefined();
51
+ expect(result.current.config).toEqual(options);
52
+ });
53
+ });
54
+
55
+ describe("memoization", () => {
56
+ it("should return same client instance when options are stable", () => {
57
+ const options = {
58
+ baseUrl: "https://api.example.com",
59
+ storageId: "test-storage",
60
+ chunkSize: 1024 * 1024,
61
+ storeFingerprintForResuming: true,
62
+ };
63
+
64
+ const { result, rerender } = renderHook(() =>
65
+ useUploadistaClient(options),
66
+ );
67
+
68
+ const firstClient = result.current.client;
69
+
70
+ rerender();
71
+
72
+ expect(result.current.client).toBe(firstClient);
73
+ expect(createUploadistaClient).toHaveBeenCalledTimes(1);
74
+ });
75
+
76
+ it("should create new client when baseUrl changes", () => {
77
+ const { rerender } = renderHook(
78
+ ({ baseUrl }) =>
79
+ useUploadistaClient({
80
+ baseUrl,
81
+ storageId: "test-storage",
82
+ chunkSize: 1024 * 1024,
83
+ storeFingerprintForResuming: true,
84
+ }),
85
+ {
86
+ initialProps: { baseUrl: "https://api1.example.com" },
87
+ },
88
+ );
89
+
90
+ expect(createUploadistaClient).toHaveBeenCalledTimes(1);
91
+
92
+ rerender({ baseUrl: "https://api2.example.com" });
93
+
94
+ expect(createUploadistaClient).toHaveBeenCalledTimes(2);
95
+ });
96
+
97
+ it("should create new client when storageId changes", () => {
98
+ const { rerender } = renderHook(
99
+ ({ storageId }) =>
100
+ useUploadistaClient({
101
+ baseUrl: "https://api.example.com",
102
+ storageId,
103
+ chunkSize: 1024 * 1024,
104
+ storeFingerprintForResuming: true,
105
+ }),
106
+ {
107
+ initialProps: { storageId: "storage-1" },
108
+ },
109
+ );
110
+
111
+ expect(createUploadistaClient).toHaveBeenCalledTimes(1);
112
+
113
+ rerender({ storageId: "storage-2" });
114
+
115
+ expect(createUploadistaClient).toHaveBeenCalledTimes(2);
116
+ });
117
+
118
+ it("should create new client when chunkSize changes", () => {
119
+ const { rerender } = renderHook(
120
+ ({ chunkSize }) =>
121
+ useUploadistaClient({
122
+ baseUrl: "https://api.example.com",
123
+ storageId: "test-storage",
124
+ chunkSize,
125
+ storeFingerprintForResuming: true,
126
+ }),
127
+ {
128
+ initialProps: { chunkSize: 1024 * 1024 },
129
+ },
130
+ );
131
+
132
+ expect(createUploadistaClient).toHaveBeenCalledTimes(1);
133
+
134
+ rerender({ chunkSize: 2 * 1024 * 1024 });
135
+
136
+ expect(createUploadistaClient).toHaveBeenCalledTimes(2);
137
+ });
138
+ });
139
+
140
+ describe("options passthrough", () => {
141
+ it("should pass all client options to createUploadistaClient", () => {
142
+ const onEvent = vi.fn();
143
+ const connectionPoolingConfig = { maxConnectionsPerHost: 6 };
144
+ const options = {
145
+ baseUrl: "https://api.example.com",
146
+ storageId: "test-storage",
147
+ uploadistaBasePath: "/custom/upload",
148
+ chunkSize: 2 * 1024 * 1024,
149
+ storeFingerprintForResuming: true,
150
+ retryDelays: [1000, 2000, 5000],
151
+ parallelUploads: 3,
152
+ parallelChunkSize: 5,
153
+ uploadStrategy: { preferredStrategy: "parallel" as const },
154
+ smartChunking: { enabled: true },
155
+ networkMonitoring: { maxSamples: 100 },
156
+ uploadMetrics: { maxChunkHistory: 500 },
157
+ connectionPooling: connectionPoolingConfig,
158
+ auth: { mode: "direct" as const, getCredentials: () => ({ headers: { Authorization: "Bearer test-token" } }) },
159
+ onEvent,
160
+ };
161
+
162
+ renderHook(() => useUploadistaClient(options));
163
+
164
+ expect(createUploadistaClient).toHaveBeenCalledWith(
165
+ expect.objectContaining({
166
+ baseUrl: "https://api.example.com",
167
+ storageId: "test-storage",
168
+ uploadistaBasePath: "/custom/upload",
169
+ chunkSize: 2 * 1024 * 1024,
170
+ storeFingerprintForResuming: true,
171
+ retryDelays: [1000, 2000, 5000],
172
+ parallelUploads: 3,
173
+ parallelChunkSize: 5,
174
+ uploadStrategy: { preferredStrategy: "parallel" },
175
+ smartChunking: { enabled: true },
176
+ networkMonitoring: { maxSamples: 100 },
177
+ uploadMetrics: { maxChunkHistory: 500 },
178
+ connectionPooling: connectionPoolingConfig,
179
+ auth: expect.objectContaining({ mode: "direct" }),
180
+ onEvent,
181
+ }),
182
+ );
183
+ });
184
+ });
185
+
186
+ describe("config updates", () => {
187
+ it("should return updated config when options change", () => {
188
+ const { result, rerender } = renderHook(
189
+ ({ storageId }) =>
190
+ useUploadistaClient({
191
+ baseUrl: "https://api.example.com",
192
+ storageId,
193
+ chunkSize: 1024 * 1024,
194
+ storeFingerprintForResuming: true,
195
+ }),
196
+ {
197
+ initialProps: { storageId: "storage-1" },
198
+ },
199
+ );
200
+
201
+ expect(result.current.config.storageId).toBe("storage-1");
202
+
203
+ rerender({ storageId: "storage-2" });
204
+
205
+ expect(result.current.config.storageId).toBe("storage-2");
206
+ });
207
+ });
208
+ });
@@ -7,12 +7,7 @@ import type {
7
7
  InputExecutionState,
8
8
  } from "@uploadista/client-core";
9
9
  import type { TypedOutput } from "@uploadista/core/flow";
10
- import {
11
- type ReactNode,
12
- createContext,
13
- useCallback,
14
- useContext,
15
- } from "react";
10
+ import { createContext, type ReactNode, useCallback, useContext } from "react";
16
11
  import {
17
12
  type DragDropState,
18
13
  type UseDragDropReturn,
@@ -78,7 +73,7 @@ export function useFlowContext(): FlowContextValue {
78
73
  if (!context) {
79
74
  throw new Error(
80
75
  "useFlowContext must be used within a <Flow> component. " +
81
- "Wrap your component tree with <Flow flowId=\"...\" storageId=\"...\">",
76
+ 'Wrap your component tree with <Flow flowId="..." storageId="...">',
82
77
  );
83
78
  }
84
79
  return context;
@@ -113,7 +108,7 @@ export function useFlowInputContext(): FlowInputContextValue {
113
108
  if (!context) {
114
109
  throw new Error(
115
110
  "useFlowInputContext must be used within a <Flow.Input> component. " +
116
- "Wrap your component with <Flow.Input nodeId=\"...\">",
111
+ 'Wrap your component with <Flow.Input nodeId="...">',
117
112
  );
118
113
  }
119
114
  return context;
@@ -1,9 +1,5 @@
1
1
  // Flow Primitives (NEW compound component)
2
- export {
3
- Flow,
4
- useFlowContext,
5
- useFlowInputContext,
6
- } from "./flow-primitives";
2
+
7
3
  export type {
8
4
  FlowCancelProps,
9
5
  FlowContextValue,
@@ -28,6 +24,11 @@ export type {
28
24
  FlowStatusRenderProps,
29
25
  FlowSubmitProps,
30
26
  } from "./flow-primitives";
27
+ export {
28
+ Flow,
29
+ useFlowContext,
30
+ useFlowInputContext,
31
+ } from "./flow-primitives";
31
32
 
32
33
  // Flow Upload List (for batch uploads with useMultiFlowUpload)
33
34
  export type {
@@ -49,7 +50,37 @@ export type {
49
50
  UploadListRenderProps,
50
51
  } from "./upload-list";
51
52
  export { SimpleUploadListItem, UploadList } from "./upload-list";
52
-
53
+ export type {
54
+ MultiUploadState,
55
+ UploadCancelProps,
56
+ UploadClearCompletedProps,
57
+ UploadContextValue,
58
+ UploadDropZoneProps,
59
+ UploadDropZoneRenderProps,
60
+ UploadErrorProps,
61
+ UploadErrorRenderProps,
62
+ UploadItem,
63
+ UploadItemContextValue,
64
+ UploadItemProps,
65
+ UploadItemsProps,
66
+ UploadItemsRenderProps,
67
+ UploadProgressProps,
68
+ UploadProgressRenderProps,
69
+ UploadProps,
70
+ UploadResetProps,
71
+ UploadRetryProps,
72
+ UploadStartAllProps,
73
+ UploadState,
74
+ UploadStatus,
75
+ UploadStatusProps,
76
+ UploadStatusRenderProps,
77
+ } from "./upload-primitives";
78
+ // Upload Primitives (NEW compound component)
79
+ export {
80
+ Upload,
81
+ useUploadContext,
82
+ useUploadItemContext,
83
+ } from "./upload-primitives";
53
84
  export type {
54
85
  SimpleUploadZoneProps,
55
86
  UploadZoneProps,