shelflife-react-hooks 1.0.19 → 1.0.20

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.
Files changed (44) hide show
  1. package/dist/index.cjs.js +22 -0
  2. package/dist/index.cjs.js.map +1 -1
  3. package/dist/index.d.cts +2 -0
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.esm.js +22 -0
  6. package/dist/index.esm.js.map +1 -1
  7. package/package.json +36 -36
  8. package/src/context/AuthContext.tsx +161 -161
  9. package/src/context/InviteContext.tsx +74 -74
  10. package/src/context/ProductContext.tsx +131 -121
  11. package/src/context/RunningLowContext.tsx +100 -100
  12. package/src/context/ShoppingListContext.tsx +76 -76
  13. package/src/context/StorageContext.tsx +105 -105
  14. package/src/context/StorageItemContext.tsx +157 -157
  15. package/src/context/StorageMemberContext.tsx +84 -84
  16. package/src/context/UserContext.tsx +109 -109
  17. package/src/context/__tests__/contexts.test.tsx +370 -370
  18. package/src/context/api/authApi.ts +155 -155
  19. package/src/context/api/inviteApi.ts +65 -65
  20. package/src/context/api/productApi.ts +223 -201
  21. package/src/context/api/requestState.ts +24 -24
  22. package/src/context/api/runningLowApi.ts +141 -141
  23. package/src/context/api/shoppingListApi.ts +161 -161
  24. package/src/context/api/storageApi.ts +166 -166
  25. package/src/context/api/storageItemApi.ts +260 -260
  26. package/src/context/api/storageMemberApi.ts +84 -84
  27. package/src/context/api/userApi.ts +161 -161
  28. package/src/context/http.ts +22 -22
  29. package/src/index.ts +21 -21
  30. package/src/type/PaginatedResponse.ts +8 -8
  31. package/src/type/auth.ts +79 -79
  32. package/src/type/base.ts +21 -21
  33. package/src/type/item.ts +12 -12
  34. package/src/type/member.ts +6 -6
  35. package/src/type/models.ts +56 -56
  36. package/src/type/product.ts +11 -11
  37. package/src/type/requests.ts +60 -60
  38. package/src/type/runninglow.ts +13 -13
  39. package/src/type/shoppingList.ts +13 -13
  40. package/src/type/storage.ts +7 -7
  41. package/src/type/user.ts +11 -11
  42. package/tsconfig.json +46 -46
  43. package/tsup.config.ts +10 -10
  44. package/vitest.config.ts +8 -8
@@ -1,370 +1,370 @@
1
- import React, { useEffect } from 'react';
2
- import { render, waitFor } from '@testing-library/react';
3
- import { describe, expect, it, vi, beforeEach } from 'vitest';
4
-
5
- import {
6
- AuthProvider,
7
- InviteProvider,
8
- ProductProvider,
9
- RunningLowProvider,
10
- StorageItemProvider,
11
- StorageMemberProvider,
12
- StorageProvider,
13
- UserProvider,
14
- useAuth,
15
- useInvites,
16
- useProducts,
17
- useRunningLow,
18
- useStorageItems,
19
- useStorageMembers,
20
- useStorages,
21
- useUsers
22
- } from '../../index.js';
23
-
24
- const jsonResponse = (data: unknown, status = 200) => ({
25
- ok: status >= 200 && status < 300,
26
- status,
27
- headers: {
28
- get: () => 'application/json'
29
- },
30
- json: async () => data,
31
- arrayBuffer: async () => new ArrayBuffer(4)
32
- });
33
-
34
- const emptyResponse = (status = 200) => ({
35
- ok: status >= 200 && status < 300,
36
- status,
37
- headers: {
38
- get: () => null
39
- },
40
- json: async () => ({}),
41
- arrayBuffer: async () => new ArrayBuffer(0)
42
- });
43
-
44
- const mockFetchQueue = (responses: Array<ReturnType<typeof jsonResponse>>) => {
45
- const queue = [...responses];
46
- global.fetch = vi.fn(async () => queue.shift() ?? emptyResponse()) as unknown as typeof fetch;
47
- };
48
-
49
- const baseUrl = 'http://localhost:3000';
50
-
51
- const AuthProbe = ({ onReady }: { onReady: (value: ReturnType<typeof useAuth>) => void }) => {
52
- const value = useAuth();
53
- useEffect(() => {
54
- onReady(value);
55
- }, [onReady, value]);
56
- return null;
57
- };
58
-
59
- const ProviderProbe = <T,>({
60
- useHook,
61
- onReady
62
- }: {
63
- useHook: () => T;
64
- onReady: (value: T) => void;
65
- }) => {
66
- const value = useHook();
67
- useEffect(() => {
68
- onReady(value);
69
- }, [onReady, value]);
70
- return null;
71
- };
72
-
73
- describe('AuthContext', () => {
74
- beforeEach(() => {
75
- vi.restoreAllMocks();
76
- });
77
-
78
- it('logs in and stores token + user', async () => {
79
- mockFetchQueue([
80
- jsonResponse({ token: 'token-123' }),
81
- jsonResponse({ id: 1, username: 'jane', admin: false })
82
- ]);
83
-
84
- const ctxRef: { current?: ReturnType<typeof useAuth> } = {};
85
-
86
- render(
87
- <AuthProvider baseUrl={baseUrl}>
88
- <AuthProbe onReady={(value) => {
89
- ctxRef.current = value;
90
- }} />
91
- </AuthProvider>
92
- );
93
-
94
- await waitFor(() => {
95
- expect(ctxRef.current).toBeDefined();
96
- });
97
-
98
- const ctx = ctxRef.current;
99
- if (!ctx) {
100
- throw new Error('Auth context not ready');
101
- }
102
-
103
- await ctx.login({ email: 'user@example.com', password: 'pass' });
104
-
105
- await waitFor(() => {
106
- expect(ctxRef.current?.token).toBe('token-123');
107
- expect(ctxRef.current?.user).toEqual({ id: 1, username: 'jane', admin: false });
108
- });
109
- });
110
- });
111
-
112
- describe('InviteContext', () => {
113
- it('fetches invites', async () => {
114
- mockFetchQueue([
115
- jsonResponse([
116
- { id: 10, storage: { id: 1, name: 'Kitchen', owner: { id: 2, username: 'sam', admin: false } }, user: { id: 3, username: 'alex', admin: false }, accepted: false }
117
- ])
118
- ]);
119
-
120
- const ctxRef: { current?: ReturnType<typeof useInvites> } = {};
121
-
122
- render(
123
- <AuthProvider baseUrl={baseUrl} initialToken="token">
124
- <InviteProvider baseUrl={baseUrl}>
125
- <ProviderProbe useHook={useInvites} onReady={(value) => { ctxRef.current = value; }} />
126
- </InviteProvider>
127
- </AuthProvider>
128
- );
129
-
130
- await waitFor(() => {
131
- expect(ctxRef.current).toBeDefined();
132
- });
133
-
134
- const ctx = ctxRef.current;
135
- if (!ctx) {
136
- throw new Error('Invite context not ready');
137
- }
138
-
139
- await ctx.fetchInvites();
140
-
141
- await waitFor(() => {
142
- expect(ctxRef.current?.invites).toHaveLength(1);
143
- });
144
- });
145
- });
146
-
147
- describe('ProductContext', () => {
148
- it('fetches products', async () => {
149
- mockFetchQueue([
150
- jsonResponse([
151
- { id: 1, ownerId: 2, name: 'Milk', category: 'Dairy', expirationDaysDelta: 7, barcode: null }
152
- ])
153
- ]);
154
-
155
- const ctxRef: { current?: ReturnType<typeof useProducts> } = {};
156
-
157
- render(
158
- <AuthProvider baseUrl={baseUrl} initialToken="token">
159
- <ProductProvider baseUrl={baseUrl}>
160
- <ProviderProbe useHook={useProducts} onReady={(value) => { ctxRef.current = value; }} />
161
- </ProductProvider>
162
- </AuthProvider>
163
- );
164
-
165
- await waitFor(() => {
166
- expect(ctxRef.current).toBeDefined();
167
- });
168
-
169
- const ctx = ctxRef.current;
170
- if (!ctx) {
171
- throw new Error('Product context not ready');
172
- }
173
-
174
- await ctx.fetchProducts();
175
-
176
- await waitFor(() => {
177
- expect(ctxRef.current?.products).toHaveLength(1);
178
- });
179
- });
180
- });
181
-
182
- describe('RunningLowContext', () => {
183
- it('fetches running low settings', async () => {
184
- mockFetchQueue([
185
- jsonResponse([
186
- {
187
- id: 5,
188
- storage: { id: 1, name: 'Pantry', owner: { id: 2, username: 'sam', admin: false } },
189
- product: { id: 3, ownerId: 2, name: 'Rice', category: 'Grains', expirationDaysDelta: 365, barcode: null },
190
- runningLow: 2
191
- }
192
- ])
193
- ]);
194
-
195
- const ctxRef: { current?: ReturnType<typeof useRunningLow> } = {};
196
-
197
- render(
198
- <AuthProvider baseUrl={baseUrl} initialToken="token">
199
- <RunningLowProvider baseUrl={baseUrl}>
200
- <ProviderProbe useHook={useRunningLow} onReady={(value) => { ctxRef.current = value; }} />
201
- </RunningLowProvider>
202
- </AuthProvider>
203
- );
204
-
205
- await waitFor(() => {
206
- expect(ctxRef.current).toBeDefined();
207
- });
208
-
209
- const ctx = ctxRef.current;
210
- if (!ctx) {
211
- throw new Error('Running low context not ready');
212
- }
213
-
214
- await ctx.fetchSettings(1);
215
-
216
- await waitFor(() => {
217
- expect(ctxRef.current?.settings).toHaveLength(1);
218
- });
219
- });
220
- });
221
-
222
- describe('StorageContext', () => {
223
- it('fetches storages', async () => {
224
- mockFetchQueue([
225
- jsonResponse([
226
- { id: 1, name: 'Garage', owner: { id: 2, username: 'sam', admin: false } }
227
- ])
228
- ]);
229
-
230
- const ctxRef: { current?: ReturnType<typeof useStorages> } = {};
231
-
232
- render(
233
- <AuthProvider baseUrl={baseUrl} initialToken="token">
234
- <StorageProvider baseUrl={baseUrl}>
235
- <ProviderProbe useHook={useStorages} onReady={(value) => { ctxRef.current = value; }} />
236
- </StorageProvider>
237
- </AuthProvider>
238
- );
239
-
240
- await waitFor(() => {
241
- expect(ctxRef.current).toBeDefined();
242
- });
243
-
244
- const ctx = ctxRef.current;
245
- if (!ctx) {
246
- throw new Error('Storage context not ready');
247
- }
248
-
249
- await ctx.fetchStorages();
250
-
251
- await waitFor(() => {
252
- expect(ctxRef.current?.storages).toHaveLength(1);
253
- });
254
- });
255
- });
256
-
257
- describe('StorageItemContext', () => {
258
- it('fetches storage items', async () => {
259
- mockFetchQueue([
260
- jsonResponse([
261
- {
262
- id: 4,
263
- product: { id: 2, ownerId: 1, name: 'Bread', category: 'Bakery', expirationDaysDelta: 3, barcode: null },
264
- expiresAt: '2025-02-01',
265
- createdAt: '2025-01-01T00:00:00'
266
- }
267
- ])
268
- ]);
269
-
270
- const ctxRef: { current?: ReturnType<typeof useStorageItems> } = {};
271
-
272
- render(
273
- <AuthProvider baseUrl={baseUrl} initialToken="token">
274
- <StorageItemProvider baseUrl={baseUrl}>
275
- <ProviderProbe useHook={useStorageItems} onReady={(value) => { ctxRef.current = value; }} />
276
- </StorageItemProvider>
277
- </AuthProvider>
278
- );
279
-
280
- await waitFor(() => {
281
- expect(ctxRef.current).toBeDefined();
282
- });
283
-
284
- const ctx = ctxRef.current;
285
- if (!ctx) {
286
- throw new Error('Storage item context not ready');
287
- }
288
-
289
- await ctx.fetchItems(1);
290
-
291
- await waitFor(() => {
292
- expect(ctxRef.current?.items).toHaveLength(1);
293
- });
294
- });
295
- });
296
-
297
- describe('StorageMemberContext', () => {
298
- it('fetches members', async () => {
299
- mockFetchQueue([
300
- jsonResponse([
301
- {
302
- id: 7,
303
- storage: { id: 1, name: 'Cellar', owner: { id: 2, username: 'sam', admin: false } },
304
- user: { id: 3, username: 'alex', admin: false },
305
- accepted: true
306
- }
307
- ])
308
- ]);
309
-
310
- const ctxRef: { current?: ReturnType<typeof useStorageMembers> } = {};
311
-
312
- render(
313
- <AuthProvider baseUrl={baseUrl} initialToken="token">
314
- <StorageMemberProvider baseUrl={baseUrl}>
315
- <ProviderProbe useHook={useStorageMembers} onReady={(value) => { ctxRef.current = value; }} />
316
- </StorageMemberProvider>
317
- </AuthProvider>
318
- );
319
-
320
- await waitFor(() => {
321
- expect(ctxRef.current).toBeDefined();
322
- });
323
-
324
- const ctx = ctxRef.current;
325
- if (!ctx) {
326
- throw new Error('Storage member context not ready');
327
- }
328
-
329
- await ctx.fetchMembers(1);
330
-
331
- await waitFor(() => {
332
- expect(ctxRef.current?.members).toHaveLength(1);
333
- });
334
- });
335
- });
336
-
337
- describe('UserContext', () => {
338
- it('fetches users', async () => {
339
- mockFetchQueue([
340
- jsonResponse([
341
- { id: 1, username: 'sam', admin: false }
342
- ])
343
- ]);
344
-
345
- const ctxRef: { current?: ReturnType<typeof useUsers> } = {};
346
-
347
- render(
348
- <AuthProvider baseUrl={baseUrl} initialToken="token">
349
- <UserProvider baseUrl={baseUrl}>
350
- <ProviderProbe useHook={useUsers} onReady={(value) => { ctxRef.current = value; }} />
351
- </UserProvider>
352
- </AuthProvider>
353
- );
354
-
355
- await waitFor(() => {
356
- expect(ctxRef.current).toBeDefined();
357
- });
358
-
359
- const ctx = ctxRef.current;
360
- if (!ctx) {
361
- throw new Error('User context not ready');
362
- }
363
-
364
- await ctx.fetchUsers();
365
-
366
- await waitFor(() => {
367
- expect(ctxRef.current?.users).toHaveLength(1);
368
- });
369
- });
370
- });
1
+ import React, { useEffect } from 'react';
2
+ import { render, waitFor } from '@testing-library/react';
3
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
4
+
5
+ import {
6
+ AuthProvider,
7
+ InviteProvider,
8
+ ProductProvider,
9
+ RunningLowProvider,
10
+ StorageItemProvider,
11
+ StorageMemberProvider,
12
+ StorageProvider,
13
+ UserProvider,
14
+ useAuth,
15
+ useInvites,
16
+ useProducts,
17
+ useRunningLow,
18
+ useStorageItems,
19
+ useStorageMembers,
20
+ useStorages,
21
+ useUsers
22
+ } from '../../index.js';
23
+
24
+ const jsonResponse = (data: unknown, status = 200) => ({
25
+ ok: status >= 200 && status < 300,
26
+ status,
27
+ headers: {
28
+ get: () => 'application/json'
29
+ },
30
+ json: async () => data,
31
+ arrayBuffer: async () => new ArrayBuffer(4)
32
+ });
33
+
34
+ const emptyResponse = (status = 200) => ({
35
+ ok: status >= 200 && status < 300,
36
+ status,
37
+ headers: {
38
+ get: () => null
39
+ },
40
+ json: async () => ({}),
41
+ arrayBuffer: async () => new ArrayBuffer(0)
42
+ });
43
+
44
+ const mockFetchQueue = (responses: Array<ReturnType<typeof jsonResponse>>) => {
45
+ const queue = [...responses];
46
+ global.fetch = vi.fn(async () => queue.shift() ?? emptyResponse()) as unknown as typeof fetch;
47
+ };
48
+
49
+ const baseUrl = 'http://localhost:3000';
50
+
51
+ const AuthProbe = ({ onReady }: { onReady: (value: ReturnType<typeof useAuth>) => void }) => {
52
+ const value = useAuth();
53
+ useEffect(() => {
54
+ onReady(value);
55
+ }, [onReady, value]);
56
+ return null;
57
+ };
58
+
59
+ const ProviderProbe = <T,>({
60
+ useHook,
61
+ onReady
62
+ }: {
63
+ useHook: () => T;
64
+ onReady: (value: T) => void;
65
+ }) => {
66
+ const value = useHook();
67
+ useEffect(() => {
68
+ onReady(value);
69
+ }, [onReady, value]);
70
+ return null;
71
+ };
72
+
73
+ describe('AuthContext', () => {
74
+ beforeEach(() => {
75
+ vi.restoreAllMocks();
76
+ });
77
+
78
+ it('logs in and stores token + user', async () => {
79
+ mockFetchQueue([
80
+ jsonResponse({ token: 'token-123' }),
81
+ jsonResponse({ id: 1, username: 'jane', admin: false })
82
+ ]);
83
+
84
+ const ctxRef: { current?: ReturnType<typeof useAuth> } = {};
85
+
86
+ render(
87
+ <AuthProvider baseUrl={baseUrl}>
88
+ <AuthProbe onReady={(value) => {
89
+ ctxRef.current = value;
90
+ }} />
91
+ </AuthProvider>
92
+ );
93
+
94
+ await waitFor(() => {
95
+ expect(ctxRef.current).toBeDefined();
96
+ });
97
+
98
+ const ctx = ctxRef.current;
99
+ if (!ctx) {
100
+ throw new Error('Auth context not ready');
101
+ }
102
+
103
+ await ctx.login({ email: 'user@example.com', password: 'pass' });
104
+
105
+ await waitFor(() => {
106
+ expect(ctxRef.current?.token).toBe('token-123');
107
+ expect(ctxRef.current?.user).toEqual({ id: 1, username: 'jane', admin: false });
108
+ });
109
+ });
110
+ });
111
+
112
+ describe('InviteContext', () => {
113
+ it('fetches invites', async () => {
114
+ mockFetchQueue([
115
+ jsonResponse([
116
+ { id: 10, storage: { id: 1, name: 'Kitchen', owner: { id: 2, username: 'sam', admin: false } }, user: { id: 3, username: 'alex', admin: false }, accepted: false }
117
+ ])
118
+ ]);
119
+
120
+ const ctxRef: { current?: ReturnType<typeof useInvites> } = {};
121
+
122
+ render(
123
+ <AuthProvider baseUrl={baseUrl} initialToken="token">
124
+ <InviteProvider baseUrl={baseUrl}>
125
+ <ProviderProbe useHook={useInvites} onReady={(value) => { ctxRef.current = value; }} />
126
+ </InviteProvider>
127
+ </AuthProvider>
128
+ );
129
+
130
+ await waitFor(() => {
131
+ expect(ctxRef.current).toBeDefined();
132
+ });
133
+
134
+ const ctx = ctxRef.current;
135
+ if (!ctx) {
136
+ throw new Error('Invite context not ready');
137
+ }
138
+
139
+ await ctx.fetchInvites();
140
+
141
+ await waitFor(() => {
142
+ expect(ctxRef.current?.invites).toHaveLength(1);
143
+ });
144
+ });
145
+ });
146
+
147
+ describe('ProductContext', () => {
148
+ it('fetches products', async () => {
149
+ mockFetchQueue([
150
+ jsonResponse([
151
+ { id: 1, ownerId: 2, name: 'Milk', category: 'Dairy', expirationDaysDelta: 7, barcode: null }
152
+ ])
153
+ ]);
154
+
155
+ const ctxRef: { current?: ReturnType<typeof useProducts> } = {};
156
+
157
+ render(
158
+ <AuthProvider baseUrl={baseUrl} initialToken="token">
159
+ <ProductProvider baseUrl={baseUrl}>
160
+ <ProviderProbe useHook={useProducts} onReady={(value) => { ctxRef.current = value; }} />
161
+ </ProductProvider>
162
+ </AuthProvider>
163
+ );
164
+
165
+ await waitFor(() => {
166
+ expect(ctxRef.current).toBeDefined();
167
+ });
168
+
169
+ const ctx = ctxRef.current;
170
+ if (!ctx) {
171
+ throw new Error('Product context not ready');
172
+ }
173
+
174
+ await ctx.fetchProducts();
175
+
176
+ await waitFor(() => {
177
+ expect(ctxRef.current?.products).toHaveLength(1);
178
+ });
179
+ });
180
+ });
181
+
182
+ describe('RunningLowContext', () => {
183
+ it('fetches running low settings', async () => {
184
+ mockFetchQueue([
185
+ jsonResponse([
186
+ {
187
+ id: 5,
188
+ storage: { id: 1, name: 'Pantry', owner: { id: 2, username: 'sam', admin: false } },
189
+ product: { id: 3, ownerId: 2, name: 'Rice', category: 'Grains', expirationDaysDelta: 365, barcode: null },
190
+ runningLow: 2
191
+ }
192
+ ])
193
+ ]);
194
+
195
+ const ctxRef: { current?: ReturnType<typeof useRunningLow> } = {};
196
+
197
+ render(
198
+ <AuthProvider baseUrl={baseUrl} initialToken="token">
199
+ <RunningLowProvider baseUrl={baseUrl}>
200
+ <ProviderProbe useHook={useRunningLow} onReady={(value) => { ctxRef.current = value; }} />
201
+ </RunningLowProvider>
202
+ </AuthProvider>
203
+ );
204
+
205
+ await waitFor(() => {
206
+ expect(ctxRef.current).toBeDefined();
207
+ });
208
+
209
+ const ctx = ctxRef.current;
210
+ if (!ctx) {
211
+ throw new Error('Running low context not ready');
212
+ }
213
+
214
+ await ctx.fetchSettings(1);
215
+
216
+ await waitFor(() => {
217
+ expect(ctxRef.current?.settings).toHaveLength(1);
218
+ });
219
+ });
220
+ });
221
+
222
+ describe('StorageContext', () => {
223
+ it('fetches storages', async () => {
224
+ mockFetchQueue([
225
+ jsonResponse([
226
+ { id: 1, name: 'Garage', owner: { id: 2, username: 'sam', admin: false } }
227
+ ])
228
+ ]);
229
+
230
+ const ctxRef: { current?: ReturnType<typeof useStorages> } = {};
231
+
232
+ render(
233
+ <AuthProvider baseUrl={baseUrl} initialToken="token">
234
+ <StorageProvider baseUrl={baseUrl}>
235
+ <ProviderProbe useHook={useStorages} onReady={(value) => { ctxRef.current = value; }} />
236
+ </StorageProvider>
237
+ </AuthProvider>
238
+ );
239
+
240
+ await waitFor(() => {
241
+ expect(ctxRef.current).toBeDefined();
242
+ });
243
+
244
+ const ctx = ctxRef.current;
245
+ if (!ctx) {
246
+ throw new Error('Storage context not ready');
247
+ }
248
+
249
+ await ctx.fetchStorages();
250
+
251
+ await waitFor(() => {
252
+ expect(ctxRef.current?.storages).toHaveLength(1);
253
+ });
254
+ });
255
+ });
256
+
257
+ describe('StorageItemContext', () => {
258
+ it('fetches storage items', async () => {
259
+ mockFetchQueue([
260
+ jsonResponse([
261
+ {
262
+ id: 4,
263
+ product: { id: 2, ownerId: 1, name: 'Bread', category: 'Bakery', expirationDaysDelta: 3, barcode: null },
264
+ expiresAt: '2025-02-01',
265
+ createdAt: '2025-01-01T00:00:00'
266
+ }
267
+ ])
268
+ ]);
269
+
270
+ const ctxRef: { current?: ReturnType<typeof useStorageItems> } = {};
271
+
272
+ render(
273
+ <AuthProvider baseUrl={baseUrl} initialToken="token">
274
+ <StorageItemProvider baseUrl={baseUrl}>
275
+ <ProviderProbe useHook={useStorageItems} onReady={(value) => { ctxRef.current = value; }} />
276
+ </StorageItemProvider>
277
+ </AuthProvider>
278
+ );
279
+
280
+ await waitFor(() => {
281
+ expect(ctxRef.current).toBeDefined();
282
+ });
283
+
284
+ const ctx = ctxRef.current;
285
+ if (!ctx) {
286
+ throw new Error('Storage item context not ready');
287
+ }
288
+
289
+ await ctx.fetchItems(1);
290
+
291
+ await waitFor(() => {
292
+ expect(ctxRef.current?.items).toHaveLength(1);
293
+ });
294
+ });
295
+ });
296
+
297
+ describe('StorageMemberContext', () => {
298
+ it('fetches members', async () => {
299
+ mockFetchQueue([
300
+ jsonResponse([
301
+ {
302
+ id: 7,
303
+ storage: { id: 1, name: 'Cellar', owner: { id: 2, username: 'sam', admin: false } },
304
+ user: { id: 3, username: 'alex', admin: false },
305
+ accepted: true
306
+ }
307
+ ])
308
+ ]);
309
+
310
+ const ctxRef: { current?: ReturnType<typeof useStorageMembers> } = {};
311
+
312
+ render(
313
+ <AuthProvider baseUrl={baseUrl} initialToken="token">
314
+ <StorageMemberProvider baseUrl={baseUrl}>
315
+ <ProviderProbe useHook={useStorageMembers} onReady={(value) => { ctxRef.current = value; }} />
316
+ </StorageMemberProvider>
317
+ </AuthProvider>
318
+ );
319
+
320
+ await waitFor(() => {
321
+ expect(ctxRef.current).toBeDefined();
322
+ });
323
+
324
+ const ctx = ctxRef.current;
325
+ if (!ctx) {
326
+ throw new Error('Storage member context not ready');
327
+ }
328
+
329
+ await ctx.fetchMembers(1);
330
+
331
+ await waitFor(() => {
332
+ expect(ctxRef.current?.members).toHaveLength(1);
333
+ });
334
+ });
335
+ });
336
+
337
+ describe('UserContext', () => {
338
+ it('fetches users', async () => {
339
+ mockFetchQueue([
340
+ jsonResponse([
341
+ { id: 1, username: 'sam', admin: false }
342
+ ])
343
+ ]);
344
+
345
+ const ctxRef: { current?: ReturnType<typeof useUsers> } = {};
346
+
347
+ render(
348
+ <AuthProvider baseUrl={baseUrl} initialToken="token">
349
+ <UserProvider baseUrl={baseUrl}>
350
+ <ProviderProbe useHook={useUsers} onReady={(value) => { ctxRef.current = value; }} />
351
+ </UserProvider>
352
+ </AuthProvider>
353
+ );
354
+
355
+ await waitFor(() => {
356
+ expect(ctxRef.current).toBeDefined();
357
+ });
358
+
359
+ const ctx = ctxRef.current;
360
+ if (!ctx) {
361
+ throw new Error('User context not ready');
362
+ }
363
+
364
+ await ctx.fetchUsers();
365
+
366
+ await waitFor(() => {
367
+ expect(ctxRef.current?.users).toHaveLength(1);
368
+ });
369
+ });
370
+ });