@ic-reactor/react 3.0.0-beta.8 → 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.
- package/README.md +11 -10
- package/dist/createActorHooks.d.ts +2 -0
- package/dist/createActorHooks.d.ts.map +1 -1
- package/dist/createActorHooks.js +2 -0
- package/dist/createActorHooks.js.map +1 -1
- package/dist/createMutation.d.ts.map +1 -1
- package/dist/createMutation.js +4 -0
- package/dist/createMutation.js.map +1 -1
- package/dist/hooks/index.d.ts +18 -5
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +15 -5
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.d.ts +13 -11
- package/dist/hooks/useActorInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorInfiniteQuery.js.map +1 -1
- package/dist/hooks/useActorMethod.d.ts +105 -0
- package/dist/hooks/useActorMethod.d.ts.map +1 -0
- package/dist/hooks/useActorMethod.js +192 -0
- package/dist/hooks/useActorMethod.js.map +1 -0
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts +13 -10
- package/dist/hooks/useActorSuspenseInfiniteQuery.d.ts.map +1 -1
- package/dist/hooks/useActorSuspenseInfiniteQuery.js.map +1 -1
- package/package.json +7 -6
- package/src/createActorHooks.ts +146 -0
- package/src/createAuthHooks.ts +137 -0
- package/src/createInfiniteQuery.ts +471 -0
- package/src/createMutation.ts +163 -0
- package/src/createQuery.ts +197 -0
- package/src/createSuspenseInfiniteQuery.ts +478 -0
- package/src/createSuspenseQuery.ts +215 -0
- package/src/hooks/index.ts +93 -0
- package/src/hooks/useActorInfiniteQuery.test.tsx +457 -0
- package/src/hooks/useActorInfiniteQuery.ts +134 -0
- package/src/hooks/useActorMethod.test.tsx +798 -0
- package/src/hooks/useActorMethod.ts +397 -0
- package/src/hooks/useActorMutation.test.tsx +220 -0
- package/src/hooks/useActorMutation.ts +124 -0
- package/src/hooks/useActorQuery.test.tsx +287 -0
- package/src/hooks/useActorQuery.ts +110 -0
- package/src/hooks/useActorSuspenseInfiniteQuery.test.tsx +472 -0
- package/src/hooks/useActorSuspenseInfiniteQuery.ts +137 -0
- package/src/hooks/useActorSuspenseQuery.test.tsx +254 -0
- package/src/hooks/useActorSuspenseQuery.ts +112 -0
- package/src/index.ts +21 -0
- package/src/types.ts +435 -0
- 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
|
+
})
|