@sylphx/lens-react 2.0.2 → 2.1.1
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/dist/index.d.ts +49 -42
- package/dist/index.js +29 -17
- package/package.json +2 -2
- package/src/hooks.test.tsx +425 -314
- package/src/hooks.ts +119 -88
- package/src/index.ts +4 -4
package/src/hooks.test.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for React Hooks (
|
|
2
|
+
* Tests for React Hooks (Selector-based API)
|
|
3
3
|
*
|
|
4
4
|
* NOTE: These tests require DOM environment (happy-dom).
|
|
5
5
|
* Run from packages/react directory: cd packages/react && bun test
|
|
@@ -12,8 +12,10 @@ import { test as bunTest, describe, expect } from "bun:test";
|
|
|
12
12
|
|
|
13
13
|
const test = hasDom ? bunTest : bunTest.skip;
|
|
14
14
|
|
|
15
|
-
import type { MutationResult, QueryResult } from "@sylphx/lens-client";
|
|
15
|
+
import type { LensClient, MutationResult, QueryResult } from "@sylphx/lens-client";
|
|
16
16
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
17
|
+
import type { ReactNode } from "react";
|
|
18
|
+
import { LensProvider } from "./context.js";
|
|
17
19
|
import { useLazyQuery, useMutation, useQuery } from "./hooks.js";
|
|
18
20
|
|
|
19
21
|
// =============================================================================
|
|
@@ -87,15 +89,32 @@ function createMockQueryResult<T>(initialValue: T | null = null): QueryResult<T>
|
|
|
87
89
|
};
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
// =============================================================================
|
|
93
|
+
// Test Wrapper with Mock Client
|
|
94
|
+
// =============================================================================
|
|
95
|
+
|
|
96
|
+
function createMockClient() {
|
|
97
|
+
return {} as LensClient<any, any>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createWrapper(mockClient: LensClient<any, any>) {
|
|
101
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
102
|
+
return <LensProvider client={mockClient}>{children}</LensProvider>;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
90
106
|
// =============================================================================
|
|
91
107
|
// Tests: useQuery (Accessor + Deps pattern)
|
|
92
108
|
// =============================================================================
|
|
93
109
|
|
|
94
110
|
describe("useQuery", () => {
|
|
95
111
|
test("returns loading state initially", () => {
|
|
112
|
+
const mockClient = createMockClient();
|
|
96
113
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
97
114
|
|
|
98
|
-
const { result } = renderHook(() => useQuery(() => mockQuery, [])
|
|
115
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
116
|
+
wrapper: createWrapper(mockClient),
|
|
117
|
+
});
|
|
99
118
|
|
|
100
119
|
expect(result.current.loading).toBe(true);
|
|
101
120
|
expect(result.current.data).toBe(null);
|
|
@@ -103,9 +122,12 @@ describe("useQuery", () => {
|
|
|
103
122
|
});
|
|
104
123
|
|
|
105
124
|
test("returns data when query resolves", async () => {
|
|
125
|
+
const mockClient = createMockClient();
|
|
106
126
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
107
127
|
|
|
108
|
-
const { result } = renderHook(() => useQuery(() => mockQuery, [])
|
|
128
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
129
|
+
wrapper: createWrapper(mockClient),
|
|
130
|
+
});
|
|
109
131
|
|
|
110
132
|
// Simulate data loading
|
|
111
133
|
act(() => {
|
|
@@ -121,9 +143,12 @@ describe("useQuery", () => {
|
|
|
121
143
|
});
|
|
122
144
|
|
|
123
145
|
test("returns error when query fails", async () => {
|
|
146
|
+
const mockClient = createMockClient();
|
|
124
147
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
125
148
|
|
|
126
|
-
const { result } = renderHook(() => useQuery(() => mockQuery, [])
|
|
149
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
150
|
+
wrapper: createWrapper(mockClient),
|
|
151
|
+
});
|
|
127
152
|
|
|
128
153
|
// Simulate error
|
|
129
154
|
act(() => {
|
|
@@ -139,6 +164,7 @@ describe("useQuery", () => {
|
|
|
139
164
|
});
|
|
140
165
|
|
|
141
166
|
test("handles non-Error rejection", async () => {
|
|
167
|
+
const mockClient = createMockClient();
|
|
142
168
|
const mockQuery = {
|
|
143
169
|
subscribe: () => () => {},
|
|
144
170
|
then: (onFulfilled: any, onRejected: any) => {
|
|
@@ -147,24 +173,36 @@ describe("useQuery", () => {
|
|
|
147
173
|
},
|
|
148
174
|
} as unknown as QueryResult<{ id: string }>;
|
|
149
175
|
|
|
150
|
-
const { result } = renderHook(() => useQuery(() => mockQuery, [])
|
|
176
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
177
|
+
wrapper: createWrapper(mockClient),
|
|
178
|
+
});
|
|
151
179
|
|
|
152
180
|
await waitFor(() => {
|
|
153
|
-
expect(result.current.
|
|
181
|
+
expect(result.current.loading).toBe(false);
|
|
154
182
|
});
|
|
183
|
+
|
|
184
|
+
expect(result.current.error?.message).toBe("String error");
|
|
155
185
|
});
|
|
156
186
|
|
|
157
187
|
test("skips query when skip option is true", () => {
|
|
158
|
-
const
|
|
188
|
+
const mockClient = createMockClient();
|
|
189
|
+
const mockQuery = createMockQueryResult<{ id: string }>();
|
|
159
190
|
|
|
160
|
-
const { result } = renderHook(() => useQuery(() => mockQuery, [], { skip: true })
|
|
191
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, [], { skip: true }), {
|
|
192
|
+
wrapper: createWrapper(mockClient),
|
|
193
|
+
});
|
|
161
194
|
|
|
162
195
|
expect(result.current.loading).toBe(false);
|
|
163
196
|
expect(result.current.data).toBe(null);
|
|
197
|
+
expect(result.current.error).toBe(null);
|
|
164
198
|
});
|
|
165
199
|
|
|
166
200
|
test("handles null query from accessor", () => {
|
|
167
|
-
const
|
|
201
|
+
const mockClient = createMockClient();
|
|
202
|
+
|
|
203
|
+
const { result } = renderHook(() => useQuery(() => null, []), {
|
|
204
|
+
wrapper: createWrapper(mockClient),
|
|
205
|
+
});
|
|
168
206
|
|
|
169
207
|
expect(result.current.loading).toBe(false);
|
|
170
208
|
expect(result.current.data).toBe(null);
|
|
@@ -172,7 +210,11 @@ describe("useQuery", () => {
|
|
|
172
210
|
});
|
|
173
211
|
|
|
174
212
|
test("handles undefined query from accessor", () => {
|
|
175
|
-
const
|
|
213
|
+
const mockClient = createMockClient();
|
|
214
|
+
|
|
215
|
+
const { result } = renderHook(() => useQuery(() => undefined, []), {
|
|
216
|
+
wrapper: createWrapper(mockClient),
|
|
217
|
+
});
|
|
176
218
|
|
|
177
219
|
expect(result.current.loading).toBe(false);
|
|
178
220
|
expect(result.current.data).toBe(null);
|
|
@@ -180,156 +222,171 @@ describe("useQuery", () => {
|
|
|
180
222
|
});
|
|
181
223
|
|
|
182
224
|
test("updates when query subscription emits", async () => {
|
|
183
|
-
const
|
|
225
|
+
const mockClient = createMockClient();
|
|
226
|
+
const mockQuery = createMockQueryResult<{ count: number }>();
|
|
184
227
|
|
|
185
|
-
const { result } = renderHook(() => useQuery(() => mockQuery, [])
|
|
228
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
229
|
+
wrapper: createWrapper(mockClient),
|
|
230
|
+
});
|
|
186
231
|
|
|
187
|
-
//
|
|
232
|
+
// Initial value
|
|
188
233
|
act(() => {
|
|
189
|
-
mockQuery._setValue({
|
|
234
|
+
mockQuery._setValue({ count: 1 });
|
|
190
235
|
});
|
|
191
236
|
|
|
192
237
|
await waitFor(() => {
|
|
193
|
-
expect(result.current.data?.
|
|
238
|
+
expect(result.current.data?.count).toBe(1);
|
|
194
239
|
});
|
|
195
240
|
|
|
196
241
|
// Update value via subscription
|
|
197
242
|
act(() => {
|
|
198
|
-
mockQuery._setValue({
|
|
243
|
+
mockQuery._setValue({ count: 2 });
|
|
199
244
|
});
|
|
200
245
|
|
|
201
246
|
await waitFor(() => {
|
|
202
|
-
expect(result.current.data?.
|
|
247
|
+
expect(result.current.data?.count).toBe(2);
|
|
203
248
|
});
|
|
204
249
|
});
|
|
205
250
|
|
|
206
251
|
test("refetch reloads the query", async () => {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
const { result } = renderHook(() => useQuery(() => mockQuery, []));
|
|
252
|
+
const mockClient = createMockClient();
|
|
253
|
+
const mockQuery = createMockQueryResult<{ id: string }>({ id: "initial" });
|
|
210
254
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
mockQuery._setValue({ id: "123", name: "John" });
|
|
255
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
256
|
+
wrapper: createWrapper(mockClient),
|
|
214
257
|
});
|
|
215
258
|
|
|
216
259
|
await waitFor(() => {
|
|
217
|
-
expect(result.current.
|
|
260
|
+
expect(result.current.loading).toBe(false);
|
|
218
261
|
});
|
|
219
262
|
|
|
220
|
-
|
|
263
|
+
expect(result.current.data?.id).toBe("initial");
|
|
264
|
+
|
|
265
|
+
// Refetch
|
|
221
266
|
act(() => {
|
|
222
267
|
result.current.refetch();
|
|
223
268
|
});
|
|
224
269
|
|
|
225
|
-
|
|
226
|
-
|
|
270
|
+
expect(result.current.loading).toBe(true);
|
|
271
|
+
|
|
227
272
|
await waitFor(() => {
|
|
228
273
|
expect(result.current.loading).toBe(false);
|
|
229
274
|
});
|
|
230
|
-
|
|
231
|
-
expect(result.current.data).toEqual({ id: "123", name: "John" });
|
|
232
275
|
});
|
|
233
276
|
|
|
234
277
|
test("refetch handles errors", async () => {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
const
|
|
278
|
+
const mockClient = createMockClient();
|
|
279
|
+
let shouldFail = false;
|
|
280
|
+
const mockQuery = {
|
|
281
|
+
subscribe: () => () => {},
|
|
282
|
+
then: (onFulfilled: any, onRejected: any) => {
|
|
283
|
+
if (shouldFail) {
|
|
284
|
+
return Promise.reject(new Error("Refetch failed")).then(onFulfilled, onRejected);
|
|
285
|
+
}
|
|
286
|
+
return Promise.resolve({ id: "test" }).then(onFulfilled, onRejected);
|
|
287
|
+
},
|
|
288
|
+
} as unknown as QueryResult<{ id: string }>;
|
|
238
289
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
mockQuery._setValue({ id: "123", name: "John" });
|
|
290
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
291
|
+
wrapper: createWrapper(mockClient),
|
|
242
292
|
});
|
|
243
293
|
|
|
244
294
|
await waitFor(() => {
|
|
245
|
-
expect(result.current.
|
|
295
|
+
expect(result.current.loading).toBe(false);
|
|
246
296
|
});
|
|
247
297
|
|
|
248
|
-
|
|
249
|
-
const failingQuery = {
|
|
250
|
-
subscribe: () => () => {},
|
|
251
|
-
then: (onFulfilled: any, onRejected: any) => {
|
|
252
|
-
return Promise.reject(new Error("Refetch failed")).then(onFulfilled, onRejected);
|
|
253
|
-
},
|
|
254
|
-
} as unknown as QueryResult<{ id: string; name: string }>;
|
|
298
|
+
shouldFail = true;
|
|
255
299
|
|
|
256
|
-
|
|
257
|
-
|
|
300
|
+
act(() => {
|
|
301
|
+
result.current.refetch();
|
|
302
|
+
});
|
|
258
303
|
|
|
259
304
|
await waitFor(() => {
|
|
260
|
-
expect(
|
|
305
|
+
expect(result.current.loading).toBe(false);
|
|
261
306
|
});
|
|
307
|
+
|
|
308
|
+
expect(result.current.error?.message).toBe("Refetch failed");
|
|
262
309
|
});
|
|
263
310
|
|
|
264
|
-
test("refetch does nothing when query is null", () => {
|
|
265
|
-
const
|
|
311
|
+
test("refetch does nothing when query is null", async () => {
|
|
312
|
+
const mockClient = createMockClient();
|
|
266
313
|
|
|
314
|
+
const { result } = renderHook(() => useQuery(() => null, []), {
|
|
315
|
+
wrapper: createWrapper(mockClient),
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Should not throw
|
|
267
319
|
act(() => {
|
|
268
320
|
result.current.refetch();
|
|
269
321
|
});
|
|
270
322
|
|
|
271
323
|
expect(result.current.loading).toBe(false);
|
|
272
|
-
expect(result.current.data).toBe(null);
|
|
273
324
|
});
|
|
274
325
|
|
|
275
|
-
test("refetch does nothing when skip is true", () => {
|
|
276
|
-
const
|
|
326
|
+
test("refetch does nothing when skip is true", async () => {
|
|
327
|
+
const mockClient = createMockClient();
|
|
328
|
+
const mockQuery = createMockQueryResult<{ id: string }>();
|
|
277
329
|
|
|
278
|
-
const { result } = renderHook(() => useQuery(() => mockQuery, [], { skip: true })
|
|
330
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, [], { skip: true }), {
|
|
331
|
+
wrapper: createWrapper(mockClient),
|
|
332
|
+
});
|
|
279
333
|
|
|
334
|
+
// Should not throw
|
|
280
335
|
act(() => {
|
|
281
336
|
result.current.refetch();
|
|
282
337
|
});
|
|
283
338
|
|
|
284
339
|
expect(result.current.loading).toBe(false);
|
|
285
|
-
expect(result.current.data).toBe(null);
|
|
286
340
|
});
|
|
287
341
|
|
|
288
342
|
test("refetch with non-Error rejection", async () => {
|
|
289
|
-
|
|
343
|
+
const mockClient = createMockClient();
|
|
344
|
+
let callCount = 0;
|
|
290
345
|
const mockQuery = {
|
|
291
346
|
subscribe: () => () => {},
|
|
292
347
|
then: (onFulfilled: any, onRejected: any) => {
|
|
293
|
-
|
|
294
|
-
|
|
348
|
+
callCount++;
|
|
349
|
+
if (callCount > 1) {
|
|
350
|
+
return Promise.reject("String error on refetch").then(onFulfilled, onRejected);
|
|
295
351
|
}
|
|
296
|
-
return Promise.resolve({ id: "
|
|
352
|
+
return Promise.resolve({ id: "test" }).then(onFulfilled, onRejected);
|
|
297
353
|
},
|
|
298
|
-
} as unknown as QueryResult<{ id: string
|
|
354
|
+
} as unknown as QueryResult<{ id: string }>;
|
|
299
355
|
|
|
300
|
-
const { result } = renderHook(() => useQuery(() => mockQuery, [])
|
|
356
|
+
const { result } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
357
|
+
wrapper: createWrapper(mockClient),
|
|
358
|
+
});
|
|
301
359
|
|
|
302
360
|
await waitFor(() => {
|
|
303
|
-
expect(result.current.
|
|
361
|
+
expect(result.current.loading).toBe(false);
|
|
304
362
|
});
|
|
305
363
|
|
|
306
|
-
// Make it fail on refetch
|
|
307
|
-
shouldFail = true;
|
|
308
|
-
|
|
309
364
|
act(() => {
|
|
310
365
|
result.current.refetch();
|
|
311
366
|
});
|
|
312
367
|
|
|
313
368
|
await waitFor(() => {
|
|
314
|
-
expect(result.current.
|
|
369
|
+
expect(result.current.loading).toBe(false);
|
|
315
370
|
});
|
|
371
|
+
|
|
372
|
+
expect(result.current.error?.message).toBe("String error on refetch");
|
|
316
373
|
});
|
|
317
374
|
|
|
318
375
|
test("cleans up subscription on unmount", async () => {
|
|
319
|
-
const
|
|
376
|
+
const mockClient = createMockClient();
|
|
320
377
|
let unsubscribeCalled = false;
|
|
378
|
+
const mockQuery = {
|
|
379
|
+
subscribe: () => {
|
|
380
|
+
return () => {
|
|
381
|
+
unsubscribeCalled = true;
|
|
382
|
+
};
|
|
383
|
+
},
|
|
384
|
+
then: (onFulfilled: any) => Promise.resolve({ id: "test" }).then(onFulfilled),
|
|
385
|
+
} as unknown as QueryResult<{ id: string }>;
|
|
321
386
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const originalUnsubscribe = originalSubscribe.call(mockQuery, callback);
|
|
326
|
-
return () => {
|
|
327
|
-
unsubscribeCalled = true;
|
|
328
|
-
originalUnsubscribe();
|
|
329
|
-
};
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
const { unmount } = renderHook(() => useQuery(() => mockQuery, []));
|
|
387
|
+
const { unmount } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
388
|
+
wrapper: createWrapper(mockClient),
|
|
389
|
+
});
|
|
333
390
|
|
|
334
391
|
unmount();
|
|
335
392
|
|
|
@@ -337,136 +394,136 @@ describe("useQuery", () => {
|
|
|
337
394
|
});
|
|
338
395
|
|
|
339
396
|
test("does not update state after unmount", async () => {
|
|
340
|
-
const
|
|
397
|
+
const mockClient = createMockClient();
|
|
398
|
+
const mockQuery = createMockQueryResult<{ id: string }>();
|
|
341
399
|
|
|
342
|
-
const { unmount } = renderHook(() => useQuery(() => mockQuery, [])
|
|
400
|
+
const { result, unmount } = renderHook(() => useQuery(() => mockQuery, []), {
|
|
401
|
+
wrapper: createWrapper(mockClient),
|
|
402
|
+
});
|
|
343
403
|
|
|
344
|
-
// Unmount before query resolves
|
|
345
404
|
unmount();
|
|
346
405
|
|
|
347
|
-
//
|
|
406
|
+
// This should not cause errors or state updates
|
|
348
407
|
act(() => {
|
|
349
|
-
mockQuery._setValue({ id: "
|
|
408
|
+
mockQuery._setValue({ id: "after-unmount" });
|
|
350
409
|
});
|
|
351
410
|
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
expect(true).toBe(true);
|
|
411
|
+
// Result should still be from before unmount
|
|
412
|
+
expect(result.current.data).toBe(null);
|
|
355
413
|
});
|
|
356
414
|
|
|
357
415
|
test("handles query change via deps", async () => {
|
|
358
|
-
const
|
|
359
|
-
const
|
|
416
|
+
const mockClient = createMockClient();
|
|
417
|
+
const mockQuery1 = createMockQueryResult<{ id: string }>({ id: "query1" });
|
|
418
|
+
const mockQuery2 = createMockQueryResult<{ id: string }>({ id: "query2" });
|
|
360
419
|
|
|
361
|
-
let
|
|
362
|
-
const { result, rerender } = renderHook(() => useQuery(() => (
|
|
363
|
-
|
|
364
|
-
// Load first query
|
|
365
|
-
act(() => {
|
|
366
|
-
mockQuery1._setValue({ id: "1", name: "First" });
|
|
420
|
+
let useQuery1 = true;
|
|
421
|
+
const { result, rerender } = renderHook(() => useQuery(() => (useQuery1 ? mockQuery1 : mockQuery2), [useQuery1]), {
|
|
422
|
+
wrapper: createWrapper(mockClient),
|
|
367
423
|
});
|
|
368
424
|
|
|
369
425
|
await waitFor(() => {
|
|
370
|
-
expect(result.current.data?.
|
|
426
|
+
expect(result.current.data?.id).toBe("query1");
|
|
371
427
|
});
|
|
372
428
|
|
|
373
|
-
// Change to
|
|
374
|
-
|
|
429
|
+
// Change to query2
|
|
430
|
+
useQuery1 = false;
|
|
375
431
|
rerender();
|
|
376
432
|
|
|
377
|
-
expect(result.current.loading).toBe(true);
|
|
378
|
-
|
|
379
|
-
// Load second query
|
|
380
|
-
act(() => {
|
|
381
|
-
mockQuery2._setValue({ id: "2", name: "Second" });
|
|
382
|
-
});
|
|
383
|
-
|
|
384
433
|
await waitFor(() => {
|
|
385
|
-
expect(result.current.data?.
|
|
434
|
+
expect(result.current.data?.id).toBe("query2");
|
|
386
435
|
});
|
|
387
436
|
});
|
|
388
437
|
|
|
389
438
|
test("handles skip option change from true to false", async () => {
|
|
390
|
-
const
|
|
439
|
+
const mockClient = createMockClient();
|
|
440
|
+
const mockQuery = createMockQueryResult<{ id: string }>({ id: "test" });
|
|
391
441
|
|
|
392
442
|
let skip = true;
|
|
393
|
-
const { result, rerender } = renderHook(() => useQuery(() => mockQuery, [], { skip })
|
|
443
|
+
const { result, rerender } = renderHook(() => useQuery(() => mockQuery, [], { skip }), {
|
|
444
|
+
wrapper: createWrapper(mockClient),
|
|
445
|
+
});
|
|
394
446
|
|
|
395
447
|
expect(result.current.loading).toBe(false);
|
|
448
|
+
expect(result.current.data).toBe(null);
|
|
396
449
|
|
|
397
|
-
//
|
|
450
|
+
// Enable query
|
|
398
451
|
skip = false;
|
|
399
452
|
rerender();
|
|
400
453
|
|
|
401
|
-
expect(result.current.loading).toBe(true);
|
|
402
|
-
|
|
403
|
-
act(() => {
|
|
404
|
-
mockQuery._setValue({ id: "123", name: "John" });
|
|
405
|
-
});
|
|
406
|
-
|
|
407
454
|
await waitFor(() => {
|
|
408
|
-
expect(result.current.data).
|
|
455
|
+
expect(result.current.data?.id).toBe("test");
|
|
409
456
|
});
|
|
410
457
|
});
|
|
411
458
|
|
|
412
459
|
test("handles skip option change from false to true", async () => {
|
|
413
|
-
const
|
|
460
|
+
const mockClient = createMockClient();
|
|
461
|
+
const mockQuery = createMockQueryResult<{ id: string }>({ id: "test" });
|
|
414
462
|
|
|
415
463
|
let skip = false;
|
|
416
|
-
const { result, rerender } = renderHook(() => useQuery(() => mockQuery, [], { skip })
|
|
417
|
-
|
|
418
|
-
act(() => {
|
|
419
|
-
mockQuery._setValue({ id: "123", name: "John" });
|
|
464
|
+
const { result, rerender } = renderHook(() => useQuery(() => mockQuery, [], { skip }), {
|
|
465
|
+
wrapper: createWrapper(mockClient),
|
|
420
466
|
});
|
|
421
467
|
|
|
422
468
|
await waitFor(() => {
|
|
423
|
-
expect(result.current.data).
|
|
469
|
+
expect(result.current.data?.id).toBe("test");
|
|
424
470
|
});
|
|
425
471
|
|
|
426
|
-
//
|
|
472
|
+
// Disable query
|
|
427
473
|
skip = true;
|
|
428
474
|
rerender();
|
|
429
475
|
|
|
476
|
+
await waitFor(() => {
|
|
477
|
+
expect(result.current.data).toBe(null);
|
|
478
|
+
});
|
|
479
|
+
|
|
430
480
|
expect(result.current.loading).toBe(false);
|
|
431
|
-
expect(result.current.data).toBe(null);
|
|
432
|
-
expect(result.current.error).toBe(null);
|
|
433
481
|
});
|
|
434
482
|
|
|
435
483
|
test("select transforms the data", async () => {
|
|
436
|
-
const
|
|
484
|
+
const mockClient = createMockClient();
|
|
485
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>({
|
|
486
|
+
id: "123",
|
|
487
|
+
name: "John",
|
|
488
|
+
});
|
|
437
489
|
|
|
438
|
-
const { result } = renderHook(
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
490
|
+
const { result } = renderHook(
|
|
491
|
+
() =>
|
|
492
|
+
useQuery(() => mockQuery, [], {
|
|
493
|
+
select: (data) => data.name.toUpperCase(),
|
|
494
|
+
}),
|
|
495
|
+
{ wrapper: createWrapper(mockClient) },
|
|
442
496
|
);
|
|
443
497
|
|
|
444
|
-
act(() => {
|
|
445
|
-
mockQuery._setValue({ id: "123", name: "John" });
|
|
446
|
-
});
|
|
447
|
-
|
|
448
498
|
await waitFor(() => {
|
|
449
|
-
expect(result.current.
|
|
499
|
+
expect(result.current.loading).toBe(false);
|
|
450
500
|
});
|
|
501
|
+
|
|
502
|
+
expect(result.current.data).toBe("JOHN");
|
|
451
503
|
});
|
|
452
504
|
|
|
453
505
|
test("Route + Params pattern works", async () => {
|
|
454
|
-
const
|
|
506
|
+
const mockClient = createMockClient();
|
|
507
|
+
const mockQuery = createMockQueryResult<{ id: string }>({ id: "user-123" });
|
|
455
508
|
const route = (_params: { id: string }) => mockQuery;
|
|
456
509
|
|
|
457
|
-
const { result } = renderHook(() => useQuery(route, { id: "123" })
|
|
458
|
-
|
|
459
|
-
act(() => {
|
|
460
|
-
mockQuery._setValue({ id: "123", name: "John" });
|
|
510
|
+
const { result } = renderHook(() => useQuery(() => route, { id: "123" }), {
|
|
511
|
+
wrapper: createWrapper(mockClient),
|
|
461
512
|
});
|
|
462
513
|
|
|
463
514
|
await waitFor(() => {
|
|
464
|
-
expect(result.current.
|
|
515
|
+
expect(result.current.loading).toBe(false);
|
|
465
516
|
});
|
|
517
|
+
|
|
518
|
+
expect(result.current.data?.id).toBe("user-123");
|
|
466
519
|
});
|
|
467
520
|
|
|
468
|
-
test("Route + Params with null route", () => {
|
|
469
|
-
const
|
|
521
|
+
test("Route + Params with null route", async () => {
|
|
522
|
+
const mockClient = createMockClient();
|
|
523
|
+
|
|
524
|
+
const { result } = renderHook(() => useQuery(() => null, { id: "123" }), {
|
|
525
|
+
wrapper: createWrapper(mockClient),
|
|
526
|
+
});
|
|
470
527
|
|
|
471
528
|
expect(result.current.loading).toBe(false);
|
|
472
529
|
expect(result.current.data).toBe(null);
|
|
@@ -479,39 +536,40 @@ describe("useQuery", () => {
|
|
|
479
536
|
|
|
480
537
|
describe("useMutation", () => {
|
|
481
538
|
test("executes mutation and returns result", async () => {
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
};
|
|
539
|
+
const mockClient = createMockClient();
|
|
540
|
+
const mockMutation = async (_input: { title: string }): Promise<MutationResult<{ id: string }>> => {
|
|
541
|
+
return { data: { id: "new-123" } };
|
|
486
542
|
};
|
|
487
543
|
|
|
488
|
-
const { result } = renderHook(() => useMutation(
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
expect(result.current.data).toBe(null);
|
|
544
|
+
const { result } = renderHook(() => useMutation(() => mockMutation), {
|
|
545
|
+
wrapper: createWrapper(mockClient),
|
|
546
|
+
});
|
|
492
547
|
|
|
493
|
-
let mutationResult: MutationResult<{ id: string
|
|
548
|
+
let mutationResult: MutationResult<{ id: string }> | undefined;
|
|
494
549
|
await act(async () => {
|
|
495
|
-
mutationResult = await result.current.mutate({
|
|
550
|
+
mutationResult = await result.current.mutate({ title: "Test" });
|
|
496
551
|
});
|
|
497
552
|
|
|
498
|
-
expect(mutationResult?.data).
|
|
499
|
-
expect(result.current.data).
|
|
553
|
+
expect(mutationResult?.data?.id).toBe("new-123");
|
|
554
|
+
expect(result.current.data?.id).toBe("new-123");
|
|
500
555
|
expect(result.current.loading).toBe(false);
|
|
501
556
|
});
|
|
502
557
|
|
|
503
558
|
test("handles mutation error", async () => {
|
|
504
|
-
const
|
|
559
|
+
const mockClient = createMockClient();
|
|
560
|
+
const mockMutation = async (): Promise<MutationResult<{ id: string }>> => {
|
|
505
561
|
throw new Error("Mutation failed");
|
|
506
562
|
};
|
|
507
563
|
|
|
508
|
-
const { result } = renderHook(() => useMutation(
|
|
564
|
+
const { result } = renderHook(() => useMutation(() => mockMutation), {
|
|
565
|
+
wrapper: createWrapper(mockClient),
|
|
566
|
+
});
|
|
509
567
|
|
|
510
568
|
await act(async () => {
|
|
511
569
|
try {
|
|
512
|
-
await result.current.mutate({
|
|
570
|
+
await result.current.mutate({ title: "Test" });
|
|
513
571
|
} catch {
|
|
514
|
-
// Expected
|
|
572
|
+
// Expected
|
|
515
573
|
}
|
|
516
574
|
});
|
|
517
575
|
|
|
@@ -520,46 +578,49 @@ describe("useMutation", () => {
|
|
|
520
578
|
});
|
|
521
579
|
|
|
522
580
|
test("handles non-Error exception in mutation", async () => {
|
|
523
|
-
const
|
|
581
|
+
const mockClient = createMockClient();
|
|
582
|
+
const mockMutation = async (): Promise<MutationResult<{ id: string }>> => {
|
|
524
583
|
throw "String error";
|
|
525
584
|
};
|
|
526
585
|
|
|
527
|
-
const { result } = renderHook(() => useMutation(
|
|
586
|
+
const { result } = renderHook(() => useMutation(() => mockMutation), {
|
|
587
|
+
wrapper: createWrapper(mockClient),
|
|
588
|
+
});
|
|
528
589
|
|
|
529
590
|
await act(async () => {
|
|
530
591
|
try {
|
|
531
|
-
await result.current.mutate({
|
|
592
|
+
await result.current.mutate({ title: "Test" });
|
|
532
593
|
} catch {
|
|
533
|
-
// Expected
|
|
594
|
+
// Expected
|
|
534
595
|
}
|
|
535
596
|
});
|
|
536
597
|
|
|
537
598
|
expect(result.current.error?.message).toBe("String error");
|
|
538
|
-
expect(result.current.loading).toBe(false);
|
|
539
599
|
});
|
|
540
600
|
|
|
541
601
|
test("shows loading state during mutation", async () => {
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
602
|
+
const mockClient = createMockClient();
|
|
603
|
+
let resolvePromise: () => void;
|
|
604
|
+
const mockMutation = async (): Promise<MutationResult<{ id: string }>> => {
|
|
605
|
+
await new Promise<void>((resolve) => {
|
|
606
|
+
resolvePromise = resolve;
|
|
546
607
|
});
|
|
608
|
+
return { data: { id: "test" } };
|
|
547
609
|
};
|
|
548
610
|
|
|
549
|
-
const { result } = renderHook(() => useMutation(
|
|
611
|
+
const { result } = renderHook(() => useMutation(() => mockMutation), {
|
|
612
|
+
wrapper: createWrapper(mockClient),
|
|
613
|
+
});
|
|
550
614
|
|
|
551
|
-
|
|
552
|
-
let mutationPromise: Promise<MutationResult<{ id: string }>> | undefined;
|
|
615
|
+
let mutationPromise: Promise<any>;
|
|
553
616
|
act(() => {
|
|
554
|
-
mutationPromise = result.current.mutate({
|
|
617
|
+
mutationPromise = result.current.mutate({ title: "Test" });
|
|
555
618
|
});
|
|
556
619
|
|
|
557
|
-
// Should be loading
|
|
558
620
|
expect(result.current.loading).toBe(true);
|
|
559
621
|
|
|
560
|
-
// Resolve mutation
|
|
561
622
|
await act(async () => {
|
|
562
|
-
|
|
623
|
+
resolvePromise!();
|
|
563
624
|
await mutationPromise;
|
|
564
625
|
});
|
|
565
626
|
|
|
@@ -567,14 +628,17 @@ describe("useMutation", () => {
|
|
|
567
628
|
});
|
|
568
629
|
|
|
569
630
|
test("reset clears mutation state", async () => {
|
|
570
|
-
const
|
|
571
|
-
|
|
631
|
+
const mockClient = createMockClient();
|
|
632
|
+
const mockMutation = async (): Promise<MutationResult<{ id: string }>> => {
|
|
633
|
+
return { data: { id: "test" } };
|
|
572
634
|
};
|
|
573
635
|
|
|
574
|
-
const { result } = renderHook(() => useMutation(
|
|
636
|
+
const { result } = renderHook(() => useMutation(() => mockMutation), {
|
|
637
|
+
wrapper: createWrapper(mockClient),
|
|
638
|
+
});
|
|
575
639
|
|
|
576
640
|
await act(async () => {
|
|
577
|
-
await result.current.mutate({
|
|
641
|
+
await result.current.mutate({ title: "Test" });
|
|
578
642
|
});
|
|
579
643
|
|
|
580
644
|
expect(result.current.data).not.toBe(null);
|
|
@@ -589,128 +653,142 @@ describe("useMutation", () => {
|
|
|
589
653
|
});
|
|
590
654
|
|
|
591
655
|
test("handles multiple mutations in sequence", async () => {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
656
|
+
const mockClient = createMockClient();
|
|
657
|
+
let callCount = 0;
|
|
658
|
+
const mockMutation = async (_input: { title: string }): Promise<MutationResult<{ count: number }>> => {
|
|
659
|
+
callCount++;
|
|
660
|
+
return { data: { count: callCount } };
|
|
596
661
|
};
|
|
597
662
|
|
|
598
|
-
const { result } = renderHook(() => useMutation(
|
|
663
|
+
const { result } = renderHook(() => useMutation(() => mockMutation), {
|
|
664
|
+
wrapper: createWrapper(mockClient),
|
|
665
|
+
});
|
|
599
666
|
|
|
600
|
-
// First mutation
|
|
601
667
|
await act(async () => {
|
|
602
|
-
await result.current.mutate({
|
|
668
|
+
await result.current.mutate({ title: "First" });
|
|
603
669
|
});
|
|
670
|
+
expect(result.current.data?.count).toBe(1);
|
|
604
671
|
|
|
605
|
-
expect(result.current.data).toEqual({ id: "id-1", name: "First" });
|
|
606
|
-
|
|
607
|
-
// Second mutation
|
|
608
672
|
await act(async () => {
|
|
609
|
-
await result.current.mutate({
|
|
673
|
+
await result.current.mutate({ title: "Second" });
|
|
610
674
|
});
|
|
611
|
-
|
|
612
|
-
expect(result.current.data).toEqual({ id: "id-2", name: "Second" });
|
|
675
|
+
expect(result.current.data?.count).toBe(2);
|
|
613
676
|
});
|
|
614
677
|
|
|
615
678
|
test("clears error on successful mutation after previous error", async () => {
|
|
679
|
+
const mockClient = createMockClient();
|
|
616
680
|
let shouldFail = true;
|
|
617
|
-
const
|
|
681
|
+
const mockMutation = async (): Promise<MutationResult<{ id: string }>> => {
|
|
618
682
|
if (shouldFail) {
|
|
619
|
-
throw new Error("
|
|
683
|
+
throw new Error("Failed");
|
|
620
684
|
}
|
|
621
|
-
return { data: { id: "
|
|
685
|
+
return { data: { id: "success" } };
|
|
622
686
|
};
|
|
623
687
|
|
|
624
|
-
const { result } = renderHook(() => useMutation(
|
|
688
|
+
const { result } = renderHook(() => useMutation(() => mockMutation), {
|
|
689
|
+
wrapper: createWrapper(mockClient),
|
|
690
|
+
});
|
|
625
691
|
|
|
626
692
|
// First mutation fails
|
|
627
693
|
await act(async () => {
|
|
628
694
|
try {
|
|
629
|
-
await result.current.mutate({
|
|
695
|
+
await result.current.mutate({ title: "Test" });
|
|
630
696
|
} catch {
|
|
631
|
-
// Expected
|
|
697
|
+
// Expected
|
|
632
698
|
}
|
|
633
699
|
});
|
|
634
700
|
|
|
635
|
-
expect(result.current.error
|
|
701
|
+
expect(result.current.error).not.toBe(null);
|
|
636
702
|
|
|
637
703
|
// Second mutation succeeds
|
|
638
704
|
shouldFail = false;
|
|
639
705
|
await act(async () => {
|
|
640
|
-
await result.current.mutate({
|
|
706
|
+
await result.current.mutate({ title: "Test" });
|
|
641
707
|
});
|
|
642
708
|
|
|
643
709
|
expect(result.current.error).toBe(null);
|
|
644
|
-
expect(result.current.data).
|
|
710
|
+
expect(result.current.data?.id).toBe("success");
|
|
645
711
|
});
|
|
646
712
|
|
|
647
713
|
test("does not update state after unmount", async () => {
|
|
648
|
-
const
|
|
649
|
-
|
|
714
|
+
const mockClient = createMockClient();
|
|
715
|
+
let resolvePromise: () => void;
|
|
716
|
+
const mockMutation = async (): Promise<MutationResult<{ id: string }>> => {
|
|
717
|
+
await new Promise<void>((resolve) => {
|
|
718
|
+
resolvePromise = resolve;
|
|
719
|
+
});
|
|
720
|
+
return { data: { id: "test" } };
|
|
650
721
|
};
|
|
651
722
|
|
|
652
|
-
const { result, unmount } = renderHook(() => useMutation(
|
|
723
|
+
const { result, unmount } = renderHook(() => useMutation(() => mockMutation), {
|
|
724
|
+
wrapper: createWrapper(mockClient),
|
|
725
|
+
});
|
|
653
726
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
727
|
+
let mutationPromise: Promise<any>;
|
|
728
|
+
act(() => {
|
|
729
|
+
mutationPromise = result.current.mutate({ title: "Test" });
|
|
730
|
+
});
|
|
657
731
|
|
|
658
|
-
|
|
659
|
-
await mutationPromise;
|
|
732
|
+
unmount();
|
|
660
733
|
|
|
661
|
-
//
|
|
662
|
-
|
|
734
|
+
// Resolve after unmount - should not cause errors
|
|
735
|
+
await act(async () => {
|
|
736
|
+
resolvePromise!();
|
|
737
|
+
await mutationPromise;
|
|
738
|
+
});
|
|
663
739
|
});
|
|
664
740
|
});
|
|
665
741
|
|
|
666
742
|
// =============================================================================
|
|
667
|
-
// Tests: useLazyQuery
|
|
743
|
+
// Tests: useLazyQuery
|
|
668
744
|
// =============================================================================
|
|
669
745
|
|
|
670
746
|
describe("useLazyQuery", () => {
|
|
671
747
|
test("does not execute query on mount", () => {
|
|
672
|
-
const
|
|
748
|
+
const mockClient = createMockClient();
|
|
749
|
+
const mockQuery = createMockQueryResult<{ id: string }>();
|
|
673
750
|
|
|
674
|
-
const { result } = renderHook(() => useLazyQuery(() => mockQuery, [])
|
|
751
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []), {
|
|
752
|
+
wrapper: createWrapper(mockClient),
|
|
753
|
+
});
|
|
675
754
|
|
|
676
755
|
expect(result.current.loading).toBe(false);
|
|
677
756
|
expect(result.current.data).toBe(null);
|
|
678
757
|
});
|
|
679
758
|
|
|
680
759
|
test("executes query when execute is called", async () => {
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
name: "John",
|
|
684
|
-
});
|
|
760
|
+
const mockClient = createMockClient();
|
|
761
|
+
const mockQuery = createMockQueryResult<{ id: string }>({ id: "lazy-123" });
|
|
685
762
|
|
|
686
|
-
const { result } = renderHook(() => useLazyQuery(() => mockQuery, [])
|
|
763
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []), {
|
|
764
|
+
wrapper: createWrapper(mockClient),
|
|
765
|
+
});
|
|
687
766
|
|
|
688
|
-
let queryResult: { id: string; name: string } | undefined;
|
|
689
767
|
await act(async () => {
|
|
690
|
-
|
|
768
|
+
await result.current.execute();
|
|
691
769
|
});
|
|
692
770
|
|
|
693
|
-
expect(
|
|
694
|
-
expect(result.current.
|
|
771
|
+
expect(result.current.data?.id).toBe("lazy-123");
|
|
772
|
+
expect(result.current.loading).toBe(false);
|
|
695
773
|
});
|
|
696
774
|
|
|
697
775
|
test("handles query error", async () => {
|
|
698
|
-
|
|
699
|
-
const mockQuery =
|
|
700
|
-
|
|
701
|
-
|
|
776
|
+
const mockClient = createMockClient();
|
|
777
|
+
const mockQuery = {
|
|
778
|
+
then: (_: any, onRejected: any) => {
|
|
779
|
+
return Promise.reject(new Error("Query failed")).then(null, onRejected);
|
|
780
|
+
},
|
|
781
|
+
} as unknown as QueryResult<{ id: string }>;
|
|
702
782
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
mockQuery._setError(new Error("Query failed"));
|
|
783
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []), {
|
|
784
|
+
wrapper: createWrapper(mockClient),
|
|
706
785
|
});
|
|
707
786
|
|
|
708
|
-
// Execute should throw
|
|
709
787
|
await act(async () => {
|
|
710
788
|
try {
|
|
711
789
|
await result.current.execute();
|
|
712
790
|
} catch {
|
|
713
|
-
// Expected
|
|
791
|
+
// Expected
|
|
714
792
|
}
|
|
715
793
|
});
|
|
716
794
|
|
|
@@ -718,19 +796,22 @@ describe("useLazyQuery", () => {
|
|
|
718
796
|
});
|
|
719
797
|
|
|
720
798
|
test("handles non-Error rejection", async () => {
|
|
799
|
+
const mockClient = createMockClient();
|
|
721
800
|
const mockQuery = {
|
|
722
|
-
then: (
|
|
723
|
-
return Promise.reject("String error").then(
|
|
801
|
+
then: (_: any, onRejected: any) => {
|
|
802
|
+
return Promise.reject("String error").then(null, onRejected);
|
|
724
803
|
},
|
|
725
804
|
} as unknown as QueryResult<{ id: string }>;
|
|
726
805
|
|
|
727
|
-
const { result } = renderHook(() => useLazyQuery(() => mockQuery, [])
|
|
806
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []), {
|
|
807
|
+
wrapper: createWrapper(mockClient),
|
|
808
|
+
});
|
|
728
809
|
|
|
729
810
|
await act(async () => {
|
|
730
811
|
try {
|
|
731
812
|
await result.current.execute();
|
|
732
813
|
} catch {
|
|
733
|
-
// Expected
|
|
814
|
+
// Expected
|
|
734
815
|
}
|
|
735
816
|
});
|
|
736
817
|
|
|
@@ -738,12 +819,12 @@ describe("useLazyQuery", () => {
|
|
|
738
819
|
});
|
|
739
820
|
|
|
740
821
|
test("reset clears query state", async () => {
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
name: "John",
|
|
744
|
-
});
|
|
822
|
+
const mockClient = createMockClient();
|
|
823
|
+
const mockQuery = createMockQueryResult<{ id: string }>({ id: "test" });
|
|
745
824
|
|
|
746
|
-
const { result } = renderHook(() => useLazyQuery(() => mockQuery, [])
|
|
825
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []), {
|
|
826
|
+
wrapper: createWrapper(mockClient),
|
|
827
|
+
});
|
|
747
828
|
|
|
748
829
|
await act(async () => {
|
|
749
830
|
await result.current.execute();
|
|
@@ -756,165 +837,195 @@ describe("useLazyQuery", () => {
|
|
|
756
837
|
});
|
|
757
838
|
|
|
758
839
|
expect(result.current.data).toBe(null);
|
|
759
|
-
expect(result.current.error).toBe(null);
|
|
760
|
-
expect(result.current.loading).toBe(false);
|
|
761
840
|
});
|
|
762
841
|
|
|
763
842
|
test("handles null query from accessor", async () => {
|
|
764
|
-
const
|
|
843
|
+
const mockClient = createMockClient();
|
|
765
844
|
|
|
766
|
-
|
|
845
|
+
const { result } = renderHook(() => useLazyQuery(() => null, []), {
|
|
846
|
+
wrapper: createWrapper(mockClient),
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
let executeResult: any;
|
|
767
850
|
await act(async () => {
|
|
768
|
-
|
|
851
|
+
executeResult = await result.current.execute();
|
|
769
852
|
});
|
|
770
853
|
|
|
771
|
-
expect(
|
|
854
|
+
expect(executeResult).toBe(null);
|
|
772
855
|
expect(result.current.data).toBe(null);
|
|
773
|
-
expect(result.current.loading).toBe(false);
|
|
774
856
|
});
|
|
775
857
|
|
|
776
858
|
test("handles undefined query from accessor", async () => {
|
|
777
|
-
const
|
|
859
|
+
const mockClient = createMockClient();
|
|
860
|
+
|
|
861
|
+
const { result } = renderHook(() => useLazyQuery(() => undefined, []), {
|
|
862
|
+
wrapper: createWrapper(mockClient),
|
|
863
|
+
});
|
|
778
864
|
|
|
779
|
-
let
|
|
865
|
+
let executeResult: any;
|
|
780
866
|
await act(async () => {
|
|
781
|
-
|
|
867
|
+
executeResult = await result.current.execute();
|
|
782
868
|
});
|
|
783
869
|
|
|
784
|
-
expect(
|
|
870
|
+
expect(executeResult).toBe(null);
|
|
785
871
|
expect(result.current.data).toBe(null);
|
|
786
|
-
expect(result.current.loading).toBe(false);
|
|
787
872
|
});
|
|
788
873
|
|
|
789
874
|
test("uses latest query value from accessor on execute", async () => {
|
|
790
|
-
|
|
791
|
-
const mockQuery1 = createMockQueryResult<string>("
|
|
792
|
-
const mockQuery2 = createMockQueryResult<string>("
|
|
793
|
-
|
|
794
|
-
|
|
875
|
+
const mockClient = createMockClient();
|
|
876
|
+
const mockQuery1 = createMockQueryResult<{ id: string }>({ id: "query1" });
|
|
877
|
+
const mockQuery2 = createMockQueryResult<{ id: string }>({ id: "query2" });
|
|
878
|
+
|
|
879
|
+
let useQuery1 = true;
|
|
880
|
+
const { result, rerender } = renderHook(
|
|
881
|
+
() => useLazyQuery(() => (useQuery1 ? mockQuery1 : mockQuery2), [useQuery1]),
|
|
882
|
+
{ wrapper: createWrapper(mockClient) },
|
|
883
|
+
);
|
|
795
884
|
|
|
796
|
-
|
|
885
|
+
// Change to query2 before executing
|
|
886
|
+
useQuery1 = false;
|
|
887
|
+
rerender();
|
|
797
888
|
|
|
798
|
-
// First execute
|
|
799
|
-
let queryResult1: string | undefined;
|
|
800
889
|
await act(async () => {
|
|
801
|
-
|
|
890
|
+
await result.current.execute();
|
|
802
891
|
});
|
|
803
892
|
|
|
804
|
-
expect(
|
|
805
|
-
|
|
806
|
-
// Change accessor to return different query
|
|
807
|
-
currentValue = "second";
|
|
893
|
+
expect(result.current.data?.id).toBe("query2");
|
|
894
|
+
});
|
|
808
895
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
896
|
+
test("shows loading state during execution", async () => {
|
|
897
|
+
const mockClient = createMockClient();
|
|
898
|
+
// Create the pending promise upfront so resolvePromise is assigned immediately
|
|
899
|
+
let resolvePromise!: (value: { id: string }) => void;
|
|
900
|
+
const pendingPromise = new Promise<{ id: string }>((resolve) => {
|
|
901
|
+
resolvePromise = resolve;
|
|
813
902
|
});
|
|
903
|
+
const mockQuery = {
|
|
904
|
+
then: (onFulfilled: any, onRejected?: any) => {
|
|
905
|
+
return pendingPromise.then(onFulfilled, onRejected);
|
|
906
|
+
},
|
|
907
|
+
} as unknown as QueryResult<{ id: string }>;
|
|
814
908
|
|
|
815
|
-
|
|
816
|
-
|
|
909
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []), {
|
|
910
|
+
wrapper: createWrapper(mockClient),
|
|
911
|
+
});
|
|
817
912
|
|
|
818
|
-
|
|
819
|
-
|
|
913
|
+
let executePromise: Promise<any>;
|
|
914
|
+
act(() => {
|
|
915
|
+
executePromise = result.current.execute();
|
|
916
|
+
});
|
|
820
917
|
|
|
821
|
-
|
|
918
|
+
expect(result.current.loading).toBe(true);
|
|
822
919
|
|
|
823
|
-
// Execute and set value
|
|
824
|
-
let executePromise: Promise<{ id: string }>;
|
|
825
920
|
await act(async () => {
|
|
826
|
-
|
|
827
|
-
mockQuery._setValue({ id: "123" });
|
|
921
|
+
resolvePromise({ id: "test" });
|
|
828
922
|
await executePromise;
|
|
829
923
|
});
|
|
830
924
|
|
|
831
925
|
expect(result.current.loading).toBe(false);
|
|
832
|
-
expect(result.current.data).toEqual({ id: "123" });
|
|
833
926
|
});
|
|
834
927
|
|
|
835
928
|
test("does not update state after unmount", async () => {
|
|
836
|
-
const
|
|
929
|
+
const mockClient = createMockClient();
|
|
930
|
+
// Create the pending promise upfront so resolvePromise is assigned immediately
|
|
931
|
+
let resolvePromise!: (value: { id: string }) => void;
|
|
932
|
+
const pendingPromise = new Promise<{ id: string }>((resolve) => {
|
|
933
|
+
resolvePromise = resolve;
|
|
934
|
+
});
|
|
935
|
+
const mockQuery = {
|
|
936
|
+
then: (onFulfilled: any, onRejected?: any) => {
|
|
937
|
+
return pendingPromise.then(onFulfilled, onRejected);
|
|
938
|
+
},
|
|
939
|
+
} as unknown as QueryResult<{ id: string }>;
|
|
837
940
|
|
|
838
|
-
const { result, unmount } = renderHook(() => useLazyQuery(() => mockQuery, [])
|
|
941
|
+
const { result, unmount } = renderHook(() => useLazyQuery(() => mockQuery, []), {
|
|
942
|
+
wrapper: createWrapper(mockClient),
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
let executePromise: Promise<any>;
|
|
946
|
+
act(() => {
|
|
947
|
+
executePromise = result.current.execute();
|
|
948
|
+
});
|
|
839
949
|
|
|
840
|
-
// Start execution, unmount, then resolve
|
|
841
|
-
const executePromise = result.current.execute();
|
|
842
950
|
unmount();
|
|
843
951
|
|
|
844
|
-
// Resolve after unmount
|
|
952
|
+
// Resolve after unmount - should not cause errors
|
|
845
953
|
await act(async () => {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
await executePromise;
|
|
849
|
-
} catch {
|
|
850
|
-
// May reject due to unmount
|
|
851
|
-
}
|
|
954
|
+
resolvePromise({ id: "test" });
|
|
955
|
+
await executePromise;
|
|
852
956
|
});
|
|
853
|
-
|
|
854
|
-
// Test passes if no error is thrown (state update after unmount would cause error)
|
|
855
|
-
expect(true).toBe(true);
|
|
856
957
|
});
|
|
857
958
|
|
|
858
959
|
test("can execute multiple times", async () => {
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
960
|
+
const mockClient = createMockClient();
|
|
961
|
+
let callCount = 0;
|
|
962
|
+
const mockQuery = {
|
|
963
|
+
then: (onFulfilled: any) => {
|
|
964
|
+
callCount++;
|
|
965
|
+
return Promise.resolve({ count: callCount }).then(onFulfilled);
|
|
966
|
+
},
|
|
967
|
+
} as unknown as QueryResult<{ count: number }>;
|
|
864
968
|
|
|
865
|
-
const { result } = renderHook(() => useLazyQuery(() =>
|
|
969
|
+
const { result } = renderHook(() => useLazyQuery(() => mockQuery, []), {
|
|
970
|
+
wrapper: createWrapper(mockClient),
|
|
971
|
+
});
|
|
866
972
|
|
|
867
|
-
// First execution
|
|
868
973
|
await act(async () => {
|
|
869
974
|
await result.current.execute();
|
|
870
975
|
});
|
|
871
|
-
|
|
872
976
|
expect(result.current.data?.count).toBe(1);
|
|
873
977
|
|
|
874
|
-
// Second execution
|
|
875
978
|
await act(async () => {
|
|
876
979
|
await result.current.execute();
|
|
877
980
|
});
|
|
878
|
-
|
|
879
981
|
expect(result.current.data?.count).toBe(2);
|
|
880
982
|
});
|
|
881
983
|
|
|
882
984
|
test("Route + Params pattern works", async () => {
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
name: "John",
|
|
886
|
-
});
|
|
985
|
+
const mockClient = createMockClient();
|
|
986
|
+
const mockQuery = createMockQueryResult<{ id: string }>({ id: "user-123" });
|
|
887
987
|
const route = (_params: { id: string }) => mockQuery;
|
|
888
988
|
|
|
889
|
-
const { result } = renderHook(() => useLazyQuery(route, { id: "123" })
|
|
989
|
+
const { result } = renderHook(() => useLazyQuery(() => route, { id: "123" }), {
|
|
990
|
+
wrapper: createWrapper(mockClient),
|
|
991
|
+
});
|
|
890
992
|
|
|
891
993
|
await act(async () => {
|
|
892
994
|
await result.current.execute();
|
|
893
995
|
});
|
|
894
996
|
|
|
895
|
-
expect(result.current.data).
|
|
997
|
+
expect(result.current.data?.id).toBe("user-123");
|
|
896
998
|
});
|
|
897
999
|
|
|
898
1000
|
test("Route + Params with null route", async () => {
|
|
899
|
-
const
|
|
1001
|
+
const mockClient = createMockClient();
|
|
900
1002
|
|
|
1003
|
+
const { result } = renderHook(() => useLazyQuery(() => null, { id: "123" }), {
|
|
1004
|
+
wrapper: createWrapper(mockClient),
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
let executeResult: any;
|
|
901
1008
|
await act(async () => {
|
|
902
|
-
await result.current.execute();
|
|
1009
|
+
executeResult = await result.current.execute();
|
|
903
1010
|
});
|
|
904
1011
|
|
|
1012
|
+
expect(executeResult).toBe(null);
|
|
905
1013
|
expect(result.current.data).toBe(null);
|
|
906
1014
|
});
|
|
907
1015
|
|
|
908
1016
|
test("select transforms the data", async () => {
|
|
1017
|
+
const mockClient = createMockClient();
|
|
909
1018
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>({
|
|
910
1019
|
id: "123",
|
|
911
1020
|
name: "John",
|
|
912
1021
|
});
|
|
913
1022
|
|
|
914
|
-
const { result } = renderHook(
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1023
|
+
const { result } = renderHook(
|
|
1024
|
+
() =>
|
|
1025
|
+
useLazyQuery(() => mockQuery, [], {
|
|
1026
|
+
select: (data) => data.name.toUpperCase(),
|
|
1027
|
+
}),
|
|
1028
|
+
{ wrapper: createWrapper(mockClient) },
|
|
918
1029
|
);
|
|
919
1030
|
|
|
920
1031
|
await act(async () => {
|