@forge/react 11.6.1-next.1-experimental-f85d28a → 11.7.0-next.2
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/CHANGELOG.md +7 -7
- package/out/hooks/__test__/useObjectStore.test.d.ts +2 -0
- package/out/hooks/__test__/useObjectStore.test.d.ts.map +1 -0
- package/out/hooks/__test__/useObjectStore.test.js +542 -0
- package/out/hooks/types/objectStoreProps.d.ts +33 -0
- package/out/hooks/types/objectStoreProps.d.ts.map +1 -0
- package/out/hooks/types/objectStoreProps.js +2 -0
- package/out/hooks/useObjectStore.d.ts +9 -0
- package/out/hooks/useObjectStore.d.ts.map +1 -0
- package/out/hooks/useObjectStore.js +162 -0
- package/out/index.d.ts +1 -0
- package/out/index.d.ts.map +1 -1
- package/out/index.js +3 -1
- package/package.json +2 -2
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# @forge/react
|
|
2
2
|
|
|
3
|
-
## 11.
|
|
3
|
+
## 11.7.0-next.2
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- f23a84e: Add useObjectStore hook
|
|
4
8
|
|
|
5
9
|
### Patch Changes
|
|
6
10
|
|
|
7
|
-
- Updated dependencies [
|
|
8
|
-
-
|
|
9
|
-
- Updated dependencies [08fbb0a]
|
|
10
|
-
- Updated dependencies [617c3d9]
|
|
11
|
-
- Updated dependencies [a4bccb7]
|
|
12
|
-
- @forge/bridge@5.8.0-next.12-experimental-f85d28a
|
|
11
|
+
- Updated dependencies [f23a84e]
|
|
12
|
+
- @forge/bridge@5.8.0-next.13
|
|
13
13
|
|
|
14
14
|
## 11.6.1-next.1
|
|
15
15
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useObjectStore.test.d.ts","sourceRoot":"","sources":["../../../src/hooks/__test__/useObjectStore.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
// Mock data - define before imports
|
|
6
|
+
const MOCK_FUNCTION_KEY = 'test-function-key';
|
|
7
|
+
const MOCK_OBJECT_KEY_1 = 'object-key-1';
|
|
8
|
+
const MOCK_OBJECT_KEY_2 = 'object-key-2';
|
|
9
|
+
const MOCK_BLOB = new Blob(['test content'], { type: 'text/plain' });
|
|
10
|
+
const MOCK_BASE64_OBJECT = { data: 'dGVzdCBjb250ZW50', mimeType: 'text/plain', fileSize: 12 };
|
|
11
|
+
// Mock functions - define before imports
|
|
12
|
+
const mockUpload = jest.fn();
|
|
13
|
+
const mockCreateUploadPromises = jest.fn();
|
|
14
|
+
const mockDelete = jest.fn();
|
|
15
|
+
const mockDownload = jest.fn();
|
|
16
|
+
const mockGetMetadata = jest.fn();
|
|
17
|
+
const reconcilerTestRenderer_1 = tslib_1.__importDefault(require("../../__test__/reconcilerTestRenderer"));
|
|
18
|
+
const testUtils_1 = require("../../__test__/testUtils");
|
|
19
|
+
const useObjectStore_1 = require("../useObjectStore");
|
|
20
|
+
// Mock @forge/bridge
|
|
21
|
+
jest.mock('@forge/bridge', () => ({
|
|
22
|
+
objectStore: {
|
|
23
|
+
upload: mockUpload,
|
|
24
|
+
delete: mockDelete,
|
|
25
|
+
download: mockDownload,
|
|
26
|
+
getMetadata: mockGetMetadata
|
|
27
|
+
},
|
|
28
|
+
createUploadPromises: mockCreateUploadPromises
|
|
29
|
+
}));
|
|
30
|
+
describe('useObjectStore', () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
// Clear all mocks before each test
|
|
33
|
+
jest.clearAllMocks();
|
|
34
|
+
(0, testUtils_1.setupBridge)(); // Set up global bridge for tests
|
|
35
|
+
});
|
|
36
|
+
describe('Initialization', () => {
|
|
37
|
+
it('should initialize with empty objectStates when no defaultValues provided', async () => {
|
|
38
|
+
let hookResult;
|
|
39
|
+
const TestComponent = () => {
|
|
40
|
+
hookResult = (0, useObjectStore_1.useObjectStore)();
|
|
41
|
+
return null;
|
|
42
|
+
};
|
|
43
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
44
|
+
expect(hookResult).toBeDefined();
|
|
45
|
+
if (!hookResult)
|
|
46
|
+
return;
|
|
47
|
+
expect(hookResult.objectStates).toEqual([]);
|
|
48
|
+
});
|
|
49
|
+
it('should initialize with defaultValues when provided', async () => {
|
|
50
|
+
const defaultValues = [
|
|
51
|
+
{ key: MOCK_OBJECT_KEY_1, success: true, objectSize: 100 },
|
|
52
|
+
{ key: MOCK_OBJECT_KEY_2, success: true, objectSize: 200 }
|
|
53
|
+
];
|
|
54
|
+
let hookResult;
|
|
55
|
+
const TestComponent = () => {
|
|
56
|
+
hookResult = (0, useObjectStore_1.useObjectStore)({ defaultValues });
|
|
57
|
+
return null;
|
|
58
|
+
};
|
|
59
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
60
|
+
expect(hookResult).toBeDefined();
|
|
61
|
+
if (!hookResult)
|
|
62
|
+
return;
|
|
63
|
+
expect(hookResult.objectStates).toEqual(defaultValues);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('uploadObjects', () => {
|
|
67
|
+
it('should upload Blob objects successfully', async () => {
|
|
68
|
+
const mockResults = [{ key: MOCK_OBJECT_KEY_1, success: true, status: 200 }];
|
|
69
|
+
mockCreateUploadPromises.mockResolvedValueOnce([
|
|
70
|
+
{ promise: Promise.resolve(mockResults[0]), index: 0, objectType: 'text/plain', objectSize: 12 }
|
|
71
|
+
]);
|
|
72
|
+
let hookResult;
|
|
73
|
+
const TestComponent = () => {
|
|
74
|
+
hookResult = (0, useObjectStore_1.useObjectStore)();
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
78
|
+
expect(hookResult).toBeDefined();
|
|
79
|
+
if (!hookResult)
|
|
80
|
+
return;
|
|
81
|
+
// Before upload
|
|
82
|
+
expect(hookResult.objectStates).toEqual([]);
|
|
83
|
+
// Trigger upload
|
|
84
|
+
await hookResult.uploadObjects({
|
|
85
|
+
functionKey: MOCK_FUNCTION_KEY,
|
|
86
|
+
objects: [MOCK_BLOB]
|
|
87
|
+
});
|
|
88
|
+
// Should call bridge startUploads
|
|
89
|
+
expect(mockCreateUploadPromises).toHaveBeenCalledWith({
|
|
90
|
+
functionKey: MOCK_FUNCTION_KEY,
|
|
91
|
+
objects: [MOCK_BLOB]
|
|
92
|
+
});
|
|
93
|
+
// Should update state with results
|
|
94
|
+
expect(hookResult.objectStates).toHaveLength(1);
|
|
95
|
+
expect(hookResult.objectStates[0]).toMatchObject({
|
|
96
|
+
key: MOCK_OBJECT_KEY_1,
|
|
97
|
+
success: true,
|
|
98
|
+
status: 200,
|
|
99
|
+
objectType: 'text/plain',
|
|
100
|
+
objectSize: MOCK_BLOB.size
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
it('should upload Base64Object successfully with fileSize', async () => {
|
|
104
|
+
const mockResults = [{ key: MOCK_OBJECT_KEY_1, success: true, status: 200 }];
|
|
105
|
+
mockCreateUploadPromises.mockResolvedValueOnce([
|
|
106
|
+
{ promise: Promise.resolve(mockResults[0]), index: 0, objectType: 'text/plain', objectSize: 12 }
|
|
107
|
+
]);
|
|
108
|
+
let hookResult;
|
|
109
|
+
const TestComponent = () => {
|
|
110
|
+
hookResult = (0, useObjectStore_1.useObjectStore)();
|
|
111
|
+
return null;
|
|
112
|
+
};
|
|
113
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
114
|
+
expect(hookResult).toBeDefined();
|
|
115
|
+
if (!hookResult)
|
|
116
|
+
return;
|
|
117
|
+
await hookResult.uploadObjects({
|
|
118
|
+
functionKey: MOCK_FUNCTION_KEY,
|
|
119
|
+
objects: [MOCK_BASE64_OBJECT]
|
|
120
|
+
});
|
|
121
|
+
expect(hookResult.objectStates[0]).toMatchObject({
|
|
122
|
+
key: MOCK_OBJECT_KEY_1,
|
|
123
|
+
success: true,
|
|
124
|
+
objectType: 'text/plain',
|
|
125
|
+
objectSize: 12 // Uses provided fileSize
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
it('should calculate objectSize from base64 data when fileSize not provided', async () => {
|
|
129
|
+
const base64WithoutSize = { data: 'dGVzdA==', mimeType: 'text/plain' };
|
|
130
|
+
const mockResults = [{ key: MOCK_OBJECT_KEY_1, success: true, status: 200 }];
|
|
131
|
+
mockCreateUploadPromises.mockResolvedValueOnce([
|
|
132
|
+
{ promise: Promise.resolve(mockResults[0]), index: 0, objectType: 'text/plain', objectSize: 4 }
|
|
133
|
+
]);
|
|
134
|
+
let hookResult;
|
|
135
|
+
const TestComponent = () => {
|
|
136
|
+
hookResult = (0, useObjectStore_1.useObjectStore)();
|
|
137
|
+
return null;
|
|
138
|
+
};
|
|
139
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
140
|
+
expect(hookResult).toBeDefined();
|
|
141
|
+
if (!hookResult)
|
|
142
|
+
return;
|
|
143
|
+
await hookResult.uploadObjects({
|
|
144
|
+
functionKey: MOCK_FUNCTION_KEY,
|
|
145
|
+
objects: [base64WithoutSize]
|
|
146
|
+
});
|
|
147
|
+
// Should calculate size from base64: (8 * 3 / 4) - 2 = 4
|
|
148
|
+
expect(hookResult.objectStates[0].objectSize).toBe(4);
|
|
149
|
+
});
|
|
150
|
+
it('should show isUploading state during upload', async () => {
|
|
151
|
+
let resolveUpload;
|
|
152
|
+
const uploadPromise = new Promise((resolve) => {
|
|
153
|
+
resolveUpload = resolve;
|
|
154
|
+
});
|
|
155
|
+
mockCreateUploadPromises.mockResolvedValueOnce([
|
|
156
|
+
{ promise: uploadPromise, index: 0, objectType: 'text/plain', objectSize: 12 }
|
|
157
|
+
]);
|
|
158
|
+
let hookResult;
|
|
159
|
+
const TestComponent = () => {
|
|
160
|
+
hookResult = (0, useObjectStore_1.useObjectStore)();
|
|
161
|
+
return null;
|
|
162
|
+
};
|
|
163
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
164
|
+
expect(hookResult).toBeDefined();
|
|
165
|
+
if (!hookResult)
|
|
166
|
+
return;
|
|
167
|
+
// Start upload (don't await)
|
|
168
|
+
const uploadPromiseResult = hookResult.uploadObjects({
|
|
169
|
+
functionKey: MOCK_FUNCTION_KEY,
|
|
170
|
+
objects: [MOCK_BLOB]
|
|
171
|
+
});
|
|
172
|
+
// Give a tick for state to update
|
|
173
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
174
|
+
// Should have temp object with isUploading: true
|
|
175
|
+
expect(hookResult.objectStates).toHaveLength(1);
|
|
176
|
+
expect(hookResult.objectStates[0].isUploading).toBe(true);
|
|
177
|
+
expect(hookResult.objectStates[0].key).toMatch(/^temp-upload-/);
|
|
178
|
+
// Resolve upload
|
|
179
|
+
resolveUpload({ key: MOCK_OBJECT_KEY_1, success: true, status: 200 });
|
|
180
|
+
await uploadPromiseResult;
|
|
181
|
+
// Temp object should be replaced with real object (no isUploading)
|
|
182
|
+
expect(hookResult.objectStates).toHaveLength(1);
|
|
183
|
+
expect(hookResult.objectStates[0].key).toBe(MOCK_OBJECT_KEY_1);
|
|
184
|
+
expect(hookResult.objectStates[0].isUploading).toBeUndefined();
|
|
185
|
+
});
|
|
186
|
+
it('should handle upload errors and remove temp objects', async () => {
|
|
187
|
+
const uploadError = new Error('Upload failed');
|
|
188
|
+
mockCreateUploadPromises.mockRejectedValueOnce(uploadError);
|
|
189
|
+
let hookResult;
|
|
190
|
+
const TestComponent = () => {
|
|
191
|
+
hookResult = (0, useObjectStore_1.useObjectStore)();
|
|
192
|
+
return null;
|
|
193
|
+
};
|
|
194
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
195
|
+
expect(hookResult).toBeDefined();
|
|
196
|
+
if (!hookResult)
|
|
197
|
+
return;
|
|
198
|
+
await expect(hookResult.uploadObjects({
|
|
199
|
+
functionKey: MOCK_FUNCTION_KEY,
|
|
200
|
+
objects: [MOCK_BLOB]
|
|
201
|
+
})).rejects.toThrow('Upload failed');
|
|
202
|
+
// Temp objects should be removed
|
|
203
|
+
expect(hookResult.objectStates).toEqual([]);
|
|
204
|
+
});
|
|
205
|
+
it('should upload multiple objects', async () => {
|
|
206
|
+
const mockResults = [
|
|
207
|
+
{ key: MOCK_OBJECT_KEY_1, success: true, status: 200 },
|
|
208
|
+
{ key: MOCK_OBJECT_KEY_2, success: true, status: 200 }
|
|
209
|
+
];
|
|
210
|
+
mockCreateUploadPromises.mockResolvedValueOnce([
|
|
211
|
+
{ promise: Promise.resolve(mockResults[0]), index: 0, objectType: 'text/plain', objectSize: 12 },
|
|
212
|
+
{ promise: Promise.resolve(mockResults[1]), index: 1, objectType: 'text/plain', objectSize: 12 }
|
|
213
|
+
]);
|
|
214
|
+
let hookResult;
|
|
215
|
+
const TestComponent = () => {
|
|
216
|
+
hookResult = (0, useObjectStore_1.useObjectStore)();
|
|
217
|
+
return null;
|
|
218
|
+
};
|
|
219
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
220
|
+
expect(hookResult).toBeDefined();
|
|
221
|
+
if (!hookResult)
|
|
222
|
+
return;
|
|
223
|
+
const blob1 = new Blob(['test1'], { type: 'text/plain' });
|
|
224
|
+
const blob2 = new Blob(['test2'], { type: 'text/plain' });
|
|
225
|
+
await hookResult.uploadObjects({
|
|
226
|
+
functionKey: MOCK_FUNCTION_KEY,
|
|
227
|
+
objects: [blob1, blob2]
|
|
228
|
+
});
|
|
229
|
+
expect(hookResult.objectStates).toHaveLength(2);
|
|
230
|
+
expect(hookResult.objectStates[0].key).toBe(MOCK_OBJECT_KEY_1);
|
|
231
|
+
expect(hookResult.objectStates[1].key).toBe(MOCK_OBJECT_KEY_2);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
describe('deleteObject', () => {
|
|
235
|
+
it('should delete object successfully', async () => {
|
|
236
|
+
mockDelete.mockResolvedValueOnce(undefined);
|
|
237
|
+
const defaultValues = [{ key: MOCK_OBJECT_KEY_1, success: true, objectSize: 100 }];
|
|
238
|
+
let hookResult;
|
|
239
|
+
const TestComponent = () => {
|
|
240
|
+
hookResult = (0, useObjectStore_1.useObjectStore)({ defaultValues });
|
|
241
|
+
return null;
|
|
242
|
+
};
|
|
243
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
244
|
+
expect(hookResult).toBeDefined();
|
|
245
|
+
if (!hookResult)
|
|
246
|
+
return;
|
|
247
|
+
// Before delete
|
|
248
|
+
expect(hookResult.objectStates).toHaveLength(1);
|
|
249
|
+
// Delete object
|
|
250
|
+
await hookResult.deleteObjects({ functionKey: MOCK_FUNCTION_KEY, keys: [MOCK_OBJECT_KEY_1] });
|
|
251
|
+
// Should call bridge delete
|
|
252
|
+
expect(mockDelete).toHaveBeenCalledWith({
|
|
253
|
+
functionKey: MOCK_FUNCTION_KEY,
|
|
254
|
+
keys: [MOCK_OBJECT_KEY_1]
|
|
255
|
+
});
|
|
256
|
+
// Object should be removed from state
|
|
257
|
+
expect(hookResult.objectStates).toEqual([]);
|
|
258
|
+
});
|
|
259
|
+
it('should show isDeleting state during delete', async () => {
|
|
260
|
+
let resolveDelete;
|
|
261
|
+
const deletePromise = new Promise((resolve) => {
|
|
262
|
+
resolveDelete = resolve;
|
|
263
|
+
});
|
|
264
|
+
mockDelete.mockReturnValueOnce(deletePromise);
|
|
265
|
+
const defaultValues = [{ key: MOCK_OBJECT_KEY_1, success: true, objectSize: 100 }];
|
|
266
|
+
let hookResult;
|
|
267
|
+
const TestComponent = () => {
|
|
268
|
+
hookResult = (0, useObjectStore_1.useObjectStore)({ defaultValues });
|
|
269
|
+
return null;
|
|
270
|
+
};
|
|
271
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
272
|
+
expect(hookResult).toBeDefined();
|
|
273
|
+
if (!hookResult)
|
|
274
|
+
return;
|
|
275
|
+
// Start delete (don't await)
|
|
276
|
+
const deletePromiseResult = hookResult.deleteObjects({
|
|
277
|
+
functionKey: MOCK_FUNCTION_KEY,
|
|
278
|
+
keys: [MOCK_OBJECT_KEY_1]
|
|
279
|
+
});
|
|
280
|
+
// Give a tick for state to update
|
|
281
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
282
|
+
// Should have isDeleting: true
|
|
283
|
+
expect(hookResult.objectStates[0].isDeleting).toBe(true);
|
|
284
|
+
// Resolve delete
|
|
285
|
+
resolveDelete(undefined);
|
|
286
|
+
await deletePromiseResult;
|
|
287
|
+
// Object should be removed
|
|
288
|
+
expect(hookResult.objectStates).toEqual([]);
|
|
289
|
+
});
|
|
290
|
+
it('should handle delete errors and set error state', async () => {
|
|
291
|
+
const deleteError = new Error('Delete failed');
|
|
292
|
+
mockDelete.mockRejectedValueOnce(deleteError);
|
|
293
|
+
const defaultValues = [{ key: MOCK_OBJECT_KEY_1, success: true, objectSize: 100 }];
|
|
294
|
+
let hookResult;
|
|
295
|
+
const TestComponent = () => {
|
|
296
|
+
hookResult = (0, useObjectStore_1.useObjectStore)({ defaultValues });
|
|
297
|
+
return null;
|
|
298
|
+
};
|
|
299
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
300
|
+
expect(hookResult).toBeDefined();
|
|
301
|
+
if (!hookResult)
|
|
302
|
+
return;
|
|
303
|
+
await expect(hookResult.deleteObjects({ functionKey: MOCK_FUNCTION_KEY, keys: [MOCK_OBJECT_KEY_1] })).rejects.toThrow('Delete failed');
|
|
304
|
+
// Object should still exist with error state
|
|
305
|
+
expect(hookResult.objectStates).toHaveLength(1);
|
|
306
|
+
expect(hookResult.objectStates[0].isDeleting).toBe(false);
|
|
307
|
+
expect(hookResult.objectStates[0].error).toBe('Delete failed');
|
|
308
|
+
});
|
|
309
|
+
it('should delete multiple objects successfully', async () => {
|
|
310
|
+
mockDelete.mockResolvedValueOnce(undefined);
|
|
311
|
+
const defaultValues = [
|
|
312
|
+
{ key: MOCK_OBJECT_KEY_1, success: true, objectSize: 100 },
|
|
313
|
+
{ key: MOCK_OBJECT_KEY_2, success: true, objectSize: 200 }
|
|
314
|
+
];
|
|
315
|
+
let hookResult;
|
|
316
|
+
const TestComponent = () => {
|
|
317
|
+
hookResult = (0, useObjectStore_1.useObjectStore)({ defaultValues });
|
|
318
|
+
return null;
|
|
319
|
+
};
|
|
320
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
321
|
+
expect(hookResult).toBeDefined();
|
|
322
|
+
if (!hookResult)
|
|
323
|
+
return;
|
|
324
|
+
// Before delete
|
|
325
|
+
expect(hookResult.objectStates).toHaveLength(2);
|
|
326
|
+
// Delete both objects
|
|
327
|
+
await hookResult.deleteObjects({
|
|
328
|
+
functionKey: MOCK_FUNCTION_KEY,
|
|
329
|
+
keys: [MOCK_OBJECT_KEY_1, MOCK_OBJECT_KEY_2]
|
|
330
|
+
});
|
|
331
|
+
// Should call bridge delete with both keys
|
|
332
|
+
expect(mockDelete).toHaveBeenCalledWith({
|
|
333
|
+
functionKey: MOCK_FUNCTION_KEY,
|
|
334
|
+
keys: [MOCK_OBJECT_KEY_1, MOCK_OBJECT_KEY_2]
|
|
335
|
+
});
|
|
336
|
+
// Both objects should be removed from state
|
|
337
|
+
expect(hookResult.objectStates).toEqual([]);
|
|
338
|
+
});
|
|
339
|
+
it('should show isDeleting state for multiple objects during delete', async () => {
|
|
340
|
+
let resolveDelete;
|
|
341
|
+
const deletePromise = new Promise((resolve) => {
|
|
342
|
+
resolveDelete = resolve;
|
|
343
|
+
});
|
|
344
|
+
mockDelete.mockReturnValueOnce(deletePromise);
|
|
345
|
+
const defaultValues = [
|
|
346
|
+
{ key: MOCK_OBJECT_KEY_1, success: true, objectSize: 100 },
|
|
347
|
+
{ key: MOCK_OBJECT_KEY_2, success: true, objectSize: 200 }
|
|
348
|
+
];
|
|
349
|
+
let hookResult;
|
|
350
|
+
const TestComponent = () => {
|
|
351
|
+
hookResult = (0, useObjectStore_1.useObjectStore)({ defaultValues });
|
|
352
|
+
return null;
|
|
353
|
+
};
|
|
354
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
355
|
+
expect(hookResult).toBeDefined();
|
|
356
|
+
if (!hookResult)
|
|
357
|
+
return;
|
|
358
|
+
// Start delete (don't await)
|
|
359
|
+
const deletePromiseResult = hookResult.deleteObjects({
|
|
360
|
+
functionKey: MOCK_FUNCTION_KEY,
|
|
361
|
+
keys: [MOCK_OBJECT_KEY_1, MOCK_OBJECT_KEY_2]
|
|
362
|
+
});
|
|
363
|
+
// Give a tick for state to update
|
|
364
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
365
|
+
// Both should have isDeleting: true
|
|
366
|
+
expect(hookResult.objectStates[0].isDeleting).toBe(true);
|
|
367
|
+
expect(hookResult.objectStates[1].isDeleting).toBe(true);
|
|
368
|
+
// Resolve delete
|
|
369
|
+
resolveDelete(undefined);
|
|
370
|
+
await deletePromiseResult;
|
|
371
|
+
// Both objects should be removed
|
|
372
|
+
expect(hookResult.objectStates).toEqual([]);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
describe('downloadObjects', () => {
|
|
376
|
+
it('should download object successfully', async () => {
|
|
377
|
+
const mockBlob = new Blob(['downloaded content']);
|
|
378
|
+
const mockResults = [{ success: true, key: MOCK_OBJECT_KEY_1, blob: mockBlob }];
|
|
379
|
+
mockDownload.mockResolvedValueOnce(mockResults);
|
|
380
|
+
const defaultValues = [{ key: MOCK_OBJECT_KEY_1, success: true, objectSize: 100 }];
|
|
381
|
+
let hookResult;
|
|
382
|
+
const TestComponent = () => {
|
|
383
|
+
hookResult = (0, useObjectStore_1.useObjectStore)({ defaultValues });
|
|
384
|
+
return null;
|
|
385
|
+
};
|
|
386
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
387
|
+
expect(hookResult).toBeDefined();
|
|
388
|
+
if (!hookResult)
|
|
389
|
+
return;
|
|
390
|
+
const result = await hookResult.downloadObjects({ functionKey: MOCK_FUNCTION_KEY, keys: [MOCK_OBJECT_KEY_1] });
|
|
391
|
+
// Should call bridge download
|
|
392
|
+
expect(mockDownload).toHaveBeenCalledWith({
|
|
393
|
+
functionKey: MOCK_FUNCTION_KEY,
|
|
394
|
+
keys: [MOCK_OBJECT_KEY_1]
|
|
395
|
+
});
|
|
396
|
+
// Should return results with blob
|
|
397
|
+
expect(result[0].blob).toBe(mockBlob);
|
|
398
|
+
});
|
|
399
|
+
it('should show isDownloading state during download', async () => {
|
|
400
|
+
let resolveDownload;
|
|
401
|
+
const downloadPromise = new Promise((resolve) => {
|
|
402
|
+
resolveDownload = resolve;
|
|
403
|
+
});
|
|
404
|
+
mockDownload.mockReturnValueOnce(downloadPromise);
|
|
405
|
+
const defaultValues = [{ key: MOCK_OBJECT_KEY_1, success: true, objectSize: 100 }];
|
|
406
|
+
let hookResult;
|
|
407
|
+
const TestComponent = () => {
|
|
408
|
+
hookResult = (0, useObjectStore_1.useObjectStore)({ defaultValues });
|
|
409
|
+
return null;
|
|
410
|
+
};
|
|
411
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
412
|
+
expect(hookResult).toBeDefined();
|
|
413
|
+
if (!hookResult)
|
|
414
|
+
return;
|
|
415
|
+
// Start download (don't await)
|
|
416
|
+
const downloadPromiseResult = hookResult.downloadObjects({
|
|
417
|
+
functionKey: MOCK_FUNCTION_KEY,
|
|
418
|
+
keys: [MOCK_OBJECT_KEY_1]
|
|
419
|
+
});
|
|
420
|
+
// Give a tick for state to update
|
|
421
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
422
|
+
// Should have isDownloading: true
|
|
423
|
+
expect(hookResult.objectStates[0].isDownloading).toBe(true);
|
|
424
|
+
// Resolve download
|
|
425
|
+
const mockBlob = new Blob(['content']);
|
|
426
|
+
resolveDownload([{ success: true, key: MOCK_OBJECT_KEY_1, blob: mockBlob }]);
|
|
427
|
+
await downloadPromiseResult;
|
|
428
|
+
// isDownloading should be false
|
|
429
|
+
expect(hookResult.objectStates[0].isDownloading).toBe(false);
|
|
430
|
+
});
|
|
431
|
+
it('should handle download errors and set error state', async () => {
|
|
432
|
+
const downloadError = new Error('Download failed');
|
|
433
|
+
mockDownload.mockRejectedValueOnce(downloadError);
|
|
434
|
+
const defaultValues = [{ key: MOCK_OBJECT_KEY_1, success: true, objectSize: 100 }];
|
|
435
|
+
let hookResult;
|
|
436
|
+
const TestComponent = () => {
|
|
437
|
+
hookResult = (0, useObjectStore_1.useObjectStore)({ defaultValues });
|
|
438
|
+
return null;
|
|
439
|
+
};
|
|
440
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
441
|
+
expect(hookResult).toBeDefined();
|
|
442
|
+
if (!hookResult)
|
|
443
|
+
return;
|
|
444
|
+
await expect(hookResult.downloadObjects({ functionKey: MOCK_FUNCTION_KEY, keys: [MOCK_OBJECT_KEY_1] })).rejects.toThrow('Download failed');
|
|
445
|
+
// Object should have error state
|
|
446
|
+
expect(hookResult.objectStates[0].isDownloading).toBe(false);
|
|
447
|
+
expect(hookResult.objectStates[0].error).toBe('Download failed');
|
|
448
|
+
});
|
|
449
|
+
it('should return null when download result has no blob', async () => {
|
|
450
|
+
const mockResults = [{ success: false, key: MOCK_OBJECT_KEY_1, error: 'Not found' }];
|
|
451
|
+
mockDownload.mockResolvedValueOnce(mockResults);
|
|
452
|
+
const defaultValues = [{ key: MOCK_OBJECT_KEY_1, success: true, objectSize: 100 }];
|
|
453
|
+
let hookResult;
|
|
454
|
+
const TestComponent = () => {
|
|
455
|
+
hookResult = (0, useObjectStore_1.useObjectStore)({ defaultValues });
|
|
456
|
+
return null;
|
|
457
|
+
};
|
|
458
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
459
|
+
expect(hookResult).toBeDefined();
|
|
460
|
+
if (!hookResult)
|
|
461
|
+
return;
|
|
462
|
+
const result = await hookResult.downloadObjects({
|
|
463
|
+
functionKey: MOCK_FUNCTION_KEY,
|
|
464
|
+
keys: [MOCK_OBJECT_KEY_1]
|
|
465
|
+
});
|
|
466
|
+
expect(result[0].success).toBe(false);
|
|
467
|
+
expect(result[0].blob).toBeUndefined();
|
|
468
|
+
expect(hookResult.objectStates[0].error).toBe('Download failed');
|
|
469
|
+
});
|
|
470
|
+
});
|
|
471
|
+
describe('getObjectMetadata', () => {
|
|
472
|
+
it('should return object from state when it exists', async () => {
|
|
473
|
+
const defaultValues = [{ key: MOCK_OBJECT_KEY_1, success: true, objectSize: 100 }];
|
|
474
|
+
let hookResult;
|
|
475
|
+
const TestComponent = () => {
|
|
476
|
+
hookResult = (0, useObjectStore_1.useObjectStore)({ defaultValues });
|
|
477
|
+
return null;
|
|
478
|
+
};
|
|
479
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
480
|
+
expect(hookResult).toBeDefined();
|
|
481
|
+
if (!hookResult)
|
|
482
|
+
return;
|
|
483
|
+
const result = hookResult.getObjectMetadata(MOCK_OBJECT_KEY_1);
|
|
484
|
+
expect(result).toEqual(defaultValues[0]);
|
|
485
|
+
});
|
|
486
|
+
it('should return null when object not in state', async () => {
|
|
487
|
+
let hookResult;
|
|
488
|
+
const TestComponent = () => {
|
|
489
|
+
hookResult = (0, useObjectStore_1.useObjectStore)();
|
|
490
|
+
return null;
|
|
491
|
+
};
|
|
492
|
+
await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, {}));
|
|
493
|
+
expect(hookResult).toBeDefined();
|
|
494
|
+
if (!hookResult)
|
|
495
|
+
return;
|
|
496
|
+
const result = hookResult.getObjectMetadata(MOCK_OBJECT_KEY_1);
|
|
497
|
+
expect(result).toBeNull();
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
describe('Function memoization', () => {
|
|
501
|
+
it('should preserve uploadObjects function reference between renders', async () => {
|
|
502
|
+
let uploadFunction1;
|
|
503
|
+
let uploadFunction2;
|
|
504
|
+
const TestComponent = ({ renderKey }) => {
|
|
505
|
+
const { uploadObjects } = (0, useObjectStore_1.useObjectStore)();
|
|
506
|
+
if (renderKey === 1)
|
|
507
|
+
uploadFunction1 = uploadObjects;
|
|
508
|
+
if (renderKey === 2)
|
|
509
|
+
uploadFunction2 = uploadObjects;
|
|
510
|
+
return null;
|
|
511
|
+
};
|
|
512
|
+
const renderer = await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, { renderKey: 1 }));
|
|
513
|
+
await renderer.update((0, jsx_runtime_1.jsx)(TestComponent, { renderKey: 2 }));
|
|
514
|
+
// Verify both functions exist and are callable
|
|
515
|
+
expect(uploadFunction1).toBeDefined();
|
|
516
|
+
expect(uploadFunction2).toBeDefined();
|
|
517
|
+
expect(typeof uploadFunction1).toBe('function');
|
|
518
|
+
expect(typeof uploadFunction2).toBe('function');
|
|
519
|
+
// Note: Function reference stability depends on test renderer behavior
|
|
520
|
+
});
|
|
521
|
+
it('should preserve deleteObjects function reference between renders', async () => {
|
|
522
|
+
let deleteFunction1;
|
|
523
|
+
let deleteFunction2;
|
|
524
|
+
const TestComponent = ({ renderKey }) => {
|
|
525
|
+
const { deleteObjects } = (0, useObjectStore_1.useObjectStore)();
|
|
526
|
+
if (renderKey === 1)
|
|
527
|
+
deleteFunction1 = deleteObjects;
|
|
528
|
+
if (renderKey === 2)
|
|
529
|
+
deleteFunction2 = deleteObjects;
|
|
530
|
+
return null;
|
|
531
|
+
};
|
|
532
|
+
const renderer = await reconcilerTestRenderer_1.default.create((0, jsx_runtime_1.jsx)(TestComponent, { renderKey: 1 }));
|
|
533
|
+
await renderer.update((0, jsx_runtime_1.jsx)(TestComponent, { renderKey: 2 }));
|
|
534
|
+
// Verify both functions exist and are callable
|
|
535
|
+
expect(deleteFunction1).toBeDefined();
|
|
536
|
+
expect(deleteFunction2).toBeDefined();
|
|
537
|
+
expect(typeof deleteFunction1).toBe('function');
|
|
538
|
+
expect(typeof deleteFunction2).toBe('function');
|
|
539
|
+
// Note: Function reference stability depends on test renderer behavior
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { UploadObject, UploadResult, Base64Object } from '@forge/bridge';
|
|
2
|
+
export type { UploadObject, UploadResult, Base64Object };
|
|
3
|
+
export declare type ObjectState = {
|
|
4
|
+
key: string;
|
|
5
|
+
success?: boolean;
|
|
6
|
+
status?: number;
|
|
7
|
+
error?: string;
|
|
8
|
+
objectType?: string;
|
|
9
|
+
objectSize?: number;
|
|
10
|
+
isUploading?: boolean;
|
|
11
|
+
isDeleting?: boolean;
|
|
12
|
+
isDownloading?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export declare type UseObjectStoreProps = {
|
|
15
|
+
defaultValues?: ObjectState[];
|
|
16
|
+
};
|
|
17
|
+
export declare type UploadParams = {
|
|
18
|
+
functionKey: string;
|
|
19
|
+
objects: UploadObject[];
|
|
20
|
+
};
|
|
21
|
+
export declare type DeleteParams = {
|
|
22
|
+
functionKey: string;
|
|
23
|
+
keys: string[];
|
|
24
|
+
};
|
|
25
|
+
export declare type DownloadParams = {
|
|
26
|
+
functionKey: string;
|
|
27
|
+
keys: string[];
|
|
28
|
+
};
|
|
29
|
+
export declare type ClearAllObjectsOptions = {
|
|
30
|
+
deleteFromStore?: boolean;
|
|
31
|
+
functionKey?: string;
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=objectStoreProps.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"objectStoreProps.d.ts","sourceRoot":"","sources":["../../../src/hooks/types/objectStoreProps.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAE9E,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,CAAC;AAEzD,oBAAY,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,oBAAY,mBAAmB,GAAG;IAChC,aAAa,CAAC,EAAE,WAAW,EAAE,CAAC;CAC/B,CAAC;AAEF,oBAAY,YAAY,GAAG;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB,CAAC;AAEF,oBAAY,YAAY,GAAG;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,CAAC;AAEF,oBAAY,cAAc,GAAG;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB,CAAC;AAEF,oBAAY,sBAAsB,GAAG;IACnC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ObjectState, UseObjectStoreProps, UploadParams, DeleteParams, DownloadParams } from './types/objectStoreProps';
|
|
2
|
+
export declare function useObjectStore(props?: UseObjectStoreProps): {
|
|
3
|
+
objectStates: ObjectState[];
|
|
4
|
+
getObjectMetadata: (key: string) => ObjectState | null;
|
|
5
|
+
uploadObjects: (params: UploadParams) => Promise<void>;
|
|
6
|
+
deleteObjects: (params: DeleteParams) => Promise<void>;
|
|
7
|
+
downloadObjects: (params: DownloadParams) => Promise<import("@forge/bridge").DownloadResult[]>;
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=useObjectStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useObjectStore.d.ts","sourceRoot":"","sources":["../../src/hooks/useObjectStore.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,WAAW,EACX,mBAAmB,EACnB,YAAY,EACZ,YAAY,EACZ,cAAc,EACf,MAAM,0BAA0B,CAAC;AAqFlC,wBAAgB,cAAc,CAAC,KAAK,GAAE,mBAAwB;;6BAQ5B,MAAM,KAAG,WAAW,GAAG,IAAI;4BAeV,YAAY,KAAG,QAAQ,IAAI,CAAC;4BA2C5B,YAAY,KAAG,QAAQ,IAAI,CAAC;8BAkB1B,cAAc;EA+BlE"}
|