@signalium/query 0.1.0 → 1.0.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.
Files changed (113) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cjs/EntityMap.js +2 -2
  3. package/dist/cjs/EntityMap.js.map +1 -1
  4. package/dist/cjs/NetworkManager.js +105 -0
  5. package/dist/cjs/NetworkManager.js.map +1 -0
  6. package/dist/cjs/QueryClient.js +390 -76
  7. package/dist/cjs/QueryClient.js.map +1 -1
  8. package/dist/cjs/QueryStore.js +295 -3
  9. package/dist/cjs/QueryStore.js.map +1 -1
  10. package/dist/cjs/index.js +16 -1
  11. package/dist/cjs/index.js.map +1 -1
  12. package/dist/cjs/package.json +3 -0
  13. package/dist/cjs/parseEntities.js +3 -0
  14. package/dist/cjs/parseEntities.js.map +1 -1
  15. package/dist/cjs/proxy.js +19 -0
  16. package/dist/cjs/proxy.js.map +1 -1
  17. package/dist/cjs/query.js +40 -2
  18. package/dist/cjs/query.js.map +1 -1
  19. package/dist/cjs/stores/async.js +6 -0
  20. package/dist/cjs/stores/async.js.map +1 -0
  21. package/dist/cjs/stores/sync.js +7 -0
  22. package/dist/cjs/stores/sync.js.map +1 -0
  23. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  24. package/dist/cjs/type-utils.js +3 -0
  25. package/dist/cjs/type-utils.js.map +1 -0
  26. package/dist/cjs/types.js +19 -1
  27. package/dist/cjs/types.js.map +1 -1
  28. package/dist/esm/EntityMap.js +3 -3
  29. package/dist/esm/EntityMap.js.map +1 -1
  30. package/dist/esm/NetworkManager.d.ts +48 -0
  31. package/dist/esm/NetworkManager.d.ts.map +1 -0
  32. package/dist/esm/NetworkManager.js +101 -0
  33. package/dist/esm/NetworkManager.js.map +1 -0
  34. package/dist/esm/QueryClient.d.ts +81 -25
  35. package/dist/esm/QueryClient.d.ts.map +1 -1
  36. package/dist/esm/QueryClient.js +390 -76
  37. package/dist/esm/QueryClient.js.map +1 -1
  38. package/dist/esm/QueryStore.d.ts +64 -2
  39. package/dist/esm/QueryStore.d.ts.map +1 -1
  40. package/dist/esm/QueryStore.js +293 -2
  41. package/dist/esm/QueryStore.js.map +1 -1
  42. package/dist/esm/index.d.ts +5 -3
  43. package/dist/esm/index.d.ts.map +1 -1
  44. package/dist/esm/index.js +3 -1
  45. package/dist/esm/index.js.map +1 -1
  46. package/dist/esm/parseEntities.d.ts.map +1 -1
  47. package/dist/esm/parseEntities.js +3 -0
  48. package/dist/esm/parseEntities.js.map +1 -1
  49. package/dist/esm/proxy.d.ts +6 -0
  50. package/dist/esm/proxy.d.ts.map +1 -1
  51. package/dist/esm/proxy.js +18 -0
  52. package/dist/esm/proxy.js.map +1 -1
  53. package/dist/esm/query.d.ts +30 -29
  54. package/dist/esm/query.d.ts.map +1 -1
  55. package/dist/esm/query.js +39 -3
  56. package/dist/esm/query.js.map +1 -1
  57. package/dist/esm/stores/async.d.ts +2 -0
  58. package/dist/esm/stores/async.d.ts.map +1 -0
  59. package/dist/esm/stores/async.js +2 -0
  60. package/dist/esm/stores/async.js.map +1 -0
  61. package/dist/esm/stores/sync.d.ts +2 -0
  62. package/dist/esm/stores/sync.d.ts.map +1 -0
  63. package/dist/esm/stores/sync.js +2 -0
  64. package/dist/esm/stores/sync.js.map +1 -0
  65. package/dist/esm/type-utils.d.ts +12 -0
  66. package/dist/esm/type-utils.d.ts.map +1 -0
  67. package/dist/esm/type-utils.js +2 -0
  68. package/dist/esm/type-utils.js.map +1 -0
  69. package/dist/esm/types.d.ts +62 -5
  70. package/dist/esm/types.d.ts.map +1 -1
  71. package/dist/esm/types.js +18 -0
  72. package/dist/esm/types.js.map +1 -1
  73. package/index.d.ts +1 -0
  74. package/package.json +25 -4
  75. package/stores/async.d.ts +1 -0
  76. package/stores/async.js +15 -0
  77. package/stores/sync.d.ts +1 -0
  78. package/stores/sync.js +15 -0
  79. package/.turbo/turbo-build.log +0 -12
  80. package/ENTITY_STORE_DESIGN.md +0 -386
  81. package/dist/tsconfig.esm.tsbuildinfo +0 -1
  82. package/src/EntityMap.ts +0 -63
  83. package/src/QueryClient.ts +0 -482
  84. package/src/QueryStore.ts +0 -322
  85. package/src/__tests__/caching-persistence.test.ts +0 -983
  86. package/src/__tests__/entity-system.test.ts +0 -556
  87. package/src/__tests__/gc-time.test.ts +0 -327
  88. package/src/__tests__/mock-fetch.test.ts +0 -186
  89. package/src/__tests__/parse-entities.test.ts +0 -425
  90. package/src/__tests__/path-interpolation.test.ts +0 -225
  91. package/src/__tests__/reactivity.test.ts +0 -424
  92. package/src/__tests__/refetch-interval.test.ts +0 -262
  93. package/src/__tests__/rest-query-api.test.ts +0 -568
  94. package/src/__tests__/stale-time.test.ts +0 -357
  95. package/src/__tests__/type-to-string.test.ts +0 -129
  96. package/src/__tests__/utils.ts +0 -258
  97. package/src/__tests__/validation-edge-cases.test.ts +0 -821
  98. package/src/errors.ts +0 -124
  99. package/src/index.ts +0 -7
  100. package/src/parseEntities.ts +0 -213
  101. package/src/pathInterpolator.ts +0 -74
  102. package/src/proxy.ts +0 -257
  103. package/src/query.ts +0 -164
  104. package/src/react/__tests__/basic.test.tsx +0 -926
  105. package/src/react/__tests__/component.test.tsx +0 -984
  106. package/src/react/__tests__/utils.tsx +0 -71
  107. package/src/typeDefs.ts +0 -351
  108. package/src/types.ts +0 -132
  109. package/src/utils.ts +0 -66
  110. package/tsconfig.cjs.json +0 -14
  111. package/tsconfig.esm.json +0 -13
  112. package/tsconfig.json +0 -20
  113. package/vitest.config.ts +0 -65
@@ -1,327 +0,0 @@
1
- /* eslint-disable @typescript-eslint/no-unused-expressions */
2
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
3
- import { SyncQueryStore, MemoryPersistentStore, valueKeyFor } from '../QueryStore.js';
4
- import { QueryClient } from '../QueryClient.js';
5
- import { query } from '../query.js';
6
- import { t, entity } from '../typeDefs.js';
7
- import { createMockFetch, testWithClient, sleep } from './utils.js';
8
- import { hashValue } from 'signalium/utils';
9
-
10
- /**
11
- * GC Time Tests
12
- *
13
- * Tests gcTime-based garbage collection with sorted queue management,
14
- * LRU interaction, and subscriber-aware eviction
15
- */
16
-
17
- describe('GC Time', () => {
18
- let client: QueryClient;
19
- let mockFetch: ReturnType<typeof createMockFetch>;
20
- let kv: any;
21
- let store: any;
22
-
23
- beforeEach(() => {
24
- kv = new MemoryPersistentStore();
25
- store = new SyncQueryStore(kv);
26
- mockFetch = createMockFetch();
27
- client = new QueryClient(store, { fetch: mockFetch as any, evictionMultiplier: 0.001 });
28
- });
29
-
30
- afterEach(() => {
31
- client?.destroy();
32
- });
33
-
34
- describe('Basic GC', () => {
35
- it('should evict queries from disk after gcTime expires', async () => {
36
- const getItem = query(t => ({
37
- path: '/item/[id]',
38
- response: { id: t.number, name: t.string },
39
- cache: { gcTime: 100, staleTime: 50 }, // 1 second
40
- }));
41
-
42
- mockFetch.get('/item/1', { id: 1, name: 'Item 1' });
43
-
44
- await testWithClient(client, async () => {
45
- const relay = getItem({ id: '1' });
46
- expect(relay.value).toEqual(undefined);
47
- await relay;
48
- expect(relay.value).toEqual({ id: 1, name: 'Item 1' });
49
- });
50
-
51
- await sleep(75);
52
-
53
- mockFetch.get('/item/1', { id: 1, name: 'Item 1 updated' }, { delay: 50 });
54
- await testWithClient(client, async () => {
55
- const relay = getItem({ id: '1' });
56
- expect(relay.value).toEqual({ id: 1, name: 'Item 1' });
57
- await relay;
58
- expect(relay.value).toEqual({ id: 1, name: 'Item 1' });
59
-
60
- await sleep(60);
61
- expect(relay.value).toEqual({ id: 1, name: 'Item 1 updated' });
62
- });
63
-
64
- await sleep(200);
65
-
66
- await testWithClient(client, async () => {
67
- const relay = getItem({ id: '1' });
68
- expect(relay.value).toEqual(undefined);
69
- await relay;
70
- expect(relay.value).toEqual({ id: 1, name: 'Item 1 updated' });
71
- });
72
- });
73
-
74
- it('should NOT evict queries with active subscribers', async () => {
75
- const getItem = query(t => ({
76
- path: '/active',
77
- response: { data: t.string },
78
- cache: { gcTime: 50 },
79
- }));
80
-
81
- mockFetch.get('/active', { data: 'test' });
82
-
83
- // Keep query active
84
- await testWithClient(client, async () => {
85
- const relay = getItem();
86
- await relay;
87
-
88
- const queryKey = hashValue(['GET:/active', undefined]);
89
-
90
- // Wait past GC time
91
- await sleep(60);
92
-
93
- // Should still be in memory because it's active (has subscriber)
94
- expect(client.queryInstances.has(queryKey)).toBe(true);
95
- });
96
- }, 3000);
97
- });
98
-
99
- describe('GC with LRU', () => {
100
- it('should work alongside LRU cache eviction', async () => {
101
- const User = entity(() => ({
102
- __typename: t.typename('User'),
103
- id: t.id,
104
- name: t.string,
105
- }));
106
-
107
- const getUser = query(t => ({
108
- path: '/users/[id]',
109
- response: { user: User },
110
- cache: {
111
- maxCount: 2, // LRU size
112
- gcTime: 5000, // 5 seconds
113
- },
114
- }));
115
-
116
- mockFetch.get('/users/1', { user: { __typename: 'User', id: 1, name: 'User 1' } });
117
- mockFetch.get('/users/2', { user: { __typename: 'User', id: 2, name: 'User 2' } });
118
- mockFetch.get('/users/3', { user: { __typename: 'User', id: 3, name: 'User 3' } });
119
-
120
- await testWithClient(client, async () => {
121
- // Fetch 3 users - third should evict first from disk via LRU
122
- const relay1 = getUser({ id: '1' });
123
- await relay1;
124
-
125
- const relay2 = getUser({ id: '2' });
126
- await relay2;
127
-
128
- const relay3 = getUser({ id: '3' });
129
- await relay3;
130
-
131
- const query1Key = hashValue(['GET:/users/[id]', { id: '1' }]);
132
- const query2Key = hashValue(['GET:/users/[id]', { id: '2' }]);
133
- const query3Key = hashValue(['GET:/users/[id]', { id: '3' }]);
134
-
135
- // All should be in memory initially
136
- expect(client.queryInstances.has(query1Key)).toBe(true);
137
- expect(client.queryInstances.has(query2Key)).toBe(true);
138
- expect(client.queryInstances.has(query3Key)).toBe(true);
139
-
140
- // First query should be evicted from DISK by LRU (but still in memory)
141
- expect(kv.getString(valueKeyFor(query1Key))).toBeUndefined();
142
- expect(kv.getString(valueKeyFor(query2Key))).toBeDefined();
143
- expect(kv.getString(valueKeyFor(query3Key))).toBeDefined();
144
- });
145
- });
146
- });
147
-
148
- describe('GC Queue Management', () => {
149
- it('should add queries to GC queue when deactivated', async () => {
150
- const getItem = query(t => ({
151
- path: '/item',
152
- response: { value: t.string },
153
- cache: { gcTime: 2000 },
154
- }));
155
-
156
- mockFetch.get('/item', { value: 'test' });
157
-
158
- const queryKey = hashValue(['GET:/item', undefined]);
159
-
160
- await testWithClient(client, async () => {
161
- const relay = getItem();
162
- await relay;
163
-
164
- expect(client.queryInstances.has(queryKey)).toBe(true);
165
- });
166
-
167
- // After context ends, query should be scheduled for GC
168
- // In a real implementation, we'd check the GC queue
169
- // For now, we verify the query is still in memory
170
- expect(client.queryInstances.has(queryKey)).toBe(true);
171
- });
172
-
173
- it('should remove queries from GC queue when reactivated', async () => {
174
- const getItem = query(t => ({
175
- path: '/reactivate',
176
- response: { n: t.number },
177
- cache: { gcTime: 1000 },
178
- }));
179
-
180
- mockFetch.get('/reactivate', { n: 1 });
181
-
182
- const queryKey = hashValue(['GET:/reactivate', undefined]);
183
-
184
- await testWithClient(client, async () => {
185
- const relay = getItem();
186
- await relay;
187
- });
188
-
189
- // Query deactivated, scheduled for GC
190
- await sleep(40);
191
-
192
- // Reactivate before GC
193
- mockFetch.get('/reactivate', { n: 2 });
194
- await testWithClient(client, async () => {
195
- const relay = getItem();
196
- relay.value; // Access it
197
- await sleep(60);
198
-
199
- // Should still be in memory
200
- expect(client.queryInstances.has(queryKey)).toBe(true);
201
- });
202
-
203
- // Even after original GC time, should not be evicted due to reactivation
204
- await sleep(40);
205
- expect(client.queryInstances.has(queryKey)).toBe(true);
206
- });
207
- });
208
-
209
- describe('GC with Entities', () => {
210
- it("should handle entity cleanup when query is GC'd", async () => {
211
- const Post = entity(() => ({
212
- __typename: t.typename('Post'),
213
- id: t.id,
214
- title: t.string,
215
- }));
216
-
217
- const User = entity(() => ({
218
- __typename: t.typename('User'),
219
- id: t.id,
220
- name: t.string,
221
- post: Post,
222
- }));
223
-
224
- const getUser = query(t => ({
225
- path: '/user',
226
- response: { user: User },
227
- cache: { gcTime: 1000 },
228
- }));
229
-
230
- mockFetch.get('/user', {
231
- user: {
232
- __typename: 'User',
233
- id: 1,
234
- name: 'Alice',
235
- post: {
236
- __typename: 'Post',
237
- id: 10,
238
- title: 'Test Post',
239
- },
240
- },
241
- });
242
-
243
- await testWithClient(client, async () => {
244
- const relay = getUser();
245
- await relay;
246
- });
247
-
248
- const userKey = hashValue('User:1');
249
- const postKey = hashValue('Post:10');
250
-
251
- // Entities should exist in store
252
- expect(kv.getString(valueKeyFor(userKey))).toBeDefined();
253
- expect(kv.getString(valueKeyFor(postKey))).toBeDefined();
254
-
255
- // Note: The actual GC of entities is handled by the LRU system
256
- // when queries are evicted. The gcTime affects when queries
257
- // are removed from memory, but disk cleanup is done by LRU.
258
- });
259
- });
260
-
261
- describe('Edge Cases', () => {
262
- it('should handle queries without gcTime', async () => {
263
- const getItem = query(t => ({
264
- path: '/no-gc',
265
- response: { data: t.string },
266
- // No gcTime configured
267
- }));
268
-
269
- mockFetch.get('/no-gc', { data: 'test' });
270
-
271
- const queryKey = hashValue(['GET:/no-gc', undefined]);
272
-
273
- await testWithClient(client, async () => {
274
- const relay = getItem();
275
- await relay;
276
- });
277
-
278
- // Should remain in memory indefinitely
279
- await sleep(100);
280
- expect(client.queryInstances.has(queryKey)).toBe(true);
281
- });
282
-
283
- it('should handle very short gcTime', async () => {
284
- const getItem = query(t => ({
285
- path: '/short-gc',
286
- response: { value: t.number },
287
- cache: { gcTime: 100 }, // Very short
288
- }));
289
-
290
- mockFetch.get('/short-gc', { value: 42 });
291
-
292
- const queryKey = hashValue(['GET:/short-gc', undefined]);
293
-
294
- await testWithClient(client, async () => {
295
- const relay = getItem();
296
- await relay;
297
- });
298
-
299
- // Should be scheduled for GC quickly
300
- // Note: Actual eviction timing depends on GC interval
301
- });
302
-
303
- it('should handle very long gcTime', async () => {
304
- const getItem = query(t => ({
305
- path: '/long-gc',
306
- response: { data: t.string },
307
- cache: { gcTime: 1000 * 60 * 60 }, // 1 hour
308
- }));
309
-
310
- mockFetch.get('/long-gc', { data: 'persisted' });
311
-
312
- const queryKey = hashValue(['GET:/long-gc', undefined]);
313
-
314
- await testWithClient(client, async () => {
315
- const relay = getItem();
316
- await relay;
317
- });
318
-
319
- // Should remain in memory for a while, then be evicted
320
- await sleep(40);
321
- expect(client.queryInstances.has(queryKey)).toBe(true);
322
-
323
- await sleep(100);
324
- expect(client.queryInstances.has(queryKey)).toBe(false);
325
- });
326
- });
327
- });
@@ -1,186 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { createMockFetch } from './utils.js';
3
-
4
- describe('createMockFetch', () => {
5
- it('should handle GET requests', async () => {
6
- const mockFetch = createMockFetch();
7
- mockFetch.get('/users/123', { id: 123, name: 'Alice' });
8
-
9
- const response = await mockFetch('/users/123', { method: 'GET' });
10
- const data = await response.json();
11
-
12
- expect(data).toEqual({ id: 123, name: 'Alice' });
13
- expect(response.status).toBe(200);
14
- expect(response.ok).toBe(true);
15
- });
16
-
17
- it('should handle POST requests with custom status', async () => {
18
- const mockFetch = createMockFetch();
19
- mockFetch.post('/users', { id: 456, name: 'Bob' }, { status: 201 });
20
-
21
- const response = await mockFetch('/users', { method: 'POST' });
22
- const data = await response.json();
23
-
24
- expect(data).toEqual({ id: 456, name: 'Bob' });
25
- expect(response.status).toBe(201);
26
- });
27
-
28
- it('should handle PUT requests', async () => {
29
- const mockFetch = createMockFetch();
30
- mockFetch.put('/users/123', { id: 123, name: 'Updated Alice' });
31
-
32
- const response = await mockFetch('/users/123', { method: 'PUT' });
33
- const data = await response.json();
34
-
35
- expect(data).toEqual({ id: 123, name: 'Updated Alice' });
36
- });
37
-
38
- it('should handle DELETE requests', async () => {
39
- const mockFetch = createMockFetch();
40
- mockFetch.delete('/users/123', { success: true });
41
-
42
- const response = await mockFetch('/users/123', { method: 'DELETE' });
43
- const data = await response.json();
44
-
45
- expect(data).toEqual({ success: true });
46
- });
47
-
48
- it('should handle PATCH requests', async () => {
49
- const mockFetch = createMockFetch();
50
- mockFetch.patch('/users/123', { id: 123, email: 'new@example.com' });
51
-
52
- const response = await mockFetch('/users/123', { method: 'PATCH' });
53
- const data = await response.json();
54
-
55
- expect(data).toEqual({ id: 123, email: 'new@example.com' });
56
- });
57
-
58
- it('should support custom headers', async () => {
59
- const mockFetch = createMockFetch();
60
- mockFetch.get(
61
- '/users/123',
62
- { id: 123 },
63
- {
64
- headers: { 'X-Custom-Header': 'test-value' },
65
- },
66
- );
67
-
68
- const response = await mockFetch('/users/123', { method: 'GET' });
69
-
70
- expect(response.headers.get('X-Custom-Header')).toBe('test-value');
71
- });
72
-
73
- it('should support delays', async () => {
74
- const mockFetch = createMockFetch();
75
- mockFetch.get('/users/123', { id: 123 }, { delay: 100 });
76
-
77
- const start = Date.now();
78
- await mockFetch('/users/123', { method: 'GET' });
79
- const duration = Date.now() - start;
80
-
81
- expect(duration).toBeGreaterThanOrEqual(90); // Allow some margin
82
- });
83
-
84
- it('should match path parameters', async () => {
85
- const mockFetch = createMockFetch();
86
- mockFetch.get('/users/[id]', { id: 123, name: 'Alice' });
87
-
88
- const response = await mockFetch('/users/123', { method: 'GET' });
89
- const data = await response.json();
90
-
91
- expect(data).toEqual({ id: 123, name: 'Alice' });
92
- });
93
-
94
- it('should match multiple path parameters', async () => {
95
- const mockFetch = createMockFetch();
96
- mockFetch.get('/users/[userId]/posts/[postId]', { userId: 5, postId: 10 });
97
-
98
- const response = await mockFetch('/users/5/posts/10', { method: 'GET' });
99
- const data = await response.json();
100
-
101
- expect(data).toEqual({ userId: 5, postId: 10 });
102
- });
103
-
104
- it('should throw error for unmocked routes', async () => {
105
- const mockFetch = createMockFetch();
106
-
107
- await expect(mockFetch('/users/123', { method: 'GET' })).rejects.toThrow(
108
- 'No mock response configured for GET /users/123',
109
- );
110
- });
111
-
112
- it('should track all calls', async () => {
113
- const mockFetch = createMockFetch();
114
- mockFetch.get('/users/123', { id: 123 });
115
- mockFetch.post('/users', { id: 456 });
116
-
117
- await mockFetch('/users/123', { method: 'GET' });
118
- await mockFetch('/users', { method: 'POST', body: '{}' });
119
-
120
- expect(mockFetch.calls).toHaveLength(2);
121
- expect(mockFetch.calls[0].url).toBe('/users/123');
122
- expect(mockFetch.calls[1].url).toBe('/users');
123
- });
124
-
125
- it('should reset routes and calls', async () => {
126
- const mockFetch = createMockFetch();
127
- mockFetch.get('/users/123', { id: 123 });
128
-
129
- await mockFetch('/users/123', { method: 'GET' });
130
- expect(mockFetch.calls).toHaveLength(1);
131
-
132
- mockFetch.reset();
133
-
134
- expect(mockFetch.calls).toHaveLength(0);
135
- await expect(mockFetch('/users/123', { method: 'GET' })).rejects.toThrow('No mock response configured');
136
- });
137
-
138
- it('should default to GET method', async () => {
139
- const mockFetch = createMockFetch();
140
- mockFetch.get('/users/123', { id: 123 });
141
-
142
- const response = await mockFetch('/users/123');
143
- const data = await response.json();
144
-
145
- expect(data).toEqual({ id: 123 });
146
- });
147
-
148
- it('should handle query parameters in URLs', async () => {
149
- const mockFetch = createMockFetch();
150
- mockFetch.get('/users', { users: [] });
151
-
152
- const response = await mockFetch('/users?page=1&limit=10', { method: 'GET' });
153
- const data = await response.json();
154
-
155
- expect(data).toEqual({ users: [] });
156
- });
157
-
158
- it('should reuse the last match when no unused mocks remain', async () => {
159
- const mockFetch = createMockFetch();
160
- mockFetch.get('/users/123', { id: 123, name: 'Alice' });
161
-
162
- // First call should succeed
163
- const response1 = await mockFetch('/users/123', { method: 'GET' });
164
- const data1 = await response1.json();
165
- expect(data1).toEqual({ id: 123, name: 'Alice' });
166
-
167
- // Second call should reuse the same mock since there are no unused ones
168
- const response2 = await mockFetch('/users/123', { method: 'GET' });
169
- const data2 = await response2.json();
170
- expect(data2).toEqual({ id: 123, name: 'Alice' });
171
- });
172
-
173
- it('should allow multiple setups for repeated calls', async () => {
174
- const mockFetch = createMockFetch();
175
- mockFetch.get('/users/123', { id: 123, name: 'Alice' });
176
- mockFetch.get('/users/123', { id: 123, name: 'Updated Alice' });
177
-
178
- const response1 = await mockFetch('/users/123', { method: 'GET' });
179
- const data1 = await response1.json();
180
- expect(data1).toEqual({ id: 123, name: 'Alice' });
181
-
182
- const response2 = await mockFetch('/users/123', { method: 'GET' });
183
- const data2 = await response2.json();
184
- expect(data2).toEqual({ id: 123, name: 'Updated Alice' });
185
- });
186
- });