@ic-reactor/react 3.0.0-beta.7 → 3.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 (46) hide show
  1. package/README.md +11 -10
  2. package/dist/createActorHooks.d.ts +2 -0
  3. package/dist/createActorHooks.d.ts.map +1 -1
  4. package/dist/createActorHooks.js +2 -0
  5. package/dist/createActorHooks.js.map +1 -1
  6. package/dist/createMutation.d.ts.map +1 -1
  7. package/dist/createMutation.js +4 -0
  8. package/dist/createMutation.js.map +1 -1
  9. package/dist/hooks/index.d.ts +18 -5
  10. package/dist/hooks/index.d.ts.map +1 -1
  11. package/dist/hooks/index.js +15 -5
  12. package/dist/hooks/index.js.map +1 -1
  13. package/dist/hooks/useActorInfiniteQuery.d.ts +13 -11
  14. package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
  15. package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
  16. package/dist/hooks/useActorMethod.d.ts +105 -0
  17. package/dist/hooks/useActorMethod.d.ts.map +1 -0
  18. package/dist/hooks/useActorMethod.js +192 -0
  19. package/dist/hooks/useActorMethod.js.map +1 -0
  20. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +13 -10
  21. package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
  22. package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
  23. package/package.json +9 -8
  24. package/src/createActorHooks.ts +146 -0
  25. package/src/createAuthHooks.ts +137 -0
  26. package/src/createInfiniteQuery.ts +471 -0
  27. package/src/createMutation.ts +163 -0
  28. package/src/createQuery.ts +197 -0
  29. package/src/createSuspenseInfiniteQuery.ts +478 -0
  30. package/src/createSuspenseQuery.ts +215 -0
  31. package/src/hooks/index.ts +93 -0
  32. package/src/hooks/useActorInfiniteQuery.test.tsx +457 -0
  33. package/src/hooks/useActorInfiniteQuery.ts +134 -0
  34. package/src/hooks/useActorMethod.test.tsx +798 -0
  35. package/src/hooks/useActorMethod.ts +397 -0
  36. package/src/hooks/useActorMutation.test.tsx +220 -0
  37. package/src/hooks/useActorMutation.ts +124 -0
  38. package/src/hooks/useActorQuery.test.tsx +287 -0
  39. package/src/hooks/useActorQuery.ts +110 -0
  40. package/src/hooks/useActorSuspenseInfiniteQuery.test.tsx +472 -0
  41. package/src/hooks/useActorSuspenseInfiniteQuery.ts +137 -0
  42. package/src/hooks/useActorSuspenseQuery.test.tsx +254 -0
  43. package/src/hooks/useActorSuspenseQuery.ts +112 -0
  44. package/src/index.ts +21 -0
  45. package/src/types.ts +435 -0
  46. package/src/validation.ts +202 -0
@@ -0,0 +1,798 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest"
2
+ import { renderHook, waitFor } from "@testing-library/react"
3
+ import React from "react"
4
+ import { ClientManager, Reactor, CallError } from "@ic-reactor/core"
5
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
6
+ import { useActorMethod } from "./useActorMethod"
7
+ import { ActorMethod } from "@icp-sdk/core/agent"
8
+
9
+ const idlFactory = ({ IDL }: any) => {
10
+ return IDL.Service({
11
+ greet: IDL.Func([IDL.Text], [IDL.Text], ["query"]),
12
+ transfer: IDL.Func(
13
+ [IDL.Record({ to: IDL.Text, amount: IDL.Nat })],
14
+ [IDL.Bool],
15
+ []
16
+ ),
17
+ })
18
+ }
19
+
20
+ interface TestActor {
21
+ greet: ActorMethod<[string], string>
22
+ transfer: ActorMethod<[{ to: string; amount: bigint }], boolean>
23
+ }
24
+
25
+ describe("useActorMethod", () => {
26
+ let queryClient: QueryClient
27
+ let clientManager: ClientManager
28
+ let reactor: Reactor<TestActor>
29
+
30
+ beforeEach(() => {
31
+ queryClient = new QueryClient({
32
+ defaultOptions: {
33
+ queries: { retry: false },
34
+ mutations: { retry: false },
35
+ },
36
+ })
37
+
38
+ clientManager = new ClientManager({
39
+ queryClient,
40
+ })
41
+
42
+ reactor = new Reactor<TestActor>({
43
+ clientManager,
44
+ canisterId: "rrkah-fqaaa-aaaaa-aaaaq-cai",
45
+ idlFactory,
46
+ name: "test",
47
+ })
48
+
49
+ // Mock reactor.callMethod instead of the entire agent stack
50
+ vi.spyOn(reactor, "callMethod").mockImplementation(
51
+ async ({ functionName, args }: any): Promise<any> => {
52
+ if (functionName === "greet") {
53
+ return `Hello, ${args[0]}!`
54
+ }
55
+ if (functionName === "transfer") {
56
+ return true
57
+ }
58
+ return null
59
+ }
60
+ )
61
+ })
62
+
63
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
64
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
65
+ )
66
+
67
+ it("should detect query method and auto-fetch", async () => {
68
+ const { result } = renderHook(
69
+ () =>
70
+ useActorMethod({
71
+ reactor,
72
+ functionName: "greet",
73
+ args: ["world"],
74
+ }),
75
+ { wrapper }
76
+ )
77
+
78
+ expect(result.current.isQuery).toBe(true)
79
+ expect(result.current.functionType).toBe("query")
80
+
81
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
82
+ expect(result.current.data).toBe("Hello, world!")
83
+ expect(reactor.callMethod).toHaveBeenCalled()
84
+ })
85
+
86
+ it("should detect update method and not auto-fetch", async () => {
87
+ const { result } = renderHook(
88
+ () =>
89
+ useActorMethod({
90
+ reactor,
91
+ functionName: "transfer",
92
+ }),
93
+ { wrapper }
94
+ )
95
+
96
+ expect(result.current.isQuery).toBe(false)
97
+ expect(result.current.functionType).toBe("update")
98
+ expect(result.current.isPending).toBe(false)
99
+ expect(reactor.callMethod).not.toHaveBeenCalled()
100
+ })
101
+
102
+ it("should call update method when 'call' is invoked", async () => {
103
+ const { result } = renderHook(
104
+ () =>
105
+ useActorMethod({
106
+ reactor,
107
+ functionName: "transfer",
108
+ }),
109
+ { wrapper }
110
+ )
111
+
112
+ const transferArgs = { to: "alice", amount: 100n }
113
+ await result.current.call([transferArgs])
114
+
115
+ expect(reactor.callMethod).toHaveBeenCalled()
116
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
117
+ expect(result.current.data).toBe(true)
118
+ })
119
+
120
+ it("should call onSuccess callback", async () => {
121
+ const onSuccess = vi.fn()
122
+
123
+ renderHook(
124
+ () =>
125
+ useActorMethod({
126
+ reactor,
127
+ functionName: "greet",
128
+ args: ["admin"],
129
+ onSuccess,
130
+ }),
131
+ { wrapper }
132
+ )
133
+
134
+ await waitFor(() => expect(onSuccess).toHaveBeenCalledWith("Hello, admin!"))
135
+ })
136
+
137
+ it("should invalidate queries on successful mutation", async () => {
138
+ const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries")
139
+
140
+ const queryKey = ["test-key"]
141
+ const { result } = renderHook(
142
+ () =>
143
+ useActorMethod({
144
+ reactor,
145
+ functionName: "transfer",
146
+ invalidateQueries: [queryKey],
147
+ }),
148
+ { wrapper }
149
+ )
150
+
151
+ await result.current.call([{ to: "bob", amount: 50n }])
152
+
153
+ expect(invalidateSpy).toHaveBeenCalledWith({ queryKey })
154
+ })
155
+ })
156
+
157
+ describe("useActorMethod - Query Method Options", () => {
158
+ let queryClient: QueryClient
159
+ let clientManager: ClientManager
160
+ let reactor: Reactor<TestActor>
161
+
162
+ beforeEach(() => {
163
+ queryClient = new QueryClient({
164
+ defaultOptions: {
165
+ queries: { retry: false },
166
+ mutations: { retry: false },
167
+ },
168
+ })
169
+
170
+ clientManager = new ClientManager({
171
+ queryClient,
172
+ })
173
+
174
+ reactor = new Reactor<TestActor>({
175
+ clientManager,
176
+ canisterId: "rrkah-fqaaa-aaaaa-aaaaq-cai",
177
+ idlFactory,
178
+ name: "test",
179
+ })
180
+ })
181
+
182
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
183
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
184
+ )
185
+
186
+ it("should respect enabled=false option for query methods", async () => {
187
+ vi.spyOn(reactor, "callMethod").mockResolvedValue("Hello!")
188
+
189
+ const { result } = renderHook(
190
+ () =>
191
+ useActorMethod({
192
+ reactor,
193
+ functionName: "greet",
194
+ args: ["world"],
195
+ enabled: false,
196
+ }),
197
+ { wrapper }
198
+ )
199
+
200
+ expect(result.current.isQuery).toBe(true)
201
+ expect(result.current.isLoading).toBe(false)
202
+ expect(reactor.callMethod).not.toHaveBeenCalled()
203
+ })
204
+
205
+ it("should respect staleTime option for query methods", async () => {
206
+ vi.spyOn(reactor, "callMethod").mockResolvedValue("Hello!")
207
+
208
+ const { result } = renderHook(
209
+ () =>
210
+ useActorMethod({
211
+ reactor,
212
+ functionName: "greet",
213
+ args: ["world"],
214
+ staleTime: 60000, // 1 minute
215
+ }),
216
+ { wrapper }
217
+ )
218
+
219
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
220
+ expect(result.current.data).toBe("Hello!")
221
+ })
222
+
223
+ it("should respect refetchInterval option for query methods", async () => {
224
+ let callCount = 0
225
+ vi.spyOn(reactor, "callMethod").mockImplementation(async () => {
226
+ callCount++
227
+ return `Hello ${callCount}!`
228
+ })
229
+
230
+ const { result } = renderHook(
231
+ () =>
232
+ useActorMethod({
233
+ reactor,
234
+ functionName: "greet",
235
+ args: ["world"],
236
+ refetchInterval: 100, // 100ms
237
+ }),
238
+ { wrapper }
239
+ )
240
+
241
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
242
+ expect(callCount).toBeGreaterThanOrEqual(1)
243
+
244
+ // Wait for refetch interval to trigger
245
+ await waitFor(() => expect(callCount).toBeGreaterThan(1), { timeout: 500 })
246
+ })
247
+
248
+ it("should support refetch for query methods", async () => {
249
+ let callCount = 0
250
+ vi.spyOn(reactor, "callMethod").mockImplementation(async () => {
251
+ callCount++
252
+ return `Hello ${callCount}!`
253
+ })
254
+
255
+ const { result } = renderHook(
256
+ () =>
257
+ useActorMethod({
258
+ reactor,
259
+ functionName: "greet",
260
+ args: ["world"],
261
+ }),
262
+ { wrapper }
263
+ )
264
+
265
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
266
+ expect(result.current.data).toBe("Hello 1!")
267
+
268
+ // Call refetch
269
+ await result.current.refetch()
270
+
271
+ await waitFor(() => expect(result.current.data).toBe("Hello 2!"))
272
+ expect(callCount).toBe(2)
273
+ })
274
+
275
+ it("should support reset for query methods", async () => {
276
+ vi.spyOn(reactor, "callMethod").mockResolvedValue("Hello!")
277
+
278
+ const { result } = renderHook(
279
+ () =>
280
+ useActorMethod({
281
+ reactor,
282
+ functionName: "greet",
283
+ args: ["world"],
284
+ }),
285
+ { wrapper }
286
+ )
287
+
288
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
289
+ expect(result.current.data).toBe("Hello!")
290
+
291
+ // Reset the query
292
+ result.current.reset()
293
+
294
+ // Data should be undefined after reset
295
+ await waitFor(() => {
296
+ const queryState = queryClient.getQueryState(
297
+ reactor.generateQueryKey({ functionName: "greet", args: ["world"] })
298
+ )
299
+ expect(queryState).toBeUndefined()
300
+ })
301
+ })
302
+
303
+ it("should expose queryResult for query methods", async () => {
304
+ vi.spyOn(reactor, "callMethod").mockResolvedValue("Hello!")
305
+
306
+ const { result } = renderHook(
307
+ () =>
308
+ useActorMethod({
309
+ reactor,
310
+ functionName: "greet",
311
+ args: ["world"],
312
+ }),
313
+ { wrapper }
314
+ )
315
+
316
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
317
+
318
+ // queryResult should be available for query methods
319
+ expect(result.current.queryResult).toBeDefined()
320
+ expect(result.current.queryResult?.data).toBe("Hello!")
321
+ expect(result.current.mutationResult).toBeUndefined()
322
+ })
323
+ })
324
+
325
+ describe("useActorMethod - Mutation Method Options", () => {
326
+ let queryClient: QueryClient
327
+ let clientManager: ClientManager
328
+ let reactor: Reactor<TestActor>
329
+
330
+ beforeEach(() => {
331
+ queryClient = new QueryClient({
332
+ defaultOptions: {
333
+ queries: { retry: false },
334
+ mutations: { retry: false },
335
+ },
336
+ })
337
+
338
+ clientManager = new ClientManager({
339
+ queryClient,
340
+ })
341
+
342
+ reactor = new Reactor<TestActor>({
343
+ clientManager,
344
+ canisterId: "rrkah-fqaaa-aaaaa-aaaaq-cai",
345
+ idlFactory,
346
+ name: "test",
347
+ })
348
+ })
349
+
350
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
351
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
352
+ )
353
+
354
+ it("should support reset for mutation methods", async () => {
355
+ vi.spyOn(reactor, "callMethod").mockResolvedValue(true)
356
+
357
+ const { result } = renderHook(
358
+ () =>
359
+ useActorMethod({
360
+ reactor,
361
+ functionName: "transfer",
362
+ }),
363
+ { wrapper }
364
+ )
365
+
366
+ await result.current.call([{ to: "alice", amount: 100n }])
367
+
368
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
369
+ expect(result.current.data).toBe(true)
370
+
371
+ // Reset the mutation
372
+ result.current.reset()
373
+
374
+ await waitFor(() => expect(result.current.data).toBeUndefined())
375
+ expect(result.current.isSuccess).toBe(false)
376
+ })
377
+
378
+ it("should expose mutationResult for mutation methods", async () => {
379
+ vi.spyOn(reactor, "callMethod").mockResolvedValue(true)
380
+
381
+ const { result } = renderHook(
382
+ () =>
383
+ useActorMethod({
384
+ reactor,
385
+ functionName: "transfer",
386
+ }),
387
+ { wrapper }
388
+ )
389
+
390
+ await result.current.call([{ to: "alice", amount: 100n }])
391
+
392
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
393
+
394
+ // mutationResult should be available for mutation methods
395
+ expect(result.current.mutationResult).toBeDefined()
396
+ expect(result.current.mutationResult?.data).toBe(true)
397
+ expect(result.current.queryResult).toBeUndefined()
398
+ })
399
+
400
+ it("should call onError callback for mutations", async () => {
401
+ const error = new Error("Transfer failed")
402
+ const callError = new CallError(
403
+ `Failed to call method "transfer": ${error.message}`,
404
+ error
405
+ )
406
+ vi.spyOn(reactor, "callMethod").mockRejectedValue(callError)
407
+
408
+ const onError = vi.fn()
409
+
410
+ const { result } = renderHook(
411
+ () =>
412
+ useActorMethod({
413
+ reactor,
414
+ functionName: "transfer",
415
+ onError,
416
+ }),
417
+ { wrapper }
418
+ )
419
+
420
+ try {
421
+ await result.current.call([{ to: "alice", amount: 100n }])
422
+ } catch {
423
+ // Expected
424
+ }
425
+
426
+ await waitFor(() => expect(result.current.isError).toBe(true))
427
+ await waitFor(() => expect(result.current.error).toBe(callError))
428
+ expect(onError).toHaveBeenCalledWith(expect.any(CallError))
429
+ expect(onError).toHaveBeenCalledWith(
430
+ expect.objectContaining({
431
+ message: expect.stringContaining("Transfer failed"),
432
+ })
433
+ )
434
+ })
435
+
436
+ it("should invalidate multiple queries on successful mutation", async () => {
437
+ vi.spyOn(reactor, "callMethod").mockResolvedValue(true)
438
+ const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries")
439
+
440
+ const queryKey1 = ["balance", "alice"]
441
+ const queryKey2 = ["balance", "bob"]
442
+
443
+ const { result } = renderHook(
444
+ () =>
445
+ useActorMethod({
446
+ reactor,
447
+ functionName: "transfer",
448
+ invalidateQueries: [queryKey1, queryKey2],
449
+ }),
450
+ { wrapper }
451
+ )
452
+
453
+ await result.current.call([{ to: "bob", amount: 50n }])
454
+
455
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
456
+
457
+ expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: queryKey1 })
458
+ expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: queryKey2 })
459
+ expect(invalidateSpy).toHaveBeenCalledTimes(2)
460
+ })
461
+ })
462
+
463
+ describe("useActorMethod - Error Handling", () => {
464
+ let queryClient: QueryClient
465
+ let clientManager: ClientManager
466
+ let reactor: Reactor<TestActor>
467
+
468
+ beforeEach(() => {
469
+ queryClient = new QueryClient({
470
+ defaultOptions: {
471
+ queries: { retry: false },
472
+ mutations: { retry: false },
473
+ },
474
+ })
475
+
476
+ clientManager = new ClientManager({
477
+ queryClient,
478
+ })
479
+
480
+ reactor = new Reactor<TestActor>({
481
+ clientManager,
482
+ canisterId: "rrkah-fqaaa-aaaaa-aaaaq-cai",
483
+ idlFactory,
484
+ name: "test",
485
+ })
486
+ })
487
+
488
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
489
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
490
+ )
491
+
492
+ it("should handle query method errors", async () => {
493
+ const error = new Error("Query failed")
494
+ const callError = new CallError(
495
+ `Failed to query method "greet": ${error.message}`,
496
+ error
497
+ )
498
+ vi.spyOn(reactor, "callMethod").mockRejectedValue(callError)
499
+
500
+ const onError = vi.fn()
501
+
502
+ const { result } = renderHook(
503
+ () =>
504
+ useActorMethod({
505
+ reactor,
506
+ functionName: "greet",
507
+ args: ["world"],
508
+ onError,
509
+ }),
510
+ { wrapper }
511
+ )
512
+
513
+ await waitFor(() => expect(result.current.isError).toBe(true))
514
+ expect(result.current.error).toBeInstanceOf(CallError)
515
+ expect(result.current.error?.message).toContain("Query failed")
516
+ expect(onError).toHaveBeenCalledWith(expect.any(CallError))
517
+ expect(onError).toHaveBeenCalledWith(
518
+ expect.objectContaining({
519
+ message: expect.stringContaining("Query failed"),
520
+ })
521
+ )
522
+ })
523
+
524
+ it("should handle mutation method errors", async () => {
525
+ const error = new Error("Transfer failed")
526
+ const callError = new CallError(
527
+ `Failed to call method "transfer": ${error.message}`,
528
+ error
529
+ )
530
+ vi.spyOn(reactor, "callMethod").mockRejectedValue(callError)
531
+
532
+ const { result } = renderHook(
533
+ () =>
534
+ useActorMethod({
535
+ reactor,
536
+ functionName: "transfer",
537
+ }),
538
+ { wrapper }
539
+ )
540
+
541
+ try {
542
+ await result.current.call([{ to: "alice", amount: 100n }])
543
+ } catch {
544
+ // Expected
545
+ }
546
+
547
+ await waitFor(() => expect(result.current.isError).toBe(true))
548
+ expect(result.current.error).toBeInstanceOf(CallError)
549
+ expect(result.current.error?.message).toContain("Transfer failed")
550
+ })
551
+ })
552
+
553
+ describe("useActorMethod - Call with Different Args", () => {
554
+ let queryClient: QueryClient
555
+ let clientManager: ClientManager
556
+ let reactor: Reactor<TestActor>
557
+
558
+ beforeEach(() => {
559
+ queryClient = new QueryClient({
560
+ defaultOptions: {
561
+ queries: { retry: false },
562
+ mutations: { retry: false },
563
+ },
564
+ })
565
+
566
+ clientManager = new ClientManager({
567
+ queryClient,
568
+ })
569
+
570
+ reactor = new Reactor<TestActor>({
571
+ clientManager,
572
+ canisterId: "rrkah-fqaaa-aaaaa-aaaaq-cai",
573
+ idlFactory,
574
+ name: "test",
575
+ })
576
+
577
+ vi.spyOn(reactor, "callMethod").mockImplementation(
578
+ async ({ functionName, args }: any): Promise<any> => {
579
+ if (functionName === "greet") {
580
+ return `Hello, ${args[0]}!`
581
+ }
582
+ if (functionName === "transfer") {
583
+ return true
584
+ }
585
+ return null
586
+ }
587
+ )
588
+ })
589
+
590
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
591
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
592
+ )
593
+
594
+ it("should call query method with different args using call()", async () => {
595
+ const onSuccess = vi.fn()
596
+
597
+ const { result } = renderHook(
598
+ () =>
599
+ useActorMethod({
600
+ reactor,
601
+ functionName: "greet",
602
+ args: ["default"],
603
+ onSuccess,
604
+ }),
605
+ { wrapper }
606
+ )
607
+
608
+ // Wait for initial fetch
609
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
610
+ expect(result.current.data).toBe("Hello, default!")
611
+
612
+ // Call with different args
613
+ const newResult = await result.current.call(["override"])
614
+ expect(newResult).toBe("Hello, override!")
615
+ expect(onSuccess).toHaveBeenCalledWith("Hello, override!")
616
+ })
617
+
618
+ it("should call mutation method with args from call()", async () => {
619
+ const { result } = renderHook(
620
+ () =>
621
+ useActorMethod({
622
+ reactor,
623
+ functionName: "transfer",
624
+ }),
625
+ { wrapper }
626
+ )
627
+
628
+ const transferResult = await result.current.call([
629
+ { to: "charlie", amount: 200n },
630
+ ])
631
+
632
+ expect(transferResult).toBe(true)
633
+ expect(reactor.callMethod).toHaveBeenCalledWith(
634
+ expect.objectContaining({
635
+ functionName: "transfer",
636
+ args: [{ to: "charlie", amount: 200n }],
637
+ })
638
+ )
639
+ })
640
+ })
641
+
642
+ describe("useActorMethod - React Query Inherited Options", () => {
643
+ let queryClient: QueryClient
644
+ let clientManager: ClientManager
645
+ let reactor: Reactor<TestActor>
646
+
647
+ beforeEach(() => {
648
+ queryClient = new QueryClient({
649
+ defaultOptions: {
650
+ queries: { retry: false },
651
+ mutations: { retry: false },
652
+ },
653
+ })
654
+
655
+ clientManager = new ClientManager({
656
+ queryClient,
657
+ })
658
+
659
+ reactor = new Reactor<TestActor>({
660
+ clientManager,
661
+ canisterId: "rrkah-fqaaa-aaaaa-aaaaq-cai",
662
+ idlFactory,
663
+ name: "test",
664
+ })
665
+ })
666
+
667
+ const wrapper = ({ children }: { children: React.ReactNode }) => (
668
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
669
+ )
670
+
671
+ it("should support gcTime option (inherited from QueryObserverOptions)", async () => {
672
+ vi.spyOn(reactor, "callMethod").mockResolvedValue("Hello!")
673
+
674
+ const { result, unmount } = renderHook(
675
+ () =>
676
+ useActorMethod({
677
+ reactor,
678
+ functionName: "greet",
679
+ args: ["world"],
680
+ gcTime: 0, // Immediately garbage collect
681
+ }),
682
+ { wrapper }
683
+ )
684
+
685
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
686
+
687
+ // Unmount the hook
688
+ unmount()
689
+
690
+ // Wait a tick for garbage collection to run
691
+ await new Promise((resolve) => setTimeout(resolve, 50))
692
+
693
+ // After unmount with gcTime: 0, the query should be garbage collected
694
+ const queryCache = queryClient.getQueryCache()
695
+ const queries = queryCache.findAll({
696
+ queryKey: reactor.generateQueryKey({
697
+ functionName: "greet",
698
+ args: ["world"],
699
+ }),
700
+ })
701
+ expect(queries.length).toBe(0)
702
+ })
703
+
704
+ it("should support retry option (inherited from QueryObserverOptions)", async () => {
705
+ let attempts = 0
706
+ vi.spyOn(reactor, "callMethod").mockImplementation(async () => {
707
+ attempts++
708
+ if (attempts < 3) {
709
+ throw new Error("Temporary error")
710
+ }
711
+ return "Hello!"
712
+ })
713
+
714
+ const { result } = renderHook(
715
+ () =>
716
+ useActorMethod({
717
+ reactor,
718
+ functionName: "greet",
719
+ args: ["world"],
720
+ retry: 2, // 2 retries = 3 total attempts (initial + 2 retries)
721
+ retryDelay: 0, // No delay for faster test
722
+ }),
723
+ { wrapper }
724
+ )
725
+
726
+ await waitFor(() => expect(result.current.isSuccess).toBe(true), {
727
+ timeout: 10000,
728
+ })
729
+ expect(attempts).toBe(3)
730
+ expect(result.current.data).toBe("Hello!")
731
+ })
732
+
733
+ it("should support custom queryKey option", async () => {
734
+ vi.spyOn(reactor, "callMethod").mockResolvedValue("Hello!")
735
+
736
+ const customQueryKey = ["custom", "key", "for", "greet"]
737
+
738
+ const { result } = renderHook(
739
+ () =>
740
+ useActorMethod({
741
+ reactor,
742
+ functionName: "greet",
743
+ args: ["world"],
744
+ queryKey: customQueryKey,
745
+ }),
746
+ { wrapper }
747
+ )
748
+
749
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
750
+
751
+ // Verify the query is cached under custom key
752
+ const cachedData = queryClient.getQueryData(customQueryKey)
753
+ expect(cachedData).toBe("Hello!")
754
+ })
755
+
756
+ it("should support refetchInterval option (inherited from QueryObserverOptions)", async () => {
757
+ let callCount = 0
758
+ vi.spyOn(reactor, "callMethod").mockImplementation(async () => {
759
+ callCount++
760
+ return `Hello ${callCount}!`
761
+ })
762
+
763
+ const { result } = renderHook(
764
+ () =>
765
+ useActorMethod({
766
+ reactor,
767
+ functionName: "greet",
768
+ args: ["world"],
769
+ refetchInterval: 50, // 50ms
770
+ }),
771
+ { wrapper }
772
+ )
773
+
774
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
775
+ expect(callCount).toBeGreaterThanOrEqual(1)
776
+
777
+ // Wait for at least one refetch
778
+ await waitFor(() => expect(callCount).toBeGreaterThan(1), { timeout: 500 })
779
+ })
780
+
781
+ it("should support networkMode option (inherited from QueryObserverOptions)", async () => {
782
+ vi.spyOn(reactor, "callMethod").mockResolvedValue("Hello!")
783
+
784
+ const { result } = renderHook(
785
+ () =>
786
+ useActorMethod({
787
+ reactor,
788
+ functionName: "greet",
789
+ args: ["world"],
790
+ networkMode: "always", // Always fetch regardless of network status
791
+ }),
792
+ { wrapper }
793
+ )
794
+
795
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
796
+ expect(result.current.data).toBe("Hello!")
797
+ })
798
+ })