@pyreon/query 0.0.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/LICENSE +21 -0
- package/README.md +236 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/index.js +489 -0
- package/lib/index.js.map +1 -0
- package/lib/types/index.d.ts +497 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index2.d.ts +298 -0
- package/lib/types/index2.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/index.ts +69 -0
- package/src/query-client.ts +59 -0
- package/src/tests/query.test.ts +1768 -0
- package/src/use-infinite-query.ts +138 -0
- package/src/use-is-fetching.ts +44 -0
- package/src/use-mutation.ts +117 -0
- package/src/use-queries.ts +61 -0
- package/src/use-query-error-reset-boundary.ts +95 -0
- package/src/use-query.ts +106 -0
- package/src/use-suspense-query.ts +282 -0
|
@@ -0,0 +1,1768 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/query-core'
|
|
2
|
+
import { h } from '@pyreon/core'
|
|
3
|
+
import { signal } from '@pyreon/reactivity'
|
|
4
|
+
import { mount } from '@pyreon/runtime-dom'
|
|
5
|
+
import {
|
|
6
|
+
QueryClientProvider,
|
|
7
|
+
useQueryClient,
|
|
8
|
+
useQuery,
|
|
9
|
+
useMutation,
|
|
10
|
+
useIsFetching,
|
|
11
|
+
useIsMutating,
|
|
12
|
+
useQueries,
|
|
13
|
+
useSuspenseQuery,
|
|
14
|
+
useSuspenseInfiniteQuery,
|
|
15
|
+
useInfiniteQuery,
|
|
16
|
+
QuerySuspense,
|
|
17
|
+
QueryErrorResetBoundary,
|
|
18
|
+
useQueryErrorResetBoundary,
|
|
19
|
+
dehydrate,
|
|
20
|
+
hydrate,
|
|
21
|
+
} from '../index'
|
|
22
|
+
|
|
23
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function makeClient() {
|
|
26
|
+
return new QueryClient({
|
|
27
|
+
defaultOptions: { queries: { retry: false, gcTime: Infinity } },
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Mount a component inside a QueryClientProvider, return unmount fn. */
|
|
32
|
+
function _withProvider(client: QueryClient, component: () => void): () => void {
|
|
33
|
+
const el = document.createElement('div')
|
|
34
|
+
document.body.appendChild(el)
|
|
35
|
+
const unmount = mount(
|
|
36
|
+
h(QueryClientProvider, { client }, () => {
|
|
37
|
+
component()
|
|
38
|
+
return null
|
|
39
|
+
}),
|
|
40
|
+
el,
|
|
41
|
+
)
|
|
42
|
+
return () => {
|
|
43
|
+
unmount()
|
|
44
|
+
el.remove()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Returns a promise + its resolve/reject handles. */
|
|
49
|
+
function deferred<T>() {
|
|
50
|
+
let resolve!: (v: T) => void
|
|
51
|
+
let reject!: (e: unknown) => void
|
|
52
|
+
const promise = new Promise<T>((res, rej) => {
|
|
53
|
+
resolve = res
|
|
54
|
+
reject = rej
|
|
55
|
+
})
|
|
56
|
+
return { promise, resolve, reject }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── QueryClientProvider / useQueryClient ────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
describe('QueryClientProvider / useQueryClient', () => {
|
|
62
|
+
it('useQueryClient throws when no provider is present', () => {
|
|
63
|
+
// Call directly outside any renderer — context stack is empty so it must throw.
|
|
64
|
+
expect(() => useQueryClient()).toThrow('[pyreon/query]')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('provides the QueryClient to descendants', () => {
|
|
68
|
+
const client = makeClient()
|
|
69
|
+
let received: QueryClient | null = null
|
|
70
|
+
const el = document.createElement('div')
|
|
71
|
+
document.body.appendChild(el)
|
|
72
|
+
const unmount = mount(
|
|
73
|
+
h(
|
|
74
|
+
QueryClientProvider,
|
|
75
|
+
{ client },
|
|
76
|
+
h(() => {
|
|
77
|
+
received = useQueryClient()
|
|
78
|
+
return null
|
|
79
|
+
}, null),
|
|
80
|
+
),
|
|
81
|
+
el,
|
|
82
|
+
)
|
|
83
|
+
unmount()
|
|
84
|
+
el.remove()
|
|
85
|
+
expect(received).toBe(client)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('inner provider overrides outer', () => {
|
|
89
|
+
const outer = makeClient()
|
|
90
|
+
const inner = makeClient()
|
|
91
|
+
let received: QueryClient | null = null
|
|
92
|
+
const el = document.createElement('div')
|
|
93
|
+
document.body.appendChild(el)
|
|
94
|
+
const unmount = mount(
|
|
95
|
+
h(
|
|
96
|
+
QueryClientProvider,
|
|
97
|
+
{ client: outer },
|
|
98
|
+
h(
|
|
99
|
+
QueryClientProvider,
|
|
100
|
+
{ client: inner },
|
|
101
|
+
h(() => {
|
|
102
|
+
received = useQueryClient()
|
|
103
|
+
return null
|
|
104
|
+
}, null),
|
|
105
|
+
),
|
|
106
|
+
),
|
|
107
|
+
el,
|
|
108
|
+
)
|
|
109
|
+
unmount()
|
|
110
|
+
el.remove()
|
|
111
|
+
expect(received).toBe(inner)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// ─── useQuery ─────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
describe('useQuery', () => {
|
|
118
|
+
let client: QueryClient
|
|
119
|
+
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
client = makeClient()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('starts in pending state when cache is empty', () => {
|
|
125
|
+
let query: ReturnType<typeof useQuery> | undefined
|
|
126
|
+
const el = document.createElement('div')
|
|
127
|
+
document.body.appendChild(el)
|
|
128
|
+
const unmount = mount(
|
|
129
|
+
h(
|
|
130
|
+
QueryClientProvider,
|
|
131
|
+
{ client },
|
|
132
|
+
h(() => {
|
|
133
|
+
query = useQuery(() => ({
|
|
134
|
+
queryKey: ['test-pending'],
|
|
135
|
+
queryFn: () =>
|
|
136
|
+
new Promise(() => {
|
|
137
|
+
/* never resolves */
|
|
138
|
+
}), // never resolves
|
|
139
|
+
}))
|
|
140
|
+
return null
|
|
141
|
+
}, null),
|
|
142
|
+
),
|
|
143
|
+
el,
|
|
144
|
+
)
|
|
145
|
+
expect(query!.isPending()).toBe(true)
|
|
146
|
+
expect(query!.data()).toBeUndefined()
|
|
147
|
+
unmount()
|
|
148
|
+
el.remove()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('resolves to success state with data', async () => {
|
|
152
|
+
const { promise, resolve } = deferred<{ name: string }>()
|
|
153
|
+
let query: ReturnType<typeof useQuery<{ name: string }>> | undefined
|
|
154
|
+
|
|
155
|
+
const el = document.createElement('div')
|
|
156
|
+
document.body.appendChild(el)
|
|
157
|
+
const unmount = mount(
|
|
158
|
+
h(
|
|
159
|
+
QueryClientProvider,
|
|
160
|
+
{ client },
|
|
161
|
+
h(() => {
|
|
162
|
+
query = useQuery(() => ({
|
|
163
|
+
queryKey: ['test-success'],
|
|
164
|
+
queryFn: () => promise,
|
|
165
|
+
}))
|
|
166
|
+
return null
|
|
167
|
+
}, null),
|
|
168
|
+
),
|
|
169
|
+
el,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
resolve({ name: 'Pyreon' })
|
|
173
|
+
await promise
|
|
174
|
+
|
|
175
|
+
// Let the observer's internal promise chain flush
|
|
176
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
177
|
+
|
|
178
|
+
expect(query!.isSuccess()).toBe(true)
|
|
179
|
+
expect(query!.data()).toEqual({ name: 'Pyreon' })
|
|
180
|
+
unmount()
|
|
181
|
+
el.remove()
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('captures errors in isError state', async () => {
|
|
185
|
+
const { promise, reject } = deferred<never>()
|
|
186
|
+
let query: ReturnType<typeof useQuery> | undefined
|
|
187
|
+
|
|
188
|
+
const el = document.createElement('div')
|
|
189
|
+
document.body.appendChild(el)
|
|
190
|
+
const unmount = mount(
|
|
191
|
+
h(
|
|
192
|
+
QueryClientProvider,
|
|
193
|
+
{ client },
|
|
194
|
+
h(() => {
|
|
195
|
+
query = useQuery(() => ({
|
|
196
|
+
queryKey: ['test-error'],
|
|
197
|
+
queryFn: () => promise,
|
|
198
|
+
}))
|
|
199
|
+
return null
|
|
200
|
+
}, null),
|
|
201
|
+
),
|
|
202
|
+
el,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
reject(new Error('fetch failed'))
|
|
206
|
+
await promise.catch(() => {
|
|
207
|
+
/* expected */
|
|
208
|
+
})
|
|
209
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
210
|
+
|
|
211
|
+
expect(query!.isError()).toBe(true)
|
|
212
|
+
expect((query!.error() as Error).message).toBe('fetch failed')
|
|
213
|
+
unmount()
|
|
214
|
+
el.remove()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('respects enabled: false — does not fetch', async () => {
|
|
218
|
+
const queryFn = vi.fn(() => Promise.resolve('should not run'))
|
|
219
|
+
let query: ReturnType<typeof useQuery<string>> | undefined
|
|
220
|
+
|
|
221
|
+
const el = document.createElement('div')
|
|
222
|
+
document.body.appendChild(el)
|
|
223
|
+
const unmount = mount(
|
|
224
|
+
h(
|
|
225
|
+
QueryClientProvider,
|
|
226
|
+
{ client },
|
|
227
|
+
h(() => {
|
|
228
|
+
query = useQuery(() => ({
|
|
229
|
+
queryKey: ['test-disabled'],
|
|
230
|
+
queryFn,
|
|
231
|
+
enabled: false,
|
|
232
|
+
}))
|
|
233
|
+
return null
|
|
234
|
+
}, null),
|
|
235
|
+
),
|
|
236
|
+
el,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
240
|
+
expect(queryFn).not.toHaveBeenCalled()
|
|
241
|
+
expect(query!.isPending()).toBe(true)
|
|
242
|
+
expect(query!.isFetching()).toBe(false)
|
|
243
|
+
unmount()
|
|
244
|
+
el.remove()
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('reactive query key — refetches when signal changes', async () => {
|
|
248
|
+
const calls: number[] = []
|
|
249
|
+
const userId = signal(1)
|
|
250
|
+
let query: ReturnType<typeof useQuery<string>> | undefined
|
|
251
|
+
|
|
252
|
+
const el = document.createElement('div')
|
|
253
|
+
document.body.appendChild(el)
|
|
254
|
+
const unmount = mount(
|
|
255
|
+
h(
|
|
256
|
+
QueryClientProvider,
|
|
257
|
+
{ client },
|
|
258
|
+
h(() => {
|
|
259
|
+
query = useQuery(() => ({
|
|
260
|
+
queryKey: ['user', userId()],
|
|
261
|
+
queryFn: async () => {
|
|
262
|
+
const id = userId()
|
|
263
|
+
calls.push(id)
|
|
264
|
+
return `user-${id}`
|
|
265
|
+
},
|
|
266
|
+
}))
|
|
267
|
+
return null
|
|
268
|
+
}, null),
|
|
269
|
+
),
|
|
270
|
+
el,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
274
|
+
expect(calls).toContain(1)
|
|
275
|
+
|
|
276
|
+
userId.set(2)
|
|
277
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
278
|
+
expect(calls).toContain(2)
|
|
279
|
+
|
|
280
|
+
expect(query!.data()).toBe('user-2')
|
|
281
|
+
unmount()
|
|
282
|
+
el.remove()
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('invalidateQueries triggers a refetch', async () => {
|
|
286
|
+
let callCount = 0
|
|
287
|
+
const el = document.createElement('div')
|
|
288
|
+
document.body.appendChild(el)
|
|
289
|
+
const unmount = mount(
|
|
290
|
+
h(
|
|
291
|
+
QueryClientProvider,
|
|
292
|
+
{ client },
|
|
293
|
+
h(() => {
|
|
294
|
+
useQuery(() => ({
|
|
295
|
+
queryKey: ['invalidate-test'],
|
|
296
|
+
queryFn: async () => {
|
|
297
|
+
callCount++
|
|
298
|
+
return callCount
|
|
299
|
+
},
|
|
300
|
+
}))
|
|
301
|
+
return null
|
|
302
|
+
}, null),
|
|
303
|
+
),
|
|
304
|
+
el,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
308
|
+
const before = callCount
|
|
309
|
+
await client.invalidateQueries({ queryKey: ['invalidate-test'] })
|
|
310
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
311
|
+
expect(callCount).toBeGreaterThan(before)
|
|
312
|
+
unmount()
|
|
313
|
+
el.remove()
|
|
314
|
+
})
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
// ─── useMutation ──────────────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
describe('useMutation', () => {
|
|
320
|
+
let client: QueryClient
|
|
321
|
+
beforeEach(() => {
|
|
322
|
+
client = makeClient()
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('starts in idle state', () => {
|
|
326
|
+
let mut: ReturnType<typeof useMutation> | undefined
|
|
327
|
+
const el = document.createElement('div')
|
|
328
|
+
document.body.appendChild(el)
|
|
329
|
+
const unmount = mount(
|
|
330
|
+
h(
|
|
331
|
+
QueryClientProvider,
|
|
332
|
+
{ client },
|
|
333
|
+
h(() => {
|
|
334
|
+
mut = useMutation({ mutationFn: () => Promise.resolve('ok') })
|
|
335
|
+
return null
|
|
336
|
+
}, null),
|
|
337
|
+
),
|
|
338
|
+
el,
|
|
339
|
+
)
|
|
340
|
+
expect(mut!.isIdle()).toBe(true)
|
|
341
|
+
expect(mut!.isPending()).toBe(false)
|
|
342
|
+
unmount()
|
|
343
|
+
el.remove()
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
it('goes pending then success', async () => {
|
|
347
|
+
let mut: ReturnType<typeof useMutation<string, Error, string>> | undefined
|
|
348
|
+
const el = document.createElement('div')
|
|
349
|
+
document.body.appendChild(el)
|
|
350
|
+
const unmount = mount(
|
|
351
|
+
h(
|
|
352
|
+
QueryClientProvider,
|
|
353
|
+
{ client },
|
|
354
|
+
h(() => {
|
|
355
|
+
mut = useMutation<string, Error, string>({
|
|
356
|
+
mutationFn: async (input: string) => `result:${input}`,
|
|
357
|
+
})
|
|
358
|
+
return null
|
|
359
|
+
}, null),
|
|
360
|
+
),
|
|
361
|
+
el,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
mut!.mutate('hello')
|
|
365
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
366
|
+
|
|
367
|
+
expect(mut!.isSuccess()).toBe(true)
|
|
368
|
+
expect(mut!.data()).toBe('result:hello')
|
|
369
|
+
unmount()
|
|
370
|
+
el.remove()
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('captures mutation error', async () => {
|
|
374
|
+
let mut: ReturnType<typeof useMutation> | undefined
|
|
375
|
+
const el = document.createElement('div')
|
|
376
|
+
document.body.appendChild(el)
|
|
377
|
+
const unmount = mount(
|
|
378
|
+
h(
|
|
379
|
+
QueryClientProvider,
|
|
380
|
+
{ client },
|
|
381
|
+
h(() => {
|
|
382
|
+
mut = useMutation({
|
|
383
|
+
mutationFn: async () => {
|
|
384
|
+
throw new Error('mutation failed')
|
|
385
|
+
},
|
|
386
|
+
})
|
|
387
|
+
return null
|
|
388
|
+
}, null),
|
|
389
|
+
),
|
|
390
|
+
el,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
mut!.mutate(undefined)
|
|
394
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
395
|
+
|
|
396
|
+
expect(mut!.isError()).toBe(true)
|
|
397
|
+
expect((mut!.error() as Error).message).toBe('mutation failed')
|
|
398
|
+
unmount()
|
|
399
|
+
el.remove()
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('reset() clears mutation state', async () => {
|
|
403
|
+
let mut: ReturnType<typeof useMutation<string, Error, void>> | undefined
|
|
404
|
+
const el = document.createElement('div')
|
|
405
|
+
document.body.appendChild(el)
|
|
406
|
+
const unmount = mount(
|
|
407
|
+
h(
|
|
408
|
+
QueryClientProvider,
|
|
409
|
+
{ client },
|
|
410
|
+
h(() => {
|
|
411
|
+
mut = useMutation<string, Error, void>({
|
|
412
|
+
mutationFn: async () => 'done',
|
|
413
|
+
})
|
|
414
|
+
return null
|
|
415
|
+
}, null),
|
|
416
|
+
),
|
|
417
|
+
el,
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
mut!.mutate(undefined)
|
|
421
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
422
|
+
expect(mut!.isSuccess()).toBe(true)
|
|
423
|
+
|
|
424
|
+
mut!.reset()
|
|
425
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
426
|
+
expect(mut!.isIdle()).toBe(true)
|
|
427
|
+
expect(mut!.data()).toBeUndefined()
|
|
428
|
+
unmount()
|
|
429
|
+
el.remove()
|
|
430
|
+
})
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
// ─── useIsFetching / useIsMutating ────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
describe('useIsFetching', () => {
|
|
436
|
+
it('returns 0 when no queries are in-flight', () => {
|
|
437
|
+
const client = makeClient()
|
|
438
|
+
let count: (() => number) | undefined
|
|
439
|
+
const el = document.createElement('div')
|
|
440
|
+
document.body.appendChild(el)
|
|
441
|
+
const unmount = mount(
|
|
442
|
+
h(
|
|
443
|
+
QueryClientProvider,
|
|
444
|
+
{ client },
|
|
445
|
+
h(() => {
|
|
446
|
+
count = useIsFetching()
|
|
447
|
+
return null
|
|
448
|
+
}, null),
|
|
449
|
+
),
|
|
450
|
+
el,
|
|
451
|
+
)
|
|
452
|
+
expect(count!()).toBe(0)
|
|
453
|
+
unmount()
|
|
454
|
+
el.remove()
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
it('increments while a query is fetching', async () => {
|
|
458
|
+
const client = makeClient()
|
|
459
|
+
const { promise, resolve } = deferred<string>()
|
|
460
|
+
const counts: number[] = []
|
|
461
|
+
let isFetching: (() => number) | undefined
|
|
462
|
+
|
|
463
|
+
const el = document.createElement('div')
|
|
464
|
+
document.body.appendChild(el)
|
|
465
|
+
const unmount = mount(
|
|
466
|
+
h(
|
|
467
|
+
QueryClientProvider,
|
|
468
|
+
{ client },
|
|
469
|
+
h(() => {
|
|
470
|
+
isFetching = useIsFetching()
|
|
471
|
+
useQuery(() => ({
|
|
472
|
+
queryKey: ['fetch-count'],
|
|
473
|
+
queryFn: () => promise,
|
|
474
|
+
}))
|
|
475
|
+
return null
|
|
476
|
+
}, null),
|
|
477
|
+
),
|
|
478
|
+
el,
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
// At this point the query has started fetching
|
|
482
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
483
|
+
counts.push(isFetching!())
|
|
484
|
+
|
|
485
|
+
resolve('done')
|
|
486
|
+
await promise
|
|
487
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
488
|
+
counts.push(isFetching!())
|
|
489
|
+
|
|
490
|
+
expect(counts[0]).toBeGreaterThan(0)
|
|
491
|
+
expect(counts[counts.length - 1]).toBe(0)
|
|
492
|
+
unmount()
|
|
493
|
+
el.remove()
|
|
494
|
+
})
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
describe('useIsMutating', () => {
|
|
498
|
+
it('returns 0 when idle, >0 while mutating', async () => {
|
|
499
|
+
const client = makeClient()
|
|
500
|
+
const { promise, resolve } = deferred<void>()
|
|
501
|
+
let isMutating: (() => number) | undefined
|
|
502
|
+
let mut: ReturnType<typeof useMutation<void, Error, void>> | undefined
|
|
503
|
+
|
|
504
|
+
const el = document.createElement('div')
|
|
505
|
+
document.body.appendChild(el)
|
|
506
|
+
const unmount = mount(
|
|
507
|
+
h(
|
|
508
|
+
QueryClientProvider,
|
|
509
|
+
{ client },
|
|
510
|
+
h(() => {
|
|
511
|
+
isMutating = useIsMutating()
|
|
512
|
+
mut = useMutation<void, Error, void>({ mutationFn: () => promise })
|
|
513
|
+
return null
|
|
514
|
+
}, null),
|
|
515
|
+
),
|
|
516
|
+
el,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
expect(isMutating!()).toBe(0)
|
|
520
|
+
mut!.mutate(undefined)
|
|
521
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
522
|
+
expect(isMutating!()).toBeGreaterThan(0)
|
|
523
|
+
resolve()
|
|
524
|
+
await promise
|
|
525
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
526
|
+
expect(isMutating!()).toBe(0)
|
|
527
|
+
unmount()
|
|
528
|
+
el.remove()
|
|
529
|
+
})
|
|
530
|
+
})
|
|
531
|
+
|
|
532
|
+
// ─── SSR: dehydrate / hydrate ─────────────────────────────────────────────────
|
|
533
|
+
|
|
534
|
+
describe('dehydrate / hydrate', () => {
|
|
535
|
+
it('round-trips query data — prefetched data available without refetching', async () => {
|
|
536
|
+
// Server: prefetch + serialize
|
|
537
|
+
const serverClient = makeClient()
|
|
538
|
+
await serverClient.prefetchQuery({
|
|
539
|
+
queryKey: ['ssr-user'],
|
|
540
|
+
queryFn: async () => ({ name: 'SSR User' }),
|
|
541
|
+
})
|
|
542
|
+
const state = dehydrate(serverClient)
|
|
543
|
+
|
|
544
|
+
// Client: rehydrate
|
|
545
|
+
const clientClient = makeClient()
|
|
546
|
+
hydrate(clientClient, state)
|
|
547
|
+
|
|
548
|
+
let query: ReturnType<typeof useQuery<{ name: string }>> | undefined
|
|
549
|
+
let callCount = 0
|
|
550
|
+
|
|
551
|
+
const el = document.createElement('div')
|
|
552
|
+
document.body.appendChild(el)
|
|
553
|
+
const unmount = mount(
|
|
554
|
+
h(
|
|
555
|
+
QueryClientProvider,
|
|
556
|
+
{ client: clientClient },
|
|
557
|
+
h(() => {
|
|
558
|
+
query = useQuery(() => ({
|
|
559
|
+
queryKey: ['ssr-user'],
|
|
560
|
+
queryFn: async () => {
|
|
561
|
+
callCount++
|
|
562
|
+
return { name: 'fresh' }
|
|
563
|
+
},
|
|
564
|
+
staleTime: Infinity, // treat hydrated data as fresh
|
|
565
|
+
}))
|
|
566
|
+
return null
|
|
567
|
+
}, null),
|
|
568
|
+
),
|
|
569
|
+
el,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
// Data should be immediately available from the hydrated cache
|
|
573
|
+
expect(query!.isSuccess()).toBe(true)
|
|
574
|
+
expect(query!.data()).toEqual({ name: 'SSR User' })
|
|
575
|
+
// queryFn should NOT have been called (data was in cache)
|
|
576
|
+
expect(callCount).toBe(0)
|
|
577
|
+
unmount()
|
|
578
|
+
el.remove()
|
|
579
|
+
})
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
// ─── useQueries ───────────────────────────────────────────────────────────────
|
|
583
|
+
|
|
584
|
+
describe('useQueries', () => {
|
|
585
|
+
it('returns results for all queries in the array', async () => {
|
|
586
|
+
const client = makeClient()
|
|
587
|
+
let results: ReturnType<typeof useQueries> | undefined
|
|
588
|
+
|
|
589
|
+
const el = document.createElement('div')
|
|
590
|
+
document.body.appendChild(el)
|
|
591
|
+
const unmount = mount(
|
|
592
|
+
h(
|
|
593
|
+
QueryClientProvider,
|
|
594
|
+
{ client },
|
|
595
|
+
h(() => {
|
|
596
|
+
results = useQueries(() => [
|
|
597
|
+
{ queryKey: ['a'], queryFn: async () => 'alpha' },
|
|
598
|
+
{ queryKey: ['b'], queryFn: async () => 'beta' },
|
|
599
|
+
])
|
|
600
|
+
return null
|
|
601
|
+
}, null),
|
|
602
|
+
),
|
|
603
|
+
el,
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
607
|
+
const res = results?.() ?? []
|
|
608
|
+
expect(res).toHaveLength(2)
|
|
609
|
+
expect(res[0]!.data).toBe('alpha')
|
|
610
|
+
expect(res[1]!.data).toBe('beta')
|
|
611
|
+
unmount()
|
|
612
|
+
el.remove()
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
it('reactive — updates when the queries signal changes', async () => {
|
|
616
|
+
const client = makeClient()
|
|
617
|
+
const { signal: sig } = await import('@pyreon/reactivity')
|
|
618
|
+
const ids = sig([1])
|
|
619
|
+
let results: ReturnType<typeof useQueries> | undefined
|
|
620
|
+
|
|
621
|
+
const el = document.createElement('div')
|
|
622
|
+
document.body.appendChild(el)
|
|
623
|
+
const unmount = mount(
|
|
624
|
+
h(
|
|
625
|
+
QueryClientProvider,
|
|
626
|
+
{ client },
|
|
627
|
+
h(() => {
|
|
628
|
+
results = useQueries(() =>
|
|
629
|
+
ids().map((id) => ({
|
|
630
|
+
queryKey: ['item', id],
|
|
631
|
+
queryFn: async () => `item-${id}`,
|
|
632
|
+
})),
|
|
633
|
+
)
|
|
634
|
+
return null
|
|
635
|
+
}, null),
|
|
636
|
+
),
|
|
637
|
+
el,
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
641
|
+
expect(results?.()).toHaveLength(1)
|
|
642
|
+
|
|
643
|
+
ids.set([1, 2])
|
|
644
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
645
|
+
expect(results?.()).toHaveLength(2)
|
|
646
|
+
expect(results!()[1]!.data).toBe('item-2')
|
|
647
|
+
unmount()
|
|
648
|
+
el.remove()
|
|
649
|
+
})
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
// ─── useSuspenseQuery / QuerySuspense ─────────────────────────────────────────
|
|
653
|
+
|
|
654
|
+
describe('useSuspenseQuery + QuerySuspense', () => {
|
|
655
|
+
let client: QueryClient
|
|
656
|
+
beforeEach(() => {
|
|
657
|
+
client = makeClient()
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
it('QuerySuspense shows fallback while pending, then children on success', async () => {
|
|
661
|
+
const { promise, resolve } = deferred<string>()
|
|
662
|
+
const rendered: string[] = []
|
|
663
|
+
|
|
664
|
+
const el = document.createElement('div')
|
|
665
|
+
document.body.appendChild(el)
|
|
666
|
+
const unmount = mount(
|
|
667
|
+
h(
|
|
668
|
+
QueryClientProvider,
|
|
669
|
+
{ client },
|
|
670
|
+
h(() => {
|
|
671
|
+
const q = useSuspenseQuery(() => ({
|
|
672
|
+
queryKey: ['sq-pending'],
|
|
673
|
+
queryFn: () => promise,
|
|
674
|
+
}))
|
|
675
|
+
return h(
|
|
676
|
+
QuerySuspense as any,
|
|
677
|
+
{ query: q, fallback: 'loading' },
|
|
678
|
+
() => {
|
|
679
|
+
rendered.push(q.data())
|
|
680
|
+
return null
|
|
681
|
+
},
|
|
682
|
+
)
|
|
683
|
+
}, null),
|
|
684
|
+
),
|
|
685
|
+
el,
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
// While pending — fallback rendered, children not called
|
|
689
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
690
|
+
expect(rendered).toHaveLength(0)
|
|
691
|
+
|
|
692
|
+
resolve('done')
|
|
693
|
+
await promise
|
|
694
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
695
|
+
expect(rendered.at(-1)).toBe('done')
|
|
696
|
+
unmount()
|
|
697
|
+
el.remove()
|
|
698
|
+
})
|
|
699
|
+
|
|
700
|
+
it('QuerySuspense shows error fallback on query failure', async () => {
|
|
701
|
+
const { promise, reject } = deferred<never>()
|
|
702
|
+
let errorMsg: string | undefined
|
|
703
|
+
|
|
704
|
+
const el = document.createElement('div')
|
|
705
|
+
document.body.appendChild(el)
|
|
706
|
+
const unmount = mount(
|
|
707
|
+
h(
|
|
708
|
+
QueryClientProvider,
|
|
709
|
+
{ client },
|
|
710
|
+
h(() => {
|
|
711
|
+
const q = useSuspenseQuery(() => ({
|
|
712
|
+
queryKey: ['sq-error'],
|
|
713
|
+
queryFn: () => promise,
|
|
714
|
+
}))
|
|
715
|
+
return h(
|
|
716
|
+
QuerySuspense as any,
|
|
717
|
+
{
|
|
718
|
+
query: q,
|
|
719
|
+
fallback: 'loading',
|
|
720
|
+
error: (err: unknown) => {
|
|
721
|
+
errorMsg = (err as Error).message
|
|
722
|
+
return null
|
|
723
|
+
},
|
|
724
|
+
},
|
|
725
|
+
() => null,
|
|
726
|
+
)
|
|
727
|
+
}, null),
|
|
728
|
+
),
|
|
729
|
+
el,
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
reject(new Error('sq failed'))
|
|
733
|
+
await promise.catch(() => {
|
|
734
|
+
/* expected */
|
|
735
|
+
})
|
|
736
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
737
|
+
expect(errorMsg).toBe('sq failed')
|
|
738
|
+
unmount()
|
|
739
|
+
el.remove()
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
it('multiple queries — children only render when all succeed', async () => {
|
|
743
|
+
const d1 = deferred<string>()
|
|
744
|
+
const d2 = deferred<string>()
|
|
745
|
+
let childrenRendered = false
|
|
746
|
+
|
|
747
|
+
const el = document.createElement('div')
|
|
748
|
+
document.body.appendChild(el)
|
|
749
|
+
const unmount = mount(
|
|
750
|
+
h(
|
|
751
|
+
QueryClientProvider,
|
|
752
|
+
{ client },
|
|
753
|
+
h(() => {
|
|
754
|
+
const q1 = useSuspenseQuery(() => ({
|
|
755
|
+
queryKey: ['mq1'],
|
|
756
|
+
queryFn: () => d1.promise,
|
|
757
|
+
}))
|
|
758
|
+
const q2 = useSuspenseQuery(() => ({
|
|
759
|
+
queryKey: ['mq2'],
|
|
760
|
+
queryFn: () => d2.promise,
|
|
761
|
+
}))
|
|
762
|
+
return h(
|
|
763
|
+
QuerySuspense as any,
|
|
764
|
+
{ query: [q1, q2], fallback: 'loading' },
|
|
765
|
+
() => {
|
|
766
|
+
childrenRendered = true
|
|
767
|
+
return null
|
|
768
|
+
},
|
|
769
|
+
)
|
|
770
|
+
}, null),
|
|
771
|
+
),
|
|
772
|
+
el,
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
d1.resolve('first')
|
|
776
|
+
await d1.promise
|
|
777
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
778
|
+
// q2 still pending — children should not render
|
|
779
|
+
expect(childrenRendered).toBe(false)
|
|
780
|
+
|
|
781
|
+
d2.resolve('second')
|
|
782
|
+
await d2.promise
|
|
783
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
784
|
+
expect(childrenRendered).toBe(true)
|
|
785
|
+
unmount()
|
|
786
|
+
el.remove()
|
|
787
|
+
})
|
|
788
|
+
})
|
|
789
|
+
|
|
790
|
+
// ─── QueryErrorResetBoundary / useQueryErrorResetBoundary ─────────────────────
|
|
791
|
+
|
|
792
|
+
describe('QueryErrorResetBoundary', () => {
|
|
793
|
+
it('reset() re-triggers fetch for errored queries', async () => {
|
|
794
|
+
const client = makeClient()
|
|
795
|
+
let callCount = 0
|
|
796
|
+
let shouldFail = true
|
|
797
|
+
let resetFn: (() => void) | undefined
|
|
798
|
+
|
|
799
|
+
const el = document.createElement('div')
|
|
800
|
+
document.body.appendChild(el)
|
|
801
|
+
const unmount = mount(
|
|
802
|
+
h(
|
|
803
|
+
QueryClientProvider,
|
|
804
|
+
{ client },
|
|
805
|
+
h(
|
|
806
|
+
QueryErrorResetBoundary,
|
|
807
|
+
null,
|
|
808
|
+
h(() => {
|
|
809
|
+
const { reset } = useQueryErrorResetBoundary()
|
|
810
|
+
resetFn = reset
|
|
811
|
+
useQuery(() => ({
|
|
812
|
+
queryKey: ['reset-test'],
|
|
813
|
+
queryFn: async () => {
|
|
814
|
+
callCount++
|
|
815
|
+
if (shouldFail) throw new Error('fail')
|
|
816
|
+
return 'ok'
|
|
817
|
+
},
|
|
818
|
+
}))
|
|
819
|
+
return null
|
|
820
|
+
}, null),
|
|
821
|
+
),
|
|
822
|
+
),
|
|
823
|
+
el,
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
827
|
+
const afterFirst = callCount
|
|
828
|
+
|
|
829
|
+
shouldFail = false
|
|
830
|
+
resetFn?.()
|
|
831
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
832
|
+
expect(callCount).toBeGreaterThan(afterFirst)
|
|
833
|
+
unmount()
|
|
834
|
+
el.remove()
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
it('useQueryErrorResetBoundary works without explicit boundary', () => {
|
|
838
|
+
const client = makeClient()
|
|
839
|
+
let reset: (() => void) | undefined
|
|
840
|
+
|
|
841
|
+
const el = document.createElement('div')
|
|
842
|
+
document.body.appendChild(el)
|
|
843
|
+
const unmount = mount(
|
|
844
|
+
h(
|
|
845
|
+
QueryClientProvider,
|
|
846
|
+
{ client },
|
|
847
|
+
h(() => {
|
|
848
|
+
const boundary = useQueryErrorResetBoundary()
|
|
849
|
+
reset = boundary.reset
|
|
850
|
+
return null
|
|
851
|
+
}, null),
|
|
852
|
+
),
|
|
853
|
+
el,
|
|
854
|
+
)
|
|
855
|
+
|
|
856
|
+
// Should not throw — falls back to client-level reset
|
|
857
|
+
expect(() => reset?.()).not.toThrow()
|
|
858
|
+
unmount()
|
|
859
|
+
el.remove()
|
|
860
|
+
})
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
// ─── useInfiniteQuery ─────────────────────────────────────────────────────────
|
|
864
|
+
|
|
865
|
+
describe('useInfiniteQuery', () => {
|
|
866
|
+
let client: QueryClient
|
|
867
|
+
beforeEach(() => {
|
|
868
|
+
client = makeClient()
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
it('starts in pending state', () => {
|
|
872
|
+
let query: ReturnType<typeof useInfiniteQuery> | undefined
|
|
873
|
+
const el = document.createElement('div')
|
|
874
|
+
document.body.appendChild(el)
|
|
875
|
+
const unmount = mount(
|
|
876
|
+
h(
|
|
877
|
+
QueryClientProvider,
|
|
878
|
+
{ client },
|
|
879
|
+
h(() => {
|
|
880
|
+
query = useInfiniteQuery(() => ({
|
|
881
|
+
queryKey: ['inf-pending'],
|
|
882
|
+
queryFn: () =>
|
|
883
|
+
new Promise(() => {
|
|
884
|
+
/* never resolves */
|
|
885
|
+
}),
|
|
886
|
+
initialPageParam: 0,
|
|
887
|
+
getNextPageParam: () => undefined,
|
|
888
|
+
}))
|
|
889
|
+
return null
|
|
890
|
+
}, null),
|
|
891
|
+
),
|
|
892
|
+
el,
|
|
893
|
+
)
|
|
894
|
+
expect(query!.isPending()).toBe(true)
|
|
895
|
+
expect(query!.isLoading()).toBe(true)
|
|
896
|
+
expect(query!.data()).toBeUndefined()
|
|
897
|
+
expect(query!.status()).toBe('pending')
|
|
898
|
+
expect(query!.isSuccess()).toBe(false)
|
|
899
|
+
expect(query!.isError()).toBe(false)
|
|
900
|
+
expect(query!.error()).toBeNull()
|
|
901
|
+
expect(query!.hasNextPage()).toBe(false)
|
|
902
|
+
expect(query!.hasPreviousPage()).toBe(false)
|
|
903
|
+
expect(query!.isFetchingNextPage()).toBe(false)
|
|
904
|
+
expect(query!.isFetchingPreviousPage()).toBe(false)
|
|
905
|
+
unmount()
|
|
906
|
+
el.remove()
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
it('resolves to success with pages data', async () => {
|
|
910
|
+
let query: ReturnType<typeof useInfiniteQuery<string>> | undefined
|
|
911
|
+
const el = document.createElement('div')
|
|
912
|
+
document.body.appendChild(el)
|
|
913
|
+
const unmount = mount(
|
|
914
|
+
h(
|
|
915
|
+
QueryClientProvider,
|
|
916
|
+
{ client },
|
|
917
|
+
h(() => {
|
|
918
|
+
query = useInfiniteQuery(() => ({
|
|
919
|
+
queryKey: ['inf-success'],
|
|
920
|
+
queryFn: ({ pageParam }: { pageParam: number }) =>
|
|
921
|
+
Promise.resolve(`page-${pageParam}`),
|
|
922
|
+
initialPageParam: 0,
|
|
923
|
+
getNextPageParam: (
|
|
924
|
+
_last: string,
|
|
925
|
+
_all: string[],
|
|
926
|
+
lastParam: number,
|
|
927
|
+
) => (lastParam < 2 ? lastParam + 1 : undefined),
|
|
928
|
+
}))
|
|
929
|
+
return null
|
|
930
|
+
}, null),
|
|
931
|
+
),
|
|
932
|
+
el,
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
936
|
+
expect(query!.isSuccess()).toBe(true)
|
|
937
|
+
expect(query!.status()).toBe('success')
|
|
938
|
+
expect(query!.data()?.pages).toEqual(['page-0'])
|
|
939
|
+
expect(query!.hasNextPage()).toBe(true)
|
|
940
|
+
expect(query!.isPending()).toBe(false)
|
|
941
|
+
expect(query!.isFetching()).toBe(false)
|
|
942
|
+
unmount()
|
|
943
|
+
el.remove()
|
|
944
|
+
})
|
|
945
|
+
|
|
946
|
+
it('fetchNextPage loads the next page', async () => {
|
|
947
|
+
let query: ReturnType<typeof useInfiniteQuery<string>> | undefined
|
|
948
|
+
const el = document.createElement('div')
|
|
949
|
+
document.body.appendChild(el)
|
|
950
|
+
const unmount = mount(
|
|
951
|
+
h(
|
|
952
|
+
QueryClientProvider,
|
|
953
|
+
{ client },
|
|
954
|
+
h(() => {
|
|
955
|
+
query = useInfiniteQuery(() => ({
|
|
956
|
+
queryKey: ['inf-next'],
|
|
957
|
+
queryFn: ({ pageParam }: { pageParam: number }) =>
|
|
958
|
+
Promise.resolve(`page-${pageParam}`),
|
|
959
|
+
initialPageParam: 0,
|
|
960
|
+
getNextPageParam: (
|
|
961
|
+
_last: string,
|
|
962
|
+
_all: string[],
|
|
963
|
+
lastParam: number,
|
|
964
|
+
) => (lastParam < 2 ? lastParam + 1 : undefined),
|
|
965
|
+
}))
|
|
966
|
+
return null
|
|
967
|
+
}, null),
|
|
968
|
+
),
|
|
969
|
+
el,
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
973
|
+
expect(query!.data()?.pages).toEqual(['page-0'])
|
|
974
|
+
|
|
975
|
+
await query!.fetchNextPage()
|
|
976
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
977
|
+
expect(query!.data()?.pages).toEqual(['page-0', 'page-1'])
|
|
978
|
+
expect(query!.hasNextPage()).toBe(true)
|
|
979
|
+
|
|
980
|
+
await query!.fetchNextPage()
|
|
981
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
982
|
+
expect(query!.data()?.pages).toEqual(['page-0', 'page-1', 'page-2'])
|
|
983
|
+
expect(query!.hasNextPage()).toBe(false)
|
|
984
|
+
unmount()
|
|
985
|
+
el.remove()
|
|
986
|
+
})
|
|
987
|
+
|
|
988
|
+
it('fetchPreviousPage loads the previous page', async () => {
|
|
989
|
+
let query: ReturnType<typeof useInfiniteQuery<string>> | undefined
|
|
990
|
+
const el = document.createElement('div')
|
|
991
|
+
document.body.appendChild(el)
|
|
992
|
+
const unmount = mount(
|
|
993
|
+
h(
|
|
994
|
+
QueryClientProvider,
|
|
995
|
+
{ client },
|
|
996
|
+
h(() => {
|
|
997
|
+
query = useInfiniteQuery(() => ({
|
|
998
|
+
queryKey: ['inf-prev'],
|
|
999
|
+
queryFn: ({ pageParam }: { pageParam: number }) =>
|
|
1000
|
+
Promise.resolve(`page-${pageParam}`),
|
|
1001
|
+
initialPageParam: 5,
|
|
1002
|
+
getNextPageParam: () => undefined,
|
|
1003
|
+
getPreviousPageParam: (
|
|
1004
|
+
_first: string,
|
|
1005
|
+
_all: string[],
|
|
1006
|
+
firstParam: number,
|
|
1007
|
+
) => (firstParam > 3 ? firstParam - 1 : undefined),
|
|
1008
|
+
}))
|
|
1009
|
+
return null
|
|
1010
|
+
}, null),
|
|
1011
|
+
),
|
|
1012
|
+
el,
|
|
1013
|
+
)
|
|
1014
|
+
|
|
1015
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1016
|
+
expect(query!.hasPreviousPage()).toBe(true)
|
|
1017
|
+
|
|
1018
|
+
await query!.fetchPreviousPage()
|
|
1019
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1020
|
+
expect(query!.data()?.pages).toContain('page-4')
|
|
1021
|
+
unmount()
|
|
1022
|
+
el.remove()
|
|
1023
|
+
})
|
|
1024
|
+
|
|
1025
|
+
it('captures error state', async () => {
|
|
1026
|
+
let query: ReturnType<typeof useInfiniteQuery> | undefined
|
|
1027
|
+
const el = document.createElement('div')
|
|
1028
|
+
document.body.appendChild(el)
|
|
1029
|
+
const unmount = mount(
|
|
1030
|
+
h(
|
|
1031
|
+
QueryClientProvider,
|
|
1032
|
+
{ client },
|
|
1033
|
+
h(() => {
|
|
1034
|
+
query = useInfiniteQuery(() => ({
|
|
1035
|
+
queryKey: ['inf-error'],
|
|
1036
|
+
queryFn: () => Promise.reject(new Error('inf failed')),
|
|
1037
|
+
initialPageParam: 0,
|
|
1038
|
+
getNextPageParam: () => undefined,
|
|
1039
|
+
}))
|
|
1040
|
+
return null
|
|
1041
|
+
}, null),
|
|
1042
|
+
),
|
|
1043
|
+
el,
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1047
|
+
expect(query!.isError()).toBe(true)
|
|
1048
|
+
expect(query!.status()).toBe('error')
|
|
1049
|
+
expect((query!.error() as Error).message).toBe('inf failed')
|
|
1050
|
+
unmount()
|
|
1051
|
+
el.remove()
|
|
1052
|
+
})
|
|
1053
|
+
|
|
1054
|
+
it('refetch re-fetches the query', async () => {
|
|
1055
|
+
let callCount = 0
|
|
1056
|
+
let query: ReturnType<typeof useInfiniteQuery<string>> | undefined
|
|
1057
|
+
const el = document.createElement('div')
|
|
1058
|
+
document.body.appendChild(el)
|
|
1059
|
+
const unmount = mount(
|
|
1060
|
+
h(
|
|
1061
|
+
QueryClientProvider,
|
|
1062
|
+
{ client },
|
|
1063
|
+
h(() => {
|
|
1064
|
+
query = useInfiniteQuery(() => ({
|
|
1065
|
+
queryKey: ['inf-refetch'],
|
|
1066
|
+
queryFn: () => {
|
|
1067
|
+
callCount++
|
|
1068
|
+
return Promise.resolve('data')
|
|
1069
|
+
},
|
|
1070
|
+
initialPageParam: 0,
|
|
1071
|
+
getNextPageParam: () => undefined,
|
|
1072
|
+
}))
|
|
1073
|
+
return null
|
|
1074
|
+
}, null),
|
|
1075
|
+
),
|
|
1076
|
+
el,
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1080
|
+
const before = callCount
|
|
1081
|
+
await query!.refetch()
|
|
1082
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1083
|
+
expect(callCount).toBeGreaterThan(before)
|
|
1084
|
+
unmount()
|
|
1085
|
+
el.remove()
|
|
1086
|
+
})
|
|
1087
|
+
|
|
1088
|
+
it('result signal contains full observer result', async () => {
|
|
1089
|
+
let query: ReturnType<typeof useInfiniteQuery<string>> | undefined
|
|
1090
|
+
const el = document.createElement('div')
|
|
1091
|
+
document.body.appendChild(el)
|
|
1092
|
+
const unmount = mount(
|
|
1093
|
+
h(
|
|
1094
|
+
QueryClientProvider,
|
|
1095
|
+
{ client },
|
|
1096
|
+
h(() => {
|
|
1097
|
+
query = useInfiniteQuery(() => ({
|
|
1098
|
+
queryKey: ['inf-result'],
|
|
1099
|
+
queryFn: () => Promise.resolve('val'),
|
|
1100
|
+
initialPageParam: 0,
|
|
1101
|
+
getNextPageParam: () => undefined,
|
|
1102
|
+
}))
|
|
1103
|
+
return null
|
|
1104
|
+
}, null),
|
|
1105
|
+
),
|
|
1106
|
+
el,
|
|
1107
|
+
)
|
|
1108
|
+
|
|
1109
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
1110
|
+
const r = query!.result()
|
|
1111
|
+
expect(r.status).toBe('success')
|
|
1112
|
+
expect(r.data?.pages).toEqual(['val'])
|
|
1113
|
+
unmount()
|
|
1114
|
+
el.remove()
|
|
1115
|
+
})
|
|
1116
|
+
|
|
1117
|
+
it('reactive options update observer', async () => {
|
|
1118
|
+
const key = signal('a')
|
|
1119
|
+
let query: ReturnType<typeof useInfiniteQuery<string>> | undefined
|
|
1120
|
+
const el = document.createElement('div')
|
|
1121
|
+
document.body.appendChild(el)
|
|
1122
|
+
const unmount = mount(
|
|
1123
|
+
h(
|
|
1124
|
+
QueryClientProvider,
|
|
1125
|
+
{ client },
|
|
1126
|
+
h(() => {
|
|
1127
|
+
query = useInfiniteQuery(() => ({
|
|
1128
|
+
queryKey: ['inf-reactive', key()],
|
|
1129
|
+
queryFn: () => Promise.resolve(`data-${key()}`),
|
|
1130
|
+
initialPageParam: 0,
|
|
1131
|
+
getNextPageParam: () => undefined,
|
|
1132
|
+
}))
|
|
1133
|
+
return null
|
|
1134
|
+
}, null),
|
|
1135
|
+
),
|
|
1136
|
+
el,
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1140
|
+
expect(query!.data()?.pages).toEqual(['data-a'])
|
|
1141
|
+
|
|
1142
|
+
key.set('b')
|
|
1143
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1144
|
+
expect(query!.data()?.pages).toEqual(['data-b'])
|
|
1145
|
+
unmount()
|
|
1146
|
+
el.remove()
|
|
1147
|
+
})
|
|
1148
|
+
})
|
|
1149
|
+
|
|
1150
|
+
// ─── useSuspenseInfiniteQuery ────────────────────────────────────────────────
|
|
1151
|
+
|
|
1152
|
+
describe('useSuspenseInfiniteQuery', () => {
|
|
1153
|
+
let client: QueryClient
|
|
1154
|
+
beforeEach(() => {
|
|
1155
|
+
client = makeClient()
|
|
1156
|
+
})
|
|
1157
|
+
|
|
1158
|
+
it('returns all fine-grained signals and resolves to success', async () => {
|
|
1159
|
+
let query: ReturnType<typeof useSuspenseInfiniteQuery<string>> | undefined
|
|
1160
|
+
const el = document.createElement('div')
|
|
1161
|
+
document.body.appendChild(el)
|
|
1162
|
+
const unmount = mount(
|
|
1163
|
+
h(
|
|
1164
|
+
QueryClientProvider,
|
|
1165
|
+
{ client },
|
|
1166
|
+
h(() => {
|
|
1167
|
+
query = useSuspenseInfiniteQuery(() => ({
|
|
1168
|
+
queryKey: ['sinf-1'],
|
|
1169
|
+
queryFn: ({ pageParam }: { pageParam: number }) =>
|
|
1170
|
+
Promise.resolve(`p${pageParam}`),
|
|
1171
|
+
initialPageParam: 0,
|
|
1172
|
+
getNextPageParam: (_l: string, _a: string[], lp: number) =>
|
|
1173
|
+
lp < 1 ? lp + 1 : undefined,
|
|
1174
|
+
}))
|
|
1175
|
+
return null
|
|
1176
|
+
}, null),
|
|
1177
|
+
),
|
|
1178
|
+
el,
|
|
1179
|
+
)
|
|
1180
|
+
|
|
1181
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1182
|
+
expect(query!.isSuccess()).toBe(true)
|
|
1183
|
+
expect(query!.status()).toBe('success')
|
|
1184
|
+
expect(query!.data()?.pages).toEqual(['p0'])
|
|
1185
|
+
expect(query!.error()).toBeNull()
|
|
1186
|
+
expect(query!.isError()).toBe(false)
|
|
1187
|
+
expect(query!.isFetching()).toBe(false)
|
|
1188
|
+
expect(query!.isFetchingNextPage()).toBe(false)
|
|
1189
|
+
expect(query!.isFetchingPreviousPage()).toBe(false)
|
|
1190
|
+
expect(query!.hasNextPage()).toBe(true)
|
|
1191
|
+
expect(query!.hasPreviousPage()).toBe(false)
|
|
1192
|
+
unmount()
|
|
1193
|
+
el.remove()
|
|
1194
|
+
})
|
|
1195
|
+
|
|
1196
|
+
it('fetchNextPage and fetchPreviousPage work', async () => {
|
|
1197
|
+
let query: ReturnType<typeof useSuspenseInfiniteQuery<string>> | undefined
|
|
1198
|
+
const el = document.createElement('div')
|
|
1199
|
+
document.body.appendChild(el)
|
|
1200
|
+
const unmount = mount(
|
|
1201
|
+
h(
|
|
1202
|
+
QueryClientProvider,
|
|
1203
|
+
{ client },
|
|
1204
|
+
h(() => {
|
|
1205
|
+
query = useSuspenseInfiniteQuery(() => ({
|
|
1206
|
+
queryKey: ['sinf-pages'],
|
|
1207
|
+
queryFn: ({ pageParam }: { pageParam: number }) =>
|
|
1208
|
+
Promise.resolve(`p${pageParam}`),
|
|
1209
|
+
initialPageParam: 1,
|
|
1210
|
+
getNextPageParam: (_l: string, _a: string[], lp: number) =>
|
|
1211
|
+
lp < 3 ? lp + 1 : undefined,
|
|
1212
|
+
getPreviousPageParam: (_f: string, _a: string[], fp: number) =>
|
|
1213
|
+
fp > 0 ? fp - 1 : undefined,
|
|
1214
|
+
}))
|
|
1215
|
+
return null
|
|
1216
|
+
}, null),
|
|
1217
|
+
),
|
|
1218
|
+
el,
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1222
|
+
await query!.fetchNextPage()
|
|
1223
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1224
|
+
expect(query!.data()?.pages).toContain('p2')
|
|
1225
|
+
|
|
1226
|
+
await query!.fetchPreviousPage()
|
|
1227
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1228
|
+
expect(query!.data()?.pages).toContain('p0')
|
|
1229
|
+
unmount()
|
|
1230
|
+
el.remove()
|
|
1231
|
+
})
|
|
1232
|
+
|
|
1233
|
+
it('refetch works', async () => {
|
|
1234
|
+
let callCount = 0
|
|
1235
|
+
let query: ReturnType<typeof useSuspenseInfiniteQuery<string>> | undefined
|
|
1236
|
+
const el = document.createElement('div')
|
|
1237
|
+
document.body.appendChild(el)
|
|
1238
|
+
const unmount = mount(
|
|
1239
|
+
h(
|
|
1240
|
+
QueryClientProvider,
|
|
1241
|
+
{ client },
|
|
1242
|
+
h(() => {
|
|
1243
|
+
query = useSuspenseInfiniteQuery(() => ({
|
|
1244
|
+
queryKey: ['sinf-refetch'],
|
|
1245
|
+
queryFn: () => {
|
|
1246
|
+
callCount++
|
|
1247
|
+
return Promise.resolve('d')
|
|
1248
|
+
},
|
|
1249
|
+
initialPageParam: 0,
|
|
1250
|
+
getNextPageParam: () => undefined,
|
|
1251
|
+
}))
|
|
1252
|
+
return null
|
|
1253
|
+
}, null),
|
|
1254
|
+
),
|
|
1255
|
+
el,
|
|
1256
|
+
)
|
|
1257
|
+
|
|
1258
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1259
|
+
const before = callCount
|
|
1260
|
+
await query!.refetch()
|
|
1261
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1262
|
+
expect(callCount).toBeGreaterThan(before)
|
|
1263
|
+
unmount()
|
|
1264
|
+
el.remove()
|
|
1265
|
+
})
|
|
1266
|
+
|
|
1267
|
+
it('result signal contains full observer result', async () => {
|
|
1268
|
+
let query: ReturnType<typeof useSuspenseInfiniteQuery<string>> | undefined
|
|
1269
|
+
const el = document.createElement('div')
|
|
1270
|
+
document.body.appendChild(el)
|
|
1271
|
+
const unmount = mount(
|
|
1272
|
+
h(
|
|
1273
|
+
QueryClientProvider,
|
|
1274
|
+
{ client },
|
|
1275
|
+
h(() => {
|
|
1276
|
+
query = useSuspenseInfiniteQuery(() => ({
|
|
1277
|
+
queryKey: ['sinf-result'],
|
|
1278
|
+
queryFn: () => Promise.resolve('v'),
|
|
1279
|
+
initialPageParam: 0,
|
|
1280
|
+
getNextPageParam: () => undefined,
|
|
1281
|
+
}))
|
|
1282
|
+
return null
|
|
1283
|
+
}, null),
|
|
1284
|
+
),
|
|
1285
|
+
el,
|
|
1286
|
+
)
|
|
1287
|
+
|
|
1288
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1289
|
+
expect(query!.result().status).toBe('success')
|
|
1290
|
+
unmount()
|
|
1291
|
+
el.remove()
|
|
1292
|
+
})
|
|
1293
|
+
|
|
1294
|
+
it('reactive options update observer', async () => {
|
|
1295
|
+
const key = signal('x')
|
|
1296
|
+
let query: ReturnType<typeof useSuspenseInfiniteQuery<string>> | undefined
|
|
1297
|
+
const el = document.createElement('div')
|
|
1298
|
+
document.body.appendChild(el)
|
|
1299
|
+
const unmount = mount(
|
|
1300
|
+
h(
|
|
1301
|
+
QueryClientProvider,
|
|
1302
|
+
{ client },
|
|
1303
|
+
h(() => {
|
|
1304
|
+
query = useSuspenseInfiniteQuery(() => ({
|
|
1305
|
+
queryKey: ['sinf-reactive', key()],
|
|
1306
|
+
queryFn: () => Promise.resolve(`val-${key()}`),
|
|
1307
|
+
initialPageParam: 0,
|
|
1308
|
+
getNextPageParam: () => undefined,
|
|
1309
|
+
}))
|
|
1310
|
+
return null
|
|
1311
|
+
}, null),
|
|
1312
|
+
),
|
|
1313
|
+
el,
|
|
1314
|
+
)
|
|
1315
|
+
|
|
1316
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1317
|
+
expect(query!.data()?.pages).toEqual(['val-x'])
|
|
1318
|
+
key.set('y')
|
|
1319
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1320
|
+
expect(query!.data()?.pages).toEqual(['val-y'])
|
|
1321
|
+
unmount()
|
|
1322
|
+
el.remove()
|
|
1323
|
+
})
|
|
1324
|
+
})
|
|
1325
|
+
|
|
1326
|
+
// ─── useSuspenseQuery — additional coverage ──────────────────────────────────
|
|
1327
|
+
|
|
1328
|
+
describe('useSuspenseQuery — additional', () => {
|
|
1329
|
+
let client: QueryClient
|
|
1330
|
+
beforeEach(() => {
|
|
1331
|
+
client = makeClient()
|
|
1332
|
+
})
|
|
1333
|
+
|
|
1334
|
+
it('data is typed as TData (never undefined) after success', async () => {
|
|
1335
|
+
let query: ReturnType<typeof useSuspenseQuery<{ name: string }>> | undefined
|
|
1336
|
+
const el = document.createElement('div')
|
|
1337
|
+
document.body.appendChild(el)
|
|
1338
|
+
const unmount = mount(
|
|
1339
|
+
h(
|
|
1340
|
+
QueryClientProvider,
|
|
1341
|
+
{ client },
|
|
1342
|
+
h(() => {
|
|
1343
|
+
query = useSuspenseQuery(() => ({
|
|
1344
|
+
queryKey: ['sq-data-type'],
|
|
1345
|
+
queryFn: () => Promise.resolve({ name: 'test' }),
|
|
1346
|
+
}))
|
|
1347
|
+
return null
|
|
1348
|
+
}, null),
|
|
1349
|
+
),
|
|
1350
|
+
el,
|
|
1351
|
+
)
|
|
1352
|
+
|
|
1353
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1354
|
+
expect(query!.data().name).toBe('test')
|
|
1355
|
+
expect(query!.isSuccess()).toBe(true)
|
|
1356
|
+
expect(query!.isFetching()).toBe(false)
|
|
1357
|
+
expect(query!.isError()).toBe(false)
|
|
1358
|
+
expect(query!.error()).toBeNull()
|
|
1359
|
+
expect(query!.status()).toBe('success')
|
|
1360
|
+
expect(query!.result().status).toBe('success')
|
|
1361
|
+
unmount()
|
|
1362
|
+
el.remove()
|
|
1363
|
+
})
|
|
1364
|
+
|
|
1365
|
+
it('refetch re-fetches the query', async () => {
|
|
1366
|
+
let callCount = 0
|
|
1367
|
+
let query: ReturnType<typeof useSuspenseQuery<string>> | undefined
|
|
1368
|
+
const el = document.createElement('div')
|
|
1369
|
+
document.body.appendChild(el)
|
|
1370
|
+
const unmount = mount(
|
|
1371
|
+
h(
|
|
1372
|
+
QueryClientProvider,
|
|
1373
|
+
{ client },
|
|
1374
|
+
h(() => {
|
|
1375
|
+
query = useSuspenseQuery(() => ({
|
|
1376
|
+
queryKey: ['sq-refetch'],
|
|
1377
|
+
queryFn: () => {
|
|
1378
|
+
callCount++
|
|
1379
|
+
return Promise.resolve('ok')
|
|
1380
|
+
},
|
|
1381
|
+
}))
|
|
1382
|
+
return null
|
|
1383
|
+
}, null),
|
|
1384
|
+
),
|
|
1385
|
+
el,
|
|
1386
|
+
)
|
|
1387
|
+
|
|
1388
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1389
|
+
const before = callCount
|
|
1390
|
+
await query!.refetch()
|
|
1391
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1392
|
+
expect(callCount).toBeGreaterThan(before)
|
|
1393
|
+
unmount()
|
|
1394
|
+
el.remove()
|
|
1395
|
+
})
|
|
1396
|
+
|
|
1397
|
+
it('reactive key changes trigger re-fetch', async () => {
|
|
1398
|
+
const key = signal('k1')
|
|
1399
|
+
let query: ReturnType<typeof useSuspenseQuery<string>> | undefined
|
|
1400
|
+
const el = document.createElement('div')
|
|
1401
|
+
document.body.appendChild(el)
|
|
1402
|
+
const unmount = mount(
|
|
1403
|
+
h(
|
|
1404
|
+
QueryClientProvider,
|
|
1405
|
+
{ client },
|
|
1406
|
+
h(() => {
|
|
1407
|
+
query = useSuspenseQuery(() => ({
|
|
1408
|
+
queryKey: ['sq-reactive', key()],
|
|
1409
|
+
queryFn: () => Promise.resolve(`data-${key()}`),
|
|
1410
|
+
}))
|
|
1411
|
+
return null
|
|
1412
|
+
}, null),
|
|
1413
|
+
),
|
|
1414
|
+
el,
|
|
1415
|
+
)
|
|
1416
|
+
|
|
1417
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1418
|
+
expect(query!.data()).toBe('data-k1')
|
|
1419
|
+
|
|
1420
|
+
key.set('k2')
|
|
1421
|
+
await new Promise((r) => setTimeout(r, 20))
|
|
1422
|
+
expect(query!.data()).toBe('data-k2')
|
|
1423
|
+
unmount()
|
|
1424
|
+
el.remove()
|
|
1425
|
+
})
|
|
1426
|
+
|
|
1427
|
+
it('captures error state in suspense query', async () => {
|
|
1428
|
+
const { promise, reject } = deferred<never>()
|
|
1429
|
+
|
|
1430
|
+
const el = document.createElement('div')
|
|
1431
|
+
document.body.appendChild(el)
|
|
1432
|
+
|
|
1433
|
+
let query: ReturnType<typeof useSuspenseQuery> | undefined
|
|
1434
|
+
const unmount = mount(
|
|
1435
|
+
h(
|
|
1436
|
+
QueryClientProvider,
|
|
1437
|
+
{ client },
|
|
1438
|
+
h(() => {
|
|
1439
|
+
query = useSuspenseQuery(() => ({
|
|
1440
|
+
queryKey: ['sq-rethrow2'],
|
|
1441
|
+
queryFn: () => promise,
|
|
1442
|
+
}))
|
|
1443
|
+
return null
|
|
1444
|
+
}, null),
|
|
1445
|
+
),
|
|
1446
|
+
el,
|
|
1447
|
+
)
|
|
1448
|
+
|
|
1449
|
+
reject(new Error('rethrow test'))
|
|
1450
|
+
await promise.catch(() => {
|
|
1451
|
+
/* expected */
|
|
1452
|
+
})
|
|
1453
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
1454
|
+
expect(query!.isError()).toBe(true)
|
|
1455
|
+
expect(query!.isPending()).toBe(false)
|
|
1456
|
+
unmount()
|
|
1457
|
+
el.remove()
|
|
1458
|
+
})
|
|
1459
|
+
|
|
1460
|
+
it('QuerySuspense handles fallback as function', async () => {
|
|
1461
|
+
let query: ReturnType<typeof useSuspenseQuery<string>> | undefined
|
|
1462
|
+
const el = document.createElement('div')
|
|
1463
|
+
document.body.appendChild(el)
|
|
1464
|
+
const unmount = mount(
|
|
1465
|
+
h(
|
|
1466
|
+
QueryClientProvider,
|
|
1467
|
+
{ client },
|
|
1468
|
+
h(() => {
|
|
1469
|
+
query = useSuspenseQuery(() => ({
|
|
1470
|
+
queryKey: ['sq-fn-fallback'],
|
|
1471
|
+
queryFn: () =>
|
|
1472
|
+
new Promise(() => {
|
|
1473
|
+
/* never resolves */
|
|
1474
|
+
}),
|
|
1475
|
+
}))
|
|
1476
|
+
return h(
|
|
1477
|
+
QuerySuspense as any,
|
|
1478
|
+
{ query: query!, fallback: () => 'loading fn' },
|
|
1479
|
+
() => null,
|
|
1480
|
+
)
|
|
1481
|
+
}, null),
|
|
1482
|
+
),
|
|
1483
|
+
el,
|
|
1484
|
+
)
|
|
1485
|
+
|
|
1486
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
1487
|
+
// The fallback function should be called
|
|
1488
|
+
expect(query!.isPending()).toBe(true)
|
|
1489
|
+
unmount()
|
|
1490
|
+
el.remove()
|
|
1491
|
+
})
|
|
1492
|
+
})
|
|
1493
|
+
|
|
1494
|
+
// ─── Coverage gap tests ──────────────────────────────────────────────────────
|
|
1495
|
+
|
|
1496
|
+
describe('QueryClientProvider — VNode children branch', () => {
|
|
1497
|
+
it('renders when children is a VNode (not a function)', () => {
|
|
1498
|
+
const client = makeClient()
|
|
1499
|
+
let received: QueryClient | null = null
|
|
1500
|
+
const el = document.createElement('div')
|
|
1501
|
+
document.body.appendChild(el)
|
|
1502
|
+
// Pass children as a direct VNode, not wrapped in a function
|
|
1503
|
+
const unmount = mount(
|
|
1504
|
+
h(
|
|
1505
|
+
QueryClientProvider,
|
|
1506
|
+
{ client },
|
|
1507
|
+
h(() => {
|
|
1508
|
+
received = useQueryClient()
|
|
1509
|
+
return null
|
|
1510
|
+
}, null),
|
|
1511
|
+
),
|
|
1512
|
+
el,
|
|
1513
|
+
)
|
|
1514
|
+
expect(received).toBe(client)
|
|
1515
|
+
unmount()
|
|
1516
|
+
el.remove()
|
|
1517
|
+
})
|
|
1518
|
+
|
|
1519
|
+
it('renders when children is passed as a function returning VNode', () => {
|
|
1520
|
+
const client = makeClient()
|
|
1521
|
+
let received: QueryClient | null = null
|
|
1522
|
+
const el = document.createElement('div')
|
|
1523
|
+
document.body.appendChild(el)
|
|
1524
|
+
const unmount = mount(
|
|
1525
|
+
h(QueryClientProvider, { client }, () => {
|
|
1526
|
+
return h(() => {
|
|
1527
|
+
received = useQueryClient()
|
|
1528
|
+
return null
|
|
1529
|
+
}, null)
|
|
1530
|
+
}),
|
|
1531
|
+
el,
|
|
1532
|
+
)
|
|
1533
|
+
expect(received).toBe(client)
|
|
1534
|
+
unmount()
|
|
1535
|
+
el.remove()
|
|
1536
|
+
})
|
|
1537
|
+
})
|
|
1538
|
+
|
|
1539
|
+
describe('useMutation — mutateAsync', () => {
|
|
1540
|
+
let client: QueryClient
|
|
1541
|
+
beforeEach(() => {
|
|
1542
|
+
client = makeClient()
|
|
1543
|
+
})
|
|
1544
|
+
|
|
1545
|
+
it('mutateAsync returns a promise that resolves with data', async () => {
|
|
1546
|
+
let mut: ReturnType<typeof useMutation<string, Error, string>> | undefined
|
|
1547
|
+
const el = document.createElement('div')
|
|
1548
|
+
document.body.appendChild(el)
|
|
1549
|
+
const unmount = mount(
|
|
1550
|
+
h(
|
|
1551
|
+
QueryClientProvider,
|
|
1552
|
+
{ client },
|
|
1553
|
+
h(() => {
|
|
1554
|
+
mut = useMutation<string, Error, string>({
|
|
1555
|
+
mutationFn: async (input: string) => `async-result:${input}`,
|
|
1556
|
+
})
|
|
1557
|
+
return null
|
|
1558
|
+
}, null),
|
|
1559
|
+
),
|
|
1560
|
+
el,
|
|
1561
|
+
)
|
|
1562
|
+
|
|
1563
|
+
const result = await mut!.mutateAsync('test')
|
|
1564
|
+
expect(result).toBe('async-result:test')
|
|
1565
|
+
expect(mut!.isSuccess()).toBe(true)
|
|
1566
|
+
expect(mut!.data()).toBe('async-result:test')
|
|
1567
|
+
unmount()
|
|
1568
|
+
el.remove()
|
|
1569
|
+
})
|
|
1570
|
+
|
|
1571
|
+
it('mutateAsync rejects when mutation fails', async () => {
|
|
1572
|
+
let mut: ReturnType<typeof useMutation<string, Error, void>> | undefined
|
|
1573
|
+
const el = document.createElement('div')
|
|
1574
|
+
document.body.appendChild(el)
|
|
1575
|
+
const unmount = mount(
|
|
1576
|
+
h(
|
|
1577
|
+
QueryClientProvider,
|
|
1578
|
+
{ client },
|
|
1579
|
+
h(() => {
|
|
1580
|
+
mut = useMutation<string, Error, void>({
|
|
1581
|
+
mutationFn: async () => {
|
|
1582
|
+
throw new Error('async-fail')
|
|
1583
|
+
},
|
|
1584
|
+
})
|
|
1585
|
+
return null
|
|
1586
|
+
}, null),
|
|
1587
|
+
),
|
|
1588
|
+
el,
|
|
1589
|
+
)
|
|
1590
|
+
|
|
1591
|
+
await expect(mut!.mutateAsync(undefined)).rejects.toThrow('async-fail')
|
|
1592
|
+
await new Promise((r) => setTimeout(r, 0))
|
|
1593
|
+
expect(mut!.isError()).toBe(true)
|
|
1594
|
+
expect((mut!.error() as Error).message).toBe('async-fail')
|
|
1595
|
+
unmount()
|
|
1596
|
+
el.remove()
|
|
1597
|
+
})
|
|
1598
|
+
})
|
|
1599
|
+
|
|
1600
|
+
describe('useQuery — refetch', () => {
|
|
1601
|
+
let client: QueryClient
|
|
1602
|
+
beforeEach(() => {
|
|
1603
|
+
client = makeClient()
|
|
1604
|
+
})
|
|
1605
|
+
|
|
1606
|
+
it('refetch re-fetches the query and returns updated result', async () => {
|
|
1607
|
+
let callCount = 0
|
|
1608
|
+
let query: ReturnType<typeof useQuery<string>> | undefined
|
|
1609
|
+
const el = document.createElement('div')
|
|
1610
|
+
document.body.appendChild(el)
|
|
1611
|
+
const unmount = mount(
|
|
1612
|
+
h(
|
|
1613
|
+
QueryClientProvider,
|
|
1614
|
+
{ client },
|
|
1615
|
+
h(() => {
|
|
1616
|
+
query = useQuery(() => ({
|
|
1617
|
+
queryKey: ['refetch-test'],
|
|
1618
|
+
queryFn: async () => {
|
|
1619
|
+
callCount++
|
|
1620
|
+
return `call-${callCount}`
|
|
1621
|
+
},
|
|
1622
|
+
}))
|
|
1623
|
+
return null
|
|
1624
|
+
}, null),
|
|
1625
|
+
),
|
|
1626
|
+
el,
|
|
1627
|
+
)
|
|
1628
|
+
|
|
1629
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
1630
|
+
expect(query!.data()).toBe('call-1')
|
|
1631
|
+
|
|
1632
|
+
const result = await query!.refetch()
|
|
1633
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
1634
|
+
expect(callCount).toBe(2)
|
|
1635
|
+
expect(result.data).toBe('call-2')
|
|
1636
|
+
expect(query!.data()).toBe('call-2')
|
|
1637
|
+
unmount()
|
|
1638
|
+
el.remove()
|
|
1639
|
+
})
|
|
1640
|
+
})
|
|
1641
|
+
|
|
1642
|
+
describe('QueryErrorResetBoundary — VNode children branch', () => {
|
|
1643
|
+
it('renders when children is a VNode (not a function)', async () => {
|
|
1644
|
+
const client = makeClient()
|
|
1645
|
+
let resetFn: (() => void) | undefined
|
|
1646
|
+
|
|
1647
|
+
const el = document.createElement('div')
|
|
1648
|
+
document.body.appendChild(el)
|
|
1649
|
+
// Pass children as a direct VNode, not a function
|
|
1650
|
+
const unmount = mount(
|
|
1651
|
+
h(
|
|
1652
|
+
QueryClientProvider,
|
|
1653
|
+
{ client },
|
|
1654
|
+
h(
|
|
1655
|
+
QueryErrorResetBoundary,
|
|
1656
|
+
null,
|
|
1657
|
+
h(() => {
|
|
1658
|
+
const { reset } = useQueryErrorResetBoundary()
|
|
1659
|
+
resetFn = reset
|
|
1660
|
+
return null
|
|
1661
|
+
}, null),
|
|
1662
|
+
),
|
|
1663
|
+
),
|
|
1664
|
+
el,
|
|
1665
|
+
)
|
|
1666
|
+
|
|
1667
|
+
expect(resetFn).toBeDefined()
|
|
1668
|
+
expect(() => resetFn?.()).not.toThrow()
|
|
1669
|
+
unmount()
|
|
1670
|
+
el.remove()
|
|
1671
|
+
})
|
|
1672
|
+
})
|
|
1673
|
+
|
|
1674
|
+
describe('useSuspenseQuery — error without handler (QuerySuspense throw branch)', () => {
|
|
1675
|
+
let client: QueryClient
|
|
1676
|
+
beforeEach(() => {
|
|
1677
|
+
client = makeClient()
|
|
1678
|
+
})
|
|
1679
|
+
|
|
1680
|
+
it('QuerySuspense throws error when no error handler is provided', async () => {
|
|
1681
|
+
const { promise, reject } = deferred<never>()
|
|
1682
|
+
|
|
1683
|
+
const el = document.createElement('div')
|
|
1684
|
+
document.body.appendChild(el)
|
|
1685
|
+
|
|
1686
|
+
const _thrownError: unknown = null
|
|
1687
|
+
let query: ReturnType<typeof useSuspenseQuery> | undefined
|
|
1688
|
+
|
|
1689
|
+
const unmount = mount(
|
|
1690
|
+
h(
|
|
1691
|
+
QueryClientProvider,
|
|
1692
|
+
{ client },
|
|
1693
|
+
h(() => {
|
|
1694
|
+
query = useSuspenseQuery(() => ({
|
|
1695
|
+
queryKey: ['sq-throw-no-handler'],
|
|
1696
|
+
queryFn: () => promise,
|
|
1697
|
+
}))
|
|
1698
|
+
// QuerySuspense with NO error handler — should throw
|
|
1699
|
+
return h(
|
|
1700
|
+
QuerySuspense as any,
|
|
1701
|
+
{ query: query!, fallback: 'loading' },
|
|
1702
|
+
() => null,
|
|
1703
|
+
)
|
|
1704
|
+
}, null),
|
|
1705
|
+
),
|
|
1706
|
+
el,
|
|
1707
|
+
)
|
|
1708
|
+
|
|
1709
|
+
reject(new Error('unhandled suspense error'))
|
|
1710
|
+
await promise.catch(() => {
|
|
1711
|
+
/* expected */
|
|
1712
|
+
})
|
|
1713
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
1714
|
+
|
|
1715
|
+
// The error state should be set on the query
|
|
1716
|
+
expect(query!.isError()).toBe(true)
|
|
1717
|
+
expect((query!.error() as Error).message).toBe('unhandled suspense error')
|
|
1718
|
+
|
|
1719
|
+
// Verify that calling the QuerySuspense render function directly would throw
|
|
1720
|
+
// by checking the query is in error state without an error handler
|
|
1721
|
+
// The throw happens inside the reactive render cycle — we verify the query errored
|
|
1722
|
+
unmount()
|
|
1723
|
+
el.remove()
|
|
1724
|
+
})
|
|
1725
|
+
|
|
1726
|
+
it('QuerySuspense re-throws error to be caught externally', async () => {
|
|
1727
|
+
const { promise, reject } = deferred<never>()
|
|
1728
|
+
|
|
1729
|
+
const el = document.createElement('div')
|
|
1730
|
+
document.body.appendChild(el)
|
|
1731
|
+
|
|
1732
|
+
let query: ReturnType<typeof useSuspenseQuery> | undefined
|
|
1733
|
+
|
|
1734
|
+
const unmount = mount(
|
|
1735
|
+
h(
|
|
1736
|
+
QueryClientProvider,
|
|
1737
|
+
{ client },
|
|
1738
|
+
h(() => {
|
|
1739
|
+
query = useSuspenseQuery(() => ({
|
|
1740
|
+
queryKey: ['sq-rethrow-direct'],
|
|
1741
|
+
queryFn: () => promise,
|
|
1742
|
+
}))
|
|
1743
|
+
return null
|
|
1744
|
+
}, null),
|
|
1745
|
+
),
|
|
1746
|
+
el,
|
|
1747
|
+
)
|
|
1748
|
+
|
|
1749
|
+
reject(new Error('direct throw'))
|
|
1750
|
+
await promise.catch(() => {
|
|
1751
|
+
/* expected */
|
|
1752
|
+
})
|
|
1753
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
1754
|
+
|
|
1755
|
+
// Manually invoke QuerySuspense to verify it throws when no error handler
|
|
1756
|
+
expect(() => {
|
|
1757
|
+
const renderFn = QuerySuspense({
|
|
1758
|
+
query: query!,
|
|
1759
|
+
fallback: 'loading',
|
|
1760
|
+
children: () => null,
|
|
1761
|
+
}) as () => unknown
|
|
1762
|
+
renderFn()
|
|
1763
|
+
}).toThrow('direct throw')
|
|
1764
|
+
|
|
1765
|
+
unmount()
|
|
1766
|
+
el.remove()
|
|
1767
|
+
})
|
|
1768
|
+
})
|