@sylphx/lens-react 1.2.13 → 1.2.21
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/package.json +2 -2
- package/src/hooks.test.tsx +609 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sylphx/lens-react",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.21",
|
|
4
4
|
"description": "React bindings for Lens API framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"author": "SylphxAI",
|
|
31
31
|
"license": "MIT",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@sylphx/lens-client": "^1.
|
|
33
|
+
"@sylphx/lens-client": "^1.15.2"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"react": ">=18.0.0"
|
package/src/hooks.test.tsx
CHANGED
|
@@ -149,6 +149,22 @@ describe("useQuery", () => {
|
|
|
149
149
|
expect(result.current.data).toBe(null);
|
|
150
150
|
});
|
|
151
151
|
|
|
152
|
+
test("handles non-Error rejection", async () => {
|
|
153
|
+
const mockQuery = {
|
|
154
|
+
subscribe: () => () => {},
|
|
155
|
+
then: (onFulfilled: any, onRejected: any) => {
|
|
156
|
+
// Reject with a string instead of Error
|
|
157
|
+
return Promise.reject("String error").then(onFulfilled, onRejected);
|
|
158
|
+
},
|
|
159
|
+
} as unknown as QueryResult<{ id: string }>;
|
|
160
|
+
|
|
161
|
+
const { result } = renderHook(() => useQuery(mockQuery));
|
|
162
|
+
|
|
163
|
+
await waitFor(() => {
|
|
164
|
+
expect(result.current.error?.message).toBe("String error");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
152
168
|
test("skips query when skip option is true", () => {
|
|
153
169
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
154
170
|
|
|
@@ -158,6 +174,55 @@ describe("useQuery", () => {
|
|
|
158
174
|
expect(result.current.data).toBe(null);
|
|
159
175
|
});
|
|
160
176
|
|
|
177
|
+
test("handles null query", () => {
|
|
178
|
+
const { result } = renderHook(() => useQuery(null));
|
|
179
|
+
|
|
180
|
+
expect(result.current.loading).toBe(false);
|
|
181
|
+
expect(result.current.data).toBe(null);
|
|
182
|
+
expect(result.current.error).toBe(null);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("handles undefined query", () => {
|
|
186
|
+
const { result } = renderHook(() => useQuery(undefined));
|
|
187
|
+
|
|
188
|
+
expect(result.current.loading).toBe(false);
|
|
189
|
+
expect(result.current.data).toBe(null);
|
|
190
|
+
expect(result.current.error).toBe(null);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("handles accessor function returning query", async () => {
|
|
194
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
195
|
+
const accessor = () => mockQuery;
|
|
196
|
+
|
|
197
|
+
const { result } = renderHook(() => useQuery(accessor));
|
|
198
|
+
|
|
199
|
+
act(() => {
|
|
200
|
+
mockQuery._setValue({ id: "123", name: "John" });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
await waitFor(() => {
|
|
204
|
+
expect(result.current.data).toEqual({ id: "123", name: "John" });
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("handles accessor function returning null", () => {
|
|
209
|
+
const accessor = () => null;
|
|
210
|
+
|
|
211
|
+
const { result } = renderHook(() => useQuery(accessor));
|
|
212
|
+
|
|
213
|
+
expect(result.current.loading).toBe(false);
|
|
214
|
+
expect(result.current.data).toBe(null);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("handles accessor function returning undefined", () => {
|
|
218
|
+
const accessor = () => undefined;
|
|
219
|
+
|
|
220
|
+
const { result } = renderHook(() => useQuery(accessor));
|
|
221
|
+
|
|
222
|
+
expect(result.current.loading).toBe(false);
|
|
223
|
+
expect(result.current.data).toBe(null);
|
|
224
|
+
});
|
|
225
|
+
|
|
161
226
|
test("updates when query subscription emits", async () => {
|
|
162
227
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
163
228
|
|
|
@@ -181,6 +246,235 @@ describe("useQuery", () => {
|
|
|
181
246
|
expect(result.current.data?.name).toBe("Jane");
|
|
182
247
|
});
|
|
183
248
|
});
|
|
249
|
+
|
|
250
|
+
test("refetch reloads the query", async () => {
|
|
251
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
252
|
+
|
|
253
|
+
const { result } = renderHook(() => useQuery(mockQuery));
|
|
254
|
+
|
|
255
|
+
// Initial load
|
|
256
|
+
act(() => {
|
|
257
|
+
mockQuery._setValue({ id: "123", name: "John" });
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
await waitFor(() => {
|
|
261
|
+
expect(result.current.data?.name).toBe("John");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Refetch should trigger loading state and reload
|
|
265
|
+
act(() => {
|
|
266
|
+
result.current.refetch();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Note: In this mock, refetch will resolve with the same data
|
|
270
|
+
// In a real scenario, it would trigger a new network request
|
|
271
|
+
await waitFor(() => {
|
|
272
|
+
expect(result.current.loading).toBe(false);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
expect(result.current.data).toEqual({ id: "123", name: "John" });
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("refetch handles errors", async () => {
|
|
279
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
280
|
+
|
|
281
|
+
const { result } = renderHook(() => useQuery(mockQuery));
|
|
282
|
+
|
|
283
|
+
// Initial load succeeds
|
|
284
|
+
act(() => {
|
|
285
|
+
mockQuery._setValue({ id: "123", name: "John" });
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await waitFor(() => {
|
|
289
|
+
expect(result.current.data?.name).toBe("John");
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// Create a new query that will fail
|
|
293
|
+
const failingQuery = {
|
|
294
|
+
subscribe: () => () => {},
|
|
295
|
+
then: (onFulfilled: any, onRejected: any) => {
|
|
296
|
+
return Promise.reject(new Error("Refetch failed")).then(onFulfilled, onRejected);
|
|
297
|
+
},
|
|
298
|
+
} as unknown as QueryResult<{ id: string; name: string }>;
|
|
299
|
+
|
|
300
|
+
// Update the query to use failing query
|
|
301
|
+
const { result: result2 } = renderHook(() => useQuery(failingQuery));
|
|
302
|
+
|
|
303
|
+
await waitFor(() => {
|
|
304
|
+
expect(result2.current.error?.message).toBe("Refetch failed");
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("refetch does nothing when query is null", () => {
|
|
309
|
+
const { result } = renderHook(() => useQuery(null));
|
|
310
|
+
|
|
311
|
+
act(() => {
|
|
312
|
+
result.current.refetch();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
expect(result.current.loading).toBe(false);
|
|
316
|
+
expect(result.current.data).toBe(null);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("refetch does nothing when skip is true", () => {
|
|
320
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
321
|
+
|
|
322
|
+
const { result } = renderHook(() => useQuery(mockQuery, { skip: true }));
|
|
323
|
+
|
|
324
|
+
act(() => {
|
|
325
|
+
result.current.refetch();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(result.current.loading).toBe(false);
|
|
329
|
+
expect(result.current.data).toBe(null);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("refetch with non-Error rejection", async () => {
|
|
333
|
+
let shouldFail = false;
|
|
334
|
+
const mockQuery = {
|
|
335
|
+
subscribe: () => () => {},
|
|
336
|
+
then: (onFulfilled: any, onRejected: any) => {
|
|
337
|
+
if (shouldFail) {
|
|
338
|
+
return Promise.reject("Refetch string error").then(onFulfilled, onRejected);
|
|
339
|
+
}
|
|
340
|
+
return Promise.resolve({ id: "123", name: "John" } as any).then(onFulfilled, onRejected);
|
|
341
|
+
},
|
|
342
|
+
} as unknown as QueryResult<{ id: string; name: string }>;
|
|
343
|
+
|
|
344
|
+
const { result } = renderHook(() => useQuery(mockQuery));
|
|
345
|
+
|
|
346
|
+
await waitFor(() => {
|
|
347
|
+
expect(result.current.data).toEqual({ id: "123", name: "John" });
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Make it fail on refetch
|
|
351
|
+
shouldFail = true;
|
|
352
|
+
|
|
353
|
+
act(() => {
|
|
354
|
+
result.current.refetch();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
await waitFor(() => {
|
|
358
|
+
expect(result.current.error?.message).toBe("Refetch string error");
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("cleans up subscription on unmount", async () => {
|
|
363
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
364
|
+
let unsubscribeCalled = false;
|
|
365
|
+
|
|
366
|
+
// Override subscribe to track unsubscribe
|
|
367
|
+
const originalSubscribe = mockQuery.subscribe;
|
|
368
|
+
mockQuery.subscribe = (callback?: (data: { id: string; name: string }) => void) => {
|
|
369
|
+
const originalUnsubscribe = originalSubscribe.call(mockQuery, callback);
|
|
370
|
+
return () => {
|
|
371
|
+
unsubscribeCalled = true;
|
|
372
|
+
originalUnsubscribe();
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const { unmount } = renderHook(() => useQuery(mockQuery));
|
|
377
|
+
|
|
378
|
+
unmount();
|
|
379
|
+
|
|
380
|
+
expect(unsubscribeCalled).toBe(true);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("does not update state after unmount", async () => {
|
|
384
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
385
|
+
|
|
386
|
+
const { unmount } = renderHook(() => useQuery(mockQuery));
|
|
387
|
+
|
|
388
|
+
// Unmount before query resolves
|
|
389
|
+
unmount();
|
|
390
|
+
|
|
391
|
+
// Try to set value after unmount
|
|
392
|
+
act(() => {
|
|
393
|
+
mockQuery._setValue({ id: "123", name: "John" });
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// State should not be updated (we can't really assert this directly,
|
|
397
|
+
// but the test passing without errors shows no state update occurred)
|
|
398
|
+
expect(true).toBe(true);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("handles query change", async () => {
|
|
402
|
+
const mockQuery1 = createMockQueryResult<{ id: string; name: string }>();
|
|
403
|
+
const mockQuery2 = createMockQueryResult<{ id: string; name: string }>();
|
|
404
|
+
|
|
405
|
+
let currentQuery = mockQuery1;
|
|
406
|
+
const { result, rerender } = renderHook(() => useQuery(currentQuery));
|
|
407
|
+
|
|
408
|
+
// Load first query
|
|
409
|
+
act(() => {
|
|
410
|
+
mockQuery1._setValue({ id: "1", name: "First" });
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
await waitFor(() => {
|
|
414
|
+
expect(result.current.data?.name).toBe("First");
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Change to second query
|
|
418
|
+
currentQuery = mockQuery2;
|
|
419
|
+
rerender();
|
|
420
|
+
|
|
421
|
+
expect(result.current.loading).toBe(true);
|
|
422
|
+
|
|
423
|
+
// Load second query
|
|
424
|
+
act(() => {
|
|
425
|
+
mockQuery2._setValue({ id: "2", name: "Second" });
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
await waitFor(() => {
|
|
429
|
+
expect(result.current.data?.name).toBe("Second");
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("handles skip option change from true to false", async () => {
|
|
434
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
435
|
+
|
|
436
|
+
let skip = true;
|
|
437
|
+
const { result, rerender } = renderHook(() => useQuery(mockQuery, { skip }));
|
|
438
|
+
|
|
439
|
+
expect(result.current.loading).toBe(false);
|
|
440
|
+
|
|
441
|
+
// Change skip to false
|
|
442
|
+
skip = false;
|
|
443
|
+
rerender();
|
|
444
|
+
|
|
445
|
+
expect(result.current.loading).toBe(true);
|
|
446
|
+
|
|
447
|
+
act(() => {
|
|
448
|
+
mockQuery._setValue({ id: "123", name: "John" });
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
await waitFor(() => {
|
|
452
|
+
expect(result.current.data).toEqual({ id: "123", name: "John" });
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("handles skip option change from false to true", async () => {
|
|
457
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
458
|
+
|
|
459
|
+
let skip = false;
|
|
460
|
+
const { result, rerender } = renderHook(() => useQuery(mockQuery, { skip }));
|
|
461
|
+
|
|
462
|
+
act(() => {
|
|
463
|
+
mockQuery._setValue({ id: "123", name: "John" });
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
await waitFor(() => {
|
|
467
|
+
expect(result.current.data).toEqual({ id: "123", name: "John" });
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
// Change skip to true
|
|
471
|
+
skip = true;
|
|
472
|
+
rerender();
|
|
473
|
+
|
|
474
|
+
expect(result.current.loading).toBe(false);
|
|
475
|
+
expect(result.current.data).toBe(null);
|
|
476
|
+
expect(result.current.error).toBe(null);
|
|
477
|
+
});
|
|
184
478
|
});
|
|
185
479
|
|
|
186
480
|
// =============================================================================
|
|
@@ -229,6 +523,25 @@ describe("useMutation", () => {
|
|
|
229
523
|
expect(result.current.loading).toBe(false);
|
|
230
524
|
});
|
|
231
525
|
|
|
526
|
+
test("handles non-Error exception in mutation", async () => {
|
|
527
|
+
const mutationFn = async (_input: { name: string }): Promise<MutationResult<{ id: string; name: string }>> => {
|
|
528
|
+
throw "String error";
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const { result } = renderHook(() => useMutation(mutationFn));
|
|
532
|
+
|
|
533
|
+
await act(async () => {
|
|
534
|
+
try {
|
|
535
|
+
await result.current.mutate({ name: "New User" });
|
|
536
|
+
} catch {
|
|
537
|
+
// Expected error
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
expect(result.current.error?.message).toBe("String error");
|
|
542
|
+
expect(result.current.loading).toBe(false);
|
|
543
|
+
});
|
|
544
|
+
|
|
232
545
|
test("shows loading state during mutation", async () => {
|
|
233
546
|
let resolveMutation: ((value: MutationResult<{ id: string }>) => void) | null = null;
|
|
234
547
|
const mutationFn = async (_input: { name: string }): Promise<MutationResult<{ id: string }>> => {
|
|
@@ -278,6 +591,99 @@ describe("useMutation", () => {
|
|
|
278
591
|
expect(result.current.error).toBe(null);
|
|
279
592
|
expect(result.current.loading).toBe(false);
|
|
280
593
|
});
|
|
594
|
+
|
|
595
|
+
test("handles multiple mutations in sequence", async () => {
|
|
596
|
+
let counter = 0;
|
|
597
|
+
const mutationFn = async (input: { name: string }): Promise<MutationResult<{ id: string; name: string }>> => {
|
|
598
|
+
counter++;
|
|
599
|
+
return { data: { id: `id-${counter}`, name: input.name } };
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
const { result } = renderHook(() => useMutation(mutationFn));
|
|
603
|
+
|
|
604
|
+
// First mutation
|
|
605
|
+
await act(async () => {
|
|
606
|
+
await result.current.mutate({ name: "First" });
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
expect(result.current.data).toEqual({ id: "id-1", name: "First" });
|
|
610
|
+
|
|
611
|
+
// Second mutation
|
|
612
|
+
await act(async () => {
|
|
613
|
+
await result.current.mutate({ name: "Second" });
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
expect(result.current.data).toEqual({ id: "id-2", name: "Second" });
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
test("clears error on successful mutation after previous error", async () => {
|
|
620
|
+
let shouldFail = true;
|
|
621
|
+
const mutationFn = async (input: { name: string }): Promise<MutationResult<{ id: string; name: string }>> => {
|
|
622
|
+
if (shouldFail) {
|
|
623
|
+
throw new Error("Mutation failed");
|
|
624
|
+
}
|
|
625
|
+
return { data: { id: "new-id", name: input.name } };
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const { result } = renderHook(() => useMutation(mutationFn));
|
|
629
|
+
|
|
630
|
+
// First mutation fails
|
|
631
|
+
await act(async () => {
|
|
632
|
+
try {
|
|
633
|
+
await result.current.mutate({ name: "New User" });
|
|
634
|
+
} catch {
|
|
635
|
+
// Expected error
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
expect(result.current.error?.message).toBe("Mutation failed");
|
|
640
|
+
|
|
641
|
+
// Second mutation succeeds
|
|
642
|
+
shouldFail = false;
|
|
643
|
+
await act(async () => {
|
|
644
|
+
await result.current.mutate({ name: "New User" });
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
expect(result.current.error).toBe(null);
|
|
648
|
+
expect(result.current.data).toEqual({ id: "new-id", name: "New User" });
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
test("does not update state after unmount", async () => {
|
|
652
|
+
const mutationFn = async (input: { name: string }): Promise<MutationResult<{ id: string; name: string }>> => {
|
|
653
|
+
return { data: { id: "new-id", name: input.name } };
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const { result, unmount } = renderHook(() => useMutation(mutationFn));
|
|
657
|
+
|
|
658
|
+
// Start mutation but unmount before it completes
|
|
659
|
+
const mutationPromise = result.current.mutate({ name: "New User" });
|
|
660
|
+
unmount();
|
|
661
|
+
|
|
662
|
+
// Wait for mutation to complete
|
|
663
|
+
await mutationPromise;
|
|
664
|
+
|
|
665
|
+
// Test passes if no error is thrown (state update after unmount would cause error)
|
|
666
|
+
expect(true).toBe(true);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
test("mutation result includes rollback function when provided", async () => {
|
|
670
|
+
const rollbackFn = () => console.log("Rollback");
|
|
671
|
+
const mutationFn = async (input: { name: string }): Promise<MutationResult<{ id: string; name: string }>> => {
|
|
672
|
+
return {
|
|
673
|
+
data: { id: "new-id", name: input.name },
|
|
674
|
+
rollback: rollbackFn,
|
|
675
|
+
};
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
const { result } = renderHook(() => useMutation(mutationFn));
|
|
679
|
+
|
|
680
|
+
let mutationResult: MutationResult<{ id: string; name: string }> | undefined;
|
|
681
|
+
await act(async () => {
|
|
682
|
+
mutationResult = await result.current.mutate({ name: "New User" });
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
expect(mutationResult?.rollback).toBe(rollbackFn);
|
|
686
|
+
});
|
|
281
687
|
});
|
|
282
688
|
|
|
283
689
|
// =============================================================================
|
|
@@ -334,6 +740,26 @@ describe("useLazyQuery", () => {
|
|
|
334
740
|
expect(result.current.error?.message).toBe("Query failed");
|
|
335
741
|
});
|
|
336
742
|
|
|
743
|
+
test("handles non-Error rejection", async () => {
|
|
744
|
+
const mockQuery = {
|
|
745
|
+
then: (onFulfilled: any, onRejected: any) => {
|
|
746
|
+
return Promise.reject("String error").then(onFulfilled, onRejected);
|
|
747
|
+
},
|
|
748
|
+
} as unknown as QueryResult<{ id: string }>;
|
|
749
|
+
|
|
750
|
+
const { result } = renderHook(() => useLazyQuery(mockQuery));
|
|
751
|
+
|
|
752
|
+
await act(async () => {
|
|
753
|
+
try {
|
|
754
|
+
await result.current.execute();
|
|
755
|
+
} catch {
|
|
756
|
+
// Expected error
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
expect(result.current.error?.message).toBe("String error");
|
|
761
|
+
});
|
|
762
|
+
|
|
337
763
|
test("reset clears query state", async () => {
|
|
338
764
|
const mockQuery = createMockQueryResult<{ id: string; name: string }>({
|
|
339
765
|
id: "123",
|
|
@@ -356,4 +782,187 @@ describe("useLazyQuery", () => {
|
|
|
356
782
|
expect(result.current.error).toBe(null);
|
|
357
783
|
expect(result.current.loading).toBe(false);
|
|
358
784
|
});
|
|
785
|
+
|
|
786
|
+
test("handles null query", async () => {
|
|
787
|
+
const { result } = renderHook(() => useLazyQuery(null));
|
|
788
|
+
|
|
789
|
+
let queryResult: any;
|
|
790
|
+
await act(async () => {
|
|
791
|
+
queryResult = await result.current.execute();
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
expect(queryResult).toBe(null);
|
|
795
|
+
expect(result.current.data).toBe(null);
|
|
796
|
+
expect(result.current.loading).toBe(false);
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
test("handles undefined query", async () => {
|
|
800
|
+
const { result } = renderHook(() => useLazyQuery(undefined));
|
|
801
|
+
|
|
802
|
+
let queryResult: any;
|
|
803
|
+
await act(async () => {
|
|
804
|
+
queryResult = await result.current.execute();
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
expect(queryResult).toBe(null);
|
|
808
|
+
expect(result.current.data).toBe(null);
|
|
809
|
+
expect(result.current.loading).toBe(false);
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
test("handles accessor function returning query", async () => {
|
|
813
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>({
|
|
814
|
+
id: "123",
|
|
815
|
+
name: "John",
|
|
816
|
+
});
|
|
817
|
+
const accessor = () => mockQuery;
|
|
818
|
+
|
|
819
|
+
const { result } = renderHook(() => useLazyQuery(accessor));
|
|
820
|
+
|
|
821
|
+
let queryResult: { id: string; name: string } | undefined;
|
|
822
|
+
await act(async () => {
|
|
823
|
+
queryResult = await result.current.execute();
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
expect(queryResult).toEqual({ id: "123", name: "John" });
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
test("handles accessor function returning null", async () => {
|
|
830
|
+
const accessor = () => null;
|
|
831
|
+
|
|
832
|
+
const { result } = renderHook(() => useLazyQuery(accessor));
|
|
833
|
+
|
|
834
|
+
let queryResult: any;
|
|
835
|
+
await act(async () => {
|
|
836
|
+
queryResult = await result.current.execute();
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
expect(queryResult).toBe(null);
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
test("uses latest query value from accessor on execute", async () => {
|
|
843
|
+
let currentValue = "first";
|
|
844
|
+
const mockQuery1 = createMockQueryResult<string>("first");
|
|
845
|
+
const mockQuery2 = createMockQueryResult<string>("second");
|
|
846
|
+
|
|
847
|
+
const accessor = () => (currentValue === "first" ? mockQuery1 : mockQuery2);
|
|
848
|
+
|
|
849
|
+
const { result } = renderHook(() => useLazyQuery(accessor));
|
|
850
|
+
|
|
851
|
+
// First execute
|
|
852
|
+
let queryResult1: string | undefined;
|
|
853
|
+
await act(async () => {
|
|
854
|
+
queryResult1 = await result.current.execute();
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
expect(queryResult1).toBe("first");
|
|
858
|
+
|
|
859
|
+
// Change accessor to return different query
|
|
860
|
+
currentValue = "second";
|
|
861
|
+
|
|
862
|
+
// Second execute should use new query
|
|
863
|
+
let queryResult2: string | undefined;
|
|
864
|
+
await act(async () => {
|
|
865
|
+
queryResult2 = await result.current.execute();
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
expect(queryResult2).toBe("second");
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
test("shows loading state during execution", async () => {
|
|
872
|
+
const mockQuery = createMockQueryResult<{ id: string }>();
|
|
873
|
+
|
|
874
|
+
const { result } = renderHook(() => useLazyQuery(mockQuery));
|
|
875
|
+
|
|
876
|
+
// Execute and set value
|
|
877
|
+
let executePromise: Promise<{ id: string }>;
|
|
878
|
+
await act(async () => {
|
|
879
|
+
executePromise = result.current.execute();
|
|
880
|
+
mockQuery._setValue({ id: "123" });
|
|
881
|
+
await executePromise;
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
expect(result.current.loading).toBe(false);
|
|
885
|
+
expect(result.current.data).toEqual({ id: "123" });
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
test("does not update state after unmount", async () => {
|
|
889
|
+
const mockQuery = createMockQueryResult<{ id: string; name: string }>();
|
|
890
|
+
|
|
891
|
+
const { result, unmount } = renderHook(() => useLazyQuery(mockQuery));
|
|
892
|
+
|
|
893
|
+
// Start execution, unmount, then resolve
|
|
894
|
+
const executePromise = result.current.execute();
|
|
895
|
+
unmount();
|
|
896
|
+
|
|
897
|
+
// Resolve after unmount
|
|
898
|
+
await act(async () => {
|
|
899
|
+
mockQuery._setValue({ id: "123", name: "John" });
|
|
900
|
+
await executePromise;
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
// Test passes if no error is thrown (state update after unmount would cause error)
|
|
904
|
+
expect(true).toBe(true);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
test("clears error on successful execute after previous error", async () => {
|
|
908
|
+
const mockQuery1 = createMockQueryResult<{ id: string }>();
|
|
909
|
+
const mockQuery2 = createMockQueryResult<{ id: string }>({ id: "123" });
|
|
910
|
+
|
|
911
|
+
const { result, rerender } = renderHook(({ query }) => useLazyQuery(query), {
|
|
912
|
+
initialProps: { query: mockQuery1 },
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
// First execution fails
|
|
916
|
+
await act(async () => {
|
|
917
|
+
const executePromise = result.current.execute();
|
|
918
|
+
mockQuery1._setError(new Error("Query failed"));
|
|
919
|
+
try {
|
|
920
|
+
await executePromise;
|
|
921
|
+
} catch {
|
|
922
|
+
// Expected error
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
expect(result.current.error?.message).toBe("Query failed");
|
|
927
|
+
|
|
928
|
+
// Switch to successful query
|
|
929
|
+
rerender({ query: mockQuery2 });
|
|
930
|
+
|
|
931
|
+
// Second execution succeeds
|
|
932
|
+
await act(async () => {
|
|
933
|
+
await result.current.execute();
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
expect(result.current.error).toBe(null);
|
|
937
|
+
expect(result.current.data).toEqual({ id: "123" });
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
test("can execute multiple times", async () => {
|
|
941
|
+
const mockQuery1 = createMockQueryResult<{ count: number }>();
|
|
942
|
+
const mockQuery2 = createMockQueryResult<{ count: number }>();
|
|
943
|
+
|
|
944
|
+
const { result, rerender } = renderHook(({ query }) => useLazyQuery(query), {
|
|
945
|
+
initialProps: { query: mockQuery1 },
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
// First execution
|
|
949
|
+
await act(async () => {
|
|
950
|
+
const executePromise = result.current.execute();
|
|
951
|
+
mockQuery1._setValue({ count: 1 });
|
|
952
|
+
await executePromise;
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
expect(result.current.data?.count).toBe(1);
|
|
956
|
+
|
|
957
|
+
// Change to second query and execute again
|
|
958
|
+
rerender({ query: mockQuery2 });
|
|
959
|
+
|
|
960
|
+
await act(async () => {
|
|
961
|
+
const executePromise = result.current.execute();
|
|
962
|
+
mockQuery2._setValue({ count: 2 });
|
|
963
|
+
await executePromise;
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
expect(result.current.data?.count).toBe(2);
|
|
967
|
+
});
|
|
359
968
|
});
|