@umituz/react-native-storage 1.5.0 → 2.3.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/README.md +0 -0
- package/package.json +28 -10
- package/src/__tests__/integration.test.ts +391 -0
- package/src/__tests__/mocks/asyncStorage.mock.ts +52 -0
- package/src/__tests__/performance.test.ts +351 -0
- package/src/__tests__/setup.ts +63 -0
- package/src/application/ports/IStorageRepository.ts +0 -12
- package/src/domain/constants/CacheDefaults.ts +64 -0
- package/src/domain/entities/CachedValue.ts +86 -0
- package/src/domain/entities/StorageResult.ts +1 -3
- package/src/domain/entities/__tests__/CachedValue.test.ts +149 -0
- package/src/domain/entities/__tests__/StorageResult.test.ts +122 -0
- package/src/domain/errors/StorageError.ts +0 -2
- package/src/domain/errors/__tests__/StorageError.test.ts +127 -0
- package/src/domain/factories/StoreFactory.ts +33 -0
- package/src/domain/types/Store.ts +18 -0
- package/src/domain/utils/CacheKeyGenerator.ts +66 -0
- package/src/domain/utils/__tests__/devUtils.test.ts +97 -0
- package/src/domain/utils/devUtils.ts +37 -0
- package/src/domain/value-objects/StorageKey.ts +27 -29
- package/src/index.ts +59 -1
- package/src/infrastructure/adapters/StorageService.ts +8 -6
- package/src/infrastructure/repositories/AsyncStorageRepository.ts +27 -108
- package/src/infrastructure/repositories/BaseStorageOperations.ts +101 -0
- package/src/infrastructure/repositories/BatchStorageOperations.ts +42 -0
- package/src/infrastructure/repositories/StringStorageOperations.ts +44 -0
- package/src/infrastructure/repositories/__tests__/AsyncStorageRepository.test.ts +169 -0
- package/src/infrastructure/repositories/__tests__/BaseStorageOperations.test.ts +200 -0
- package/src/presentation/hooks/CacheStorageOperations.ts +95 -0
- package/src/presentation/hooks/__tests__/usePersistentCache.test.ts +404 -0
- package/src/presentation/hooks/__tests__/useStorage.test.ts +246 -0
- package/src/presentation/hooks/__tests__/useStorageState.test.ts +292 -0
- package/src/presentation/hooks/useCacheState.ts +55 -0
- package/src/presentation/hooks/usePersistentCache.ts +154 -0
- package/src/presentation/hooks/useStorage.ts +4 -3
- package/src/presentation/hooks/useStorageState.ts +24 -8
- package/src/presentation/hooks/useStore.ts +15 -0
- package/src/types/global.d.ts +40 -0
- package/LICENSE +0 -22
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePersistentCache Hook Tests
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for usePersistentCache hook
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { renderHook, act } from '@testing-library/react-hooks';
|
|
8
|
+
import { usePersistentCache } from '../usePersistentCache';
|
|
9
|
+
import { AsyncStorage } from '../../__tests__/mocks/asyncStorage.mock';
|
|
10
|
+
import { DEFAULT_TTL } from '../../../domain/constants/CacheDefaults';
|
|
11
|
+
|
|
12
|
+
describe('usePersistentCache Hook', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
(AsyncStorage as any).__clear();
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
jest.spyOn(Date, 'now').mockReturnValue(1000000);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
jest.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('Initial Load', () => {
|
|
24
|
+
it('should load cached data', async () => {
|
|
25
|
+
const key = 'test-cache';
|
|
26
|
+
const data = { test: 'value' };
|
|
27
|
+
const cachedValue = {
|
|
28
|
+
value: data,
|
|
29
|
+
cachedAt: 1000000 - 10000, // 10 seconds ago
|
|
30
|
+
expiresAt: 1000000 + 50000, // 50 seconds from now
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
await AsyncStorage.setItem(key, JSON.stringify(cachedValue));
|
|
34
|
+
|
|
35
|
+
const { result, waitForNextUpdate } = renderHook(() =>
|
|
36
|
+
usePersistentCache(key)
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
await act(async () => {
|
|
40
|
+
await waitForNextUpdate();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(result.current.data).toEqual(data);
|
|
44
|
+
expect(result.current.isLoading).toBe(false);
|
|
45
|
+
expect(result.current.isExpired).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should handle missing cache', async () => {
|
|
49
|
+
const key = 'missing-cache';
|
|
50
|
+
|
|
51
|
+
const { result, waitForNextUpdate } = renderHook(() =>
|
|
52
|
+
usePersistentCache(key)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
await act(async () => {
|
|
56
|
+
await waitForNextUpdate();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(result.current.data).toBeNull();
|
|
60
|
+
expect(result.current.isLoading).toBe(false);
|
|
61
|
+
expect(result.current.isExpired).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should handle expired cache', async () => {
|
|
65
|
+
const key = 'expired-cache';
|
|
66
|
+
const data = { test: 'value' };
|
|
67
|
+
const cachedValue = {
|
|
68
|
+
value: data,
|
|
69
|
+
cachedAt: 1000000 - 120000, // 2 minutes ago
|
|
70
|
+
expiresAt: 1000000 - 60000, // 1 minute ago (expired)
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
await AsyncStorage.setItem(key, JSON.stringify(cachedValue));
|
|
74
|
+
|
|
75
|
+
const { result, waitForNextUpdate } = renderHook(() =>
|
|
76
|
+
usePersistentCache(key)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
await act(async () => {
|
|
80
|
+
await waitForNextUpdate();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(result.current.data).toEqual(data);
|
|
84
|
+
expect(result.current.isLoading).toBe(false);
|
|
85
|
+
expect(result.current.isExpired).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('setData', () => {
|
|
90
|
+
it('should set data to cache', async () => {
|
|
91
|
+
const key = 'test-cache';
|
|
92
|
+
const data = { test: 'value' };
|
|
93
|
+
|
|
94
|
+
const { result, waitForNextUpdate } = renderHook(() =>
|
|
95
|
+
usePersistentCache(key)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
await act(async () => {
|
|
99
|
+
await waitForNextUpdate();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Set data
|
|
103
|
+
await act(async () => {
|
|
104
|
+
await result.current.setData(data);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(result.current.data).toEqual(data);
|
|
108
|
+
expect(result.current.isExpired).toBe(false);
|
|
109
|
+
|
|
110
|
+
// Verify storage
|
|
111
|
+
const stored = await AsyncStorage.getItem(key);
|
|
112
|
+
const parsed = JSON.parse(stored!);
|
|
113
|
+
expect(parsed.value).toEqual(data);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should use custom TTL', async () => {
|
|
117
|
+
const key = 'test-cache';
|
|
118
|
+
const data = { test: 'value' };
|
|
119
|
+
const ttl = 120000; // 2 minutes
|
|
120
|
+
|
|
121
|
+
const { result } = renderHook(() =>
|
|
122
|
+
usePersistentCache(key, { ttl })
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await act(async () => {
|
|
126
|
+
await result.current.setData(data);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Verify storage
|
|
130
|
+
const stored = await AsyncStorage.getItem(key);
|
|
131
|
+
const parsed = JSON.parse(stored!);
|
|
132
|
+
expect(parsed.expiresAt - parsed.cachedAt).toBe(ttl);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should use version', async () => {
|
|
136
|
+
const key = 'test-cache';
|
|
137
|
+
const data = { test: 'value' };
|
|
138
|
+
const version = 2;
|
|
139
|
+
|
|
140
|
+
const { result } = renderHook(() =>
|
|
141
|
+
usePersistentCache(key, { version })
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
await act(async () => {
|
|
145
|
+
await result.current.setData(data);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Verify storage
|
|
149
|
+
const stored = await AsyncStorage.getItem(key);
|
|
150
|
+
const parsed = JSON.parse(stored!);
|
|
151
|
+
expect(parsed.version).toBe(version);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should not set data when disabled', async () => {
|
|
155
|
+
const key = 'test-cache';
|
|
156
|
+
const data = { test: 'value' };
|
|
157
|
+
|
|
158
|
+
const { result } = renderHook(() =>
|
|
159
|
+
usePersistentCache(key, { enabled: false })
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
await act(async () => {
|
|
163
|
+
await result.current.setData(data);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Verify storage
|
|
167
|
+
const stored = await AsyncStorage.getItem(key);
|
|
168
|
+
expect(stored).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('clearData', () => {
|
|
173
|
+
it('should clear cached data', async () => {
|
|
174
|
+
const key = 'test-cache';
|
|
175
|
+
const data = { test: 'value' };
|
|
176
|
+
const cachedValue = {
|
|
177
|
+
value: data,
|
|
178
|
+
cachedAt: 1000000 - 10000,
|
|
179
|
+
expiresAt: 1000000 + 50000,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
await AsyncStorage.setItem(key, JSON.stringify(cachedValue));
|
|
183
|
+
|
|
184
|
+
const { result, waitForNextUpdate } = renderHook(() =>
|
|
185
|
+
usePersistentCache(key)
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
await act(async () => {
|
|
189
|
+
await waitForNextUpdate();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Clear data
|
|
193
|
+
await act(async () => {
|
|
194
|
+
await result.current.clearData();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(result.current.data).toBeNull();
|
|
198
|
+
expect(result.current.isExpired).toBe(true);
|
|
199
|
+
|
|
200
|
+
// Verify storage
|
|
201
|
+
const stored = await AsyncStorage.getItem(key);
|
|
202
|
+
expect(stored).toBeNull();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should not clear when disabled', async () => {
|
|
206
|
+
const key = 'test-cache';
|
|
207
|
+
const data = { test: 'value' };
|
|
208
|
+
const cachedValue = {
|
|
209
|
+
value: data,
|
|
210
|
+
cachedAt: 1000000 - 10000,
|
|
211
|
+
expiresAt: 1000000 + 50000,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
await AsyncStorage.setItem(key, JSON.stringify(cachedValue));
|
|
215
|
+
|
|
216
|
+
const { result } = renderHook(() =>
|
|
217
|
+
usePersistentCache(key, { enabled: false })
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
await act(async () => {
|
|
221
|
+
await result.current.clearData();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Storage should not be cleared
|
|
225
|
+
const stored = await AsyncStorage.getItem(key);
|
|
226
|
+
expect(stored).toBe(JSON.stringify(cachedValue));
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('refresh', () => {
|
|
231
|
+
it('should refresh cache from storage', async () => {
|
|
232
|
+
const key = 'test-cache';
|
|
233
|
+
const data1 = { test: 'value1' };
|
|
234
|
+
const data2 = { test: 'value2' };
|
|
235
|
+
|
|
236
|
+
// Set initial cache
|
|
237
|
+
const cachedValue1 = {
|
|
238
|
+
value: data1,
|
|
239
|
+
timestamp: 1000000 - 10000,
|
|
240
|
+
ttl: 60000,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
await AsyncStorage.setItem(key, JSON.stringify(cachedValue1));
|
|
244
|
+
|
|
245
|
+
const { result, waitForNextUpdate } = renderHook(() =>
|
|
246
|
+
usePersistentCache(key)
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
await act(async () => {
|
|
250
|
+
await waitForNextUpdate();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(result.current.data).toEqual(data1);
|
|
254
|
+
|
|
255
|
+
// Update storage directly
|
|
256
|
+
const cachedValue2 = {
|
|
257
|
+
value: data2,
|
|
258
|
+
timestamp: 1000000 - 5000,
|
|
259
|
+
ttl: 60000,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
await AsyncStorage.setItem(key, JSON.stringify(cachedValue2));
|
|
263
|
+
|
|
264
|
+
// Refresh
|
|
265
|
+
await act(async () => {
|
|
266
|
+
await result.current.refresh();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
expect(result.current.data).toEqual(data2);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('Performance', () => {
|
|
274
|
+
it('should use singleton CacheStorageOperations', () => {
|
|
275
|
+
const { result: result1 } = renderHook(() =>
|
|
276
|
+
usePersistentCache('key1')
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const { result: result2 } = renderHook(() =>
|
|
280
|
+
usePersistentCache('key2')
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
// Both hooks should use the same instance
|
|
284
|
+
expect(result1.current.setData).toBe(result2.current.setData);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('should not re-run effect when options change', async () => {
|
|
288
|
+
const key = 'test-cache';
|
|
289
|
+
const data = { test: 'value' };
|
|
290
|
+
const cachedValue = {
|
|
291
|
+
value: data,
|
|
292
|
+
cachedAt: 1000000 - 10000,
|
|
293
|
+
expiresAt: 1000000 + 50000,
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
await AsyncStorage.setItem(key, JSON.stringify(cachedValue));
|
|
297
|
+
|
|
298
|
+
const { result, rerender, waitForNextUpdate } = renderHook(
|
|
299
|
+
({ options }) => usePersistentCache(key, options),
|
|
300
|
+
{
|
|
301
|
+
initialProps: {
|
|
302
|
+
options: { ttl: 60000, version: 1, enabled: true }
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
await act(async () => {
|
|
308
|
+
await waitForNextUpdate();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const getItemCalls = (AsyncStorage.getItem as jest.Mock).mock.calls.length;
|
|
312
|
+
|
|
313
|
+
// Rerender with different options
|
|
314
|
+
rerender({
|
|
315
|
+
options: { ttl: 120000, version: 2, enabled: true }
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Should not trigger another storage read
|
|
319
|
+
expect((AsyncStorage.getItem as jest.Mock).mock.calls.length).toBe(getItemCalls);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should re-run effect when key changes', async () => {
|
|
323
|
+
const key1 = 'key1';
|
|
324
|
+
const key2 = 'key2';
|
|
325
|
+
const data1 = { test: 'value1' };
|
|
326
|
+
const data2 = { test: 'value2' };
|
|
327
|
+
|
|
328
|
+
const cachedValue1 = {
|
|
329
|
+
value: data1,
|
|
330
|
+
timestamp: 1000000 - 10000,
|
|
331
|
+
ttl: 60000,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
const cachedValue2 = {
|
|
335
|
+
value: data2,
|
|
336
|
+
timestamp: 1000000 - 10000,
|
|
337
|
+
ttl: 60000,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
await AsyncStorage.setItem(key1, JSON.stringify(cachedValue1));
|
|
341
|
+
await AsyncStorage.setItem(key2, JSON.stringify(cachedValue2));
|
|
342
|
+
|
|
343
|
+
const { result, rerender, waitForNextUpdate } = renderHook(
|
|
344
|
+
({ key }) => usePersistentCache(key),
|
|
345
|
+
{ initialProps: { key: key1 } }
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
await act(async () => {
|
|
349
|
+
await waitForNextUpdate();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
expect(result.current.data).toEqual(data1);
|
|
353
|
+
|
|
354
|
+
// Change key
|
|
355
|
+
rerender({ key: key2 });
|
|
356
|
+
|
|
357
|
+
await act(async () => {
|
|
358
|
+
await waitForNextUpdate();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
expect(result.current.data).toEqual(data2);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
describe('Error Handling', () => {
|
|
366
|
+
it('should handle storage read error', async () => {
|
|
367
|
+
const key = 'test-cache';
|
|
368
|
+
|
|
369
|
+
// Mock storage error
|
|
370
|
+
(AsyncStorage.getItem as jest.Mock).mockRejectedValue(new Error('Storage error'));
|
|
371
|
+
|
|
372
|
+
const { result, waitForNextUpdate } = renderHook(() =>
|
|
373
|
+
usePersistentCache(key)
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
await act(async () => {
|
|
377
|
+
await waitForNextUpdate();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
expect(result.current.data).toBeNull();
|
|
381
|
+
expect(result.current.isLoading).toBe(false);
|
|
382
|
+
expect(result.current.isExpired).toBe(true);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should handle invalid cache data', async () => {
|
|
386
|
+
const key = 'test-cache';
|
|
387
|
+
|
|
388
|
+
// Set invalid JSON
|
|
389
|
+
await AsyncStorage.setItem(key, 'invalid-json');
|
|
390
|
+
|
|
391
|
+
const { result, waitForNextUpdate } = renderHook(() =>
|
|
392
|
+
usePersistentCache(key)
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
await act(async () => {
|
|
396
|
+
await waitForNextUpdate();
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
expect(result.current.data).toBeNull();
|
|
400
|
+
expect(result.current.isLoading).toBe(false);
|
|
401
|
+
expect(result.current.isExpired).toBe(true);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
});
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useStorage Hook Tests
|
|
3
|
+
*
|
|
4
|
+
* Unit tests for useStorage hook
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { renderHook, act } from '@testing-library/react-hooks';
|
|
8
|
+
import { useStorage } from '../useStorage';
|
|
9
|
+
import { AsyncStorage } from '../../__tests__/mocks/asyncStorage.mock';
|
|
10
|
+
|
|
11
|
+
describe('useStorage Hook', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
(AsyncStorage as any).__clear();
|
|
14
|
+
jest.clearAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe('getItem', () => {
|
|
18
|
+
it('should get item from storage', async () => {
|
|
19
|
+
const key = 'test-key';
|
|
20
|
+
const defaultValue = 'default';
|
|
21
|
+
const expectedValue = 'test-value';
|
|
22
|
+
|
|
23
|
+
await AsyncStorage.setItem(key, JSON.stringify(expectedValue));
|
|
24
|
+
|
|
25
|
+
const { result } = renderHook(() => useStorage());
|
|
26
|
+
|
|
27
|
+
const value = await result.current.getItem(key, defaultValue);
|
|
28
|
+
|
|
29
|
+
expect(value).toBe(expectedValue);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should return default value for missing key', async () => {
|
|
33
|
+
const key = 'missing-key';
|
|
34
|
+
const defaultValue = 'default';
|
|
35
|
+
|
|
36
|
+
const { result } = renderHook(() => useStorage());
|
|
37
|
+
|
|
38
|
+
const value = await result.current.getItem(key, defaultValue);
|
|
39
|
+
|
|
40
|
+
expect(value).toBe(defaultValue);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should handle StorageKey enum', async () => {
|
|
44
|
+
const key = '@ui_preferences';
|
|
45
|
+
const defaultValue = 'default';
|
|
46
|
+
const expectedValue = 'test-value';
|
|
47
|
+
|
|
48
|
+
await AsyncStorage.setItem(key, JSON.stringify(expectedValue));
|
|
49
|
+
|
|
50
|
+
const { result } = renderHook(() => useStorage());
|
|
51
|
+
|
|
52
|
+
const value = await result.current.getItem(key as any, defaultValue);
|
|
53
|
+
|
|
54
|
+
expect(value).toBe(expectedValue);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('setItem', () => {
|
|
59
|
+
it('should set item to storage', async () => {
|
|
60
|
+
const key = 'test-key';
|
|
61
|
+
const value = 'test-value';
|
|
62
|
+
|
|
63
|
+
const { result } = renderHook(() => useStorage());
|
|
64
|
+
|
|
65
|
+
const success = await result.current.setItem(key, value);
|
|
66
|
+
|
|
67
|
+
expect(success).toBe(true);
|
|
68
|
+
|
|
69
|
+
// Verify storage
|
|
70
|
+
const stored = await AsyncStorage.getItem(key);
|
|
71
|
+
expect(JSON.parse(stored!)).toBe(value);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should return false on failure', async () => {
|
|
75
|
+
const key = 'test-key';
|
|
76
|
+
const value = { circular: {} };
|
|
77
|
+
value.circular = value; // Create circular reference
|
|
78
|
+
|
|
79
|
+
const { result } = renderHook(() => useStorage());
|
|
80
|
+
|
|
81
|
+
const success = await result.current.setItem(key, value);
|
|
82
|
+
|
|
83
|
+
expect(success).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('getString', () => {
|
|
88
|
+
it('should get string from storage', async () => {
|
|
89
|
+
const key = 'test-key';
|
|
90
|
+
const defaultValue = 'default';
|
|
91
|
+
const expectedValue = 'test-string';
|
|
92
|
+
|
|
93
|
+
await AsyncStorage.setItem(key, expectedValue);
|
|
94
|
+
|
|
95
|
+
const { result } = renderHook(() => useStorage());
|
|
96
|
+
|
|
97
|
+
const value = await result.current.getString(key, defaultValue);
|
|
98
|
+
|
|
99
|
+
expect(value).toBe(expectedValue);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('setString', () => {
|
|
104
|
+
it('should set string to storage', async () => {
|
|
105
|
+
const key = 'test-key';
|
|
106
|
+
const value = 'test-string';
|
|
107
|
+
|
|
108
|
+
const { result } = renderHook(() => useStorage());
|
|
109
|
+
|
|
110
|
+
const success = await result.current.setString(key, value);
|
|
111
|
+
|
|
112
|
+
expect(success).toBe(true);
|
|
113
|
+
|
|
114
|
+
// Verify storage
|
|
115
|
+
const stored = await AsyncStorage.getItem(key);
|
|
116
|
+
expect(stored).toBe(value);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('removeItem', () => {
|
|
121
|
+
it('should remove item from storage', async () => {
|
|
122
|
+
const key = 'test-key';
|
|
123
|
+
|
|
124
|
+
// Setup
|
|
125
|
+
await AsyncStorage.setItem(key, 'test-value');
|
|
126
|
+
|
|
127
|
+
const { result } = renderHook(() => useStorage());
|
|
128
|
+
|
|
129
|
+
const success = await result.current.removeItem(key);
|
|
130
|
+
|
|
131
|
+
expect(success).toBe(true);
|
|
132
|
+
|
|
133
|
+
// Verify removal
|
|
134
|
+
const stored = await AsyncStorage.getItem(key);
|
|
135
|
+
expect(stored).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('hasItem', () => {
|
|
140
|
+
it('should return true for existing item', async () => {
|
|
141
|
+
const key = 'test-key';
|
|
142
|
+
|
|
143
|
+
await AsyncStorage.setItem(key, 'test-value');
|
|
144
|
+
|
|
145
|
+
const { result } = renderHook(() => useStorage());
|
|
146
|
+
|
|
147
|
+
const exists = await result.current.hasItem(key);
|
|
148
|
+
|
|
149
|
+
expect(exists).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should return false for missing item', async () => {
|
|
153
|
+
const key = 'missing-key';
|
|
154
|
+
|
|
155
|
+
const { result } = renderHook(() => useStorage());
|
|
156
|
+
|
|
157
|
+
const exists = await result.current.hasItem(key);
|
|
158
|
+
|
|
159
|
+
expect(exists).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('clearAll', () => {
|
|
164
|
+
it('should clear all storage', async () => {
|
|
165
|
+
// Setup
|
|
166
|
+
await AsyncStorage.setItem('key1', 'value1');
|
|
167
|
+
await AsyncStorage.setItem('key2', 'value2');
|
|
168
|
+
|
|
169
|
+
const { result } = renderHook(() => useStorage());
|
|
170
|
+
|
|
171
|
+
const success = await result.current.clearAll();
|
|
172
|
+
|
|
173
|
+
expect(success).toBe(true);
|
|
174
|
+
|
|
175
|
+
// Verify clear
|
|
176
|
+
const keys = await AsyncStorage.getAllKeys();
|
|
177
|
+
expect(keys).toHaveLength(0);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('getItemWithResult', () => {
|
|
182
|
+
it('should return full result object', async () => {
|
|
183
|
+
const key = 'test-key';
|
|
184
|
+
const defaultValue = 'default';
|
|
185
|
+
const expectedValue = 'test-value';
|
|
186
|
+
|
|
187
|
+
await AsyncStorage.setItem(key, JSON.stringify(expectedValue));
|
|
188
|
+
|
|
189
|
+
const { result } = renderHook(() => useStorage());
|
|
190
|
+
|
|
191
|
+
const storageResult = await result.current.getItemWithResult(key, defaultValue);
|
|
192
|
+
|
|
193
|
+
expect(storageResult.success).toBe(true);
|
|
194
|
+
expect(storageResult.data).toBe(expectedValue);
|
|
195
|
+
expect(storageResult.error).toBeUndefined();
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe('Performance', () => {
|
|
200
|
+
it('should memoize return object', () => {
|
|
201
|
+
const { result, rerender } = renderHook(() => useStorage());
|
|
202
|
+
|
|
203
|
+
const firstCall = result.current;
|
|
204
|
+
rerender();
|
|
205
|
+
const secondCall = result.current;
|
|
206
|
+
|
|
207
|
+
// Should be the same reference (memoized)
|
|
208
|
+
expect(firstCall).toBe(secondCall);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should not recreate functions on re-render', () => {
|
|
212
|
+
const { result, rerender } = renderHook(() => useStorage());
|
|
213
|
+
|
|
214
|
+
const firstFunctions = {
|
|
215
|
+
getItem: result.current.getItem,
|
|
216
|
+
setItem: result.current.setItem,
|
|
217
|
+
getString: result.current.getString,
|
|
218
|
+
setString: result.current.setString,
|
|
219
|
+
removeItem: result.current.removeItem,
|
|
220
|
+
hasItem: result.current.hasItem,
|
|
221
|
+
clearAll: result.current.clearAll,
|
|
222
|
+
getItemWithResult: result.current.getItemWithResult,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
rerender();
|
|
226
|
+
|
|
227
|
+
const secondFunctions = {
|
|
228
|
+
getItem: result.current.getItem,
|
|
229
|
+
setItem: result.current.setItem,
|
|
230
|
+
getString: result.current.getString,
|
|
231
|
+
setString: result.current.setString,
|
|
232
|
+
removeItem: result.current.removeItem,
|
|
233
|
+
hasItem: result.current.hasItem,
|
|
234
|
+
clearAll: result.current.clearAll,
|
|
235
|
+
getItemWithResult: result.current.getItemWithResult,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// All functions should be the same reference
|
|
239
|
+
Object.keys(firstFunctions).forEach(key => {
|
|
240
|
+
expect(firstFunctions[key as keyof typeof firstFunctions]).toBe(
|
|
241
|
+
secondFunctions[key as keyof typeof secondFunctions]
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|