@pyreon/query 0.11.3 → 0.11.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/query",
3
- "version": "0.11.3",
3
+ "version": "0.11.5",
4
4
  "description": "Pyreon adapter for TanStack Query",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -44,13 +44,13 @@
44
44
  "@tanstack/query-core": "^5.0.0"
45
45
  },
46
46
  "peerDependencies": {
47
- "@pyreon/core": "^0.11.3",
48
- "@pyreon/reactivity": "^0.11.3"
47
+ "@pyreon/core": "^0.11.5",
48
+ "@pyreon/reactivity": "^0.11.5"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@happy-dom/global-registrator": "^20.8.3",
52
- "@pyreon/core": "^0.11.3",
53
- "@pyreon/reactivity": "^0.11.3",
54
- "@pyreon/runtime-dom": "^0.11.3"
52
+ "@pyreon/core": "^0.11.5",
53
+ "@pyreon/reactivity": "^0.11.5",
54
+ "@pyreon/runtime-dom": "^0.11.5"
55
55
  }
56
56
  }
@@ -0,0 +1,477 @@
1
+ import { mount } from "@pyreon/runtime-dom"
2
+ import { QueryClient } from "@tanstack/query-core"
3
+ import {
4
+ QueryClientProvider,
5
+ QuerySuspense,
6
+ useInfiniteQuery,
7
+ useIsFetching,
8
+ useIsMutating,
9
+ useMutation,
10
+ useQuery,
11
+ useQueryClient,
12
+ useSuspenseQuery,
13
+ } from "../index"
14
+
15
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
16
+
17
+ function makeClient() {
18
+ return new QueryClient({
19
+ defaultOptions: { queries: { retry: false, gcTime: Infinity } },
20
+ })
21
+ }
22
+
23
+ function deferred<T>() {
24
+ let resolve!: (v: T) => void
25
+ let reject!: (e: unknown) => void
26
+ const promise = new Promise<T>((res, rej) => {
27
+ resolve = res
28
+ reject = rej
29
+ })
30
+ return { promise, resolve, reject }
31
+ }
32
+
33
+ // ─── useInfiniteQuery — additional ───────────────────────────────────────────
34
+
35
+ describe("useInfiniteQuery — additional", () => {
36
+ let client: QueryClient
37
+ beforeEach(() => {
38
+ client = makeClient()
39
+ })
40
+
41
+ it("isFetchingNextPage is true during fetchNextPage", async () => {
42
+ const { promise: pagePromise, resolve: resolveNextPage } = deferred<string>()
43
+ let callCount = 0
44
+ let query: ReturnType<typeof useInfiniteQuery<string>> | undefined
45
+
46
+ const el = document.createElement("div")
47
+ document.body.appendChild(el)
48
+ const unmount = mount(
49
+ <QueryClientProvider client={client}>
50
+ {() => {
51
+ query = useInfiniteQuery(() => ({
52
+ queryKey: ["inf-fetching-next"],
53
+ queryFn: ({ pageParam }: { pageParam: number }) => {
54
+ callCount++
55
+ if (callCount === 1) return Promise.resolve("page-0")
56
+ return pagePromise
57
+ },
58
+ initialPageParam: 0,
59
+ getNextPageParam: (_last: string, _all: string[], lastParam: number) =>
60
+ lastParam < 2 ? lastParam + 1 : undefined,
61
+ }))
62
+ return null
63
+ }}
64
+ </QueryClientProvider>,
65
+ el,
66
+ )
67
+
68
+ // Wait for first page
69
+ await new Promise((r) => setTimeout(r, 20))
70
+ expect(query!.isSuccess()).toBe(true)
71
+ expect(query!.isFetchingNextPage()).toBe(false)
72
+
73
+ // Start fetching next page
74
+ const nextPromise = query!.fetchNextPage()
75
+ await new Promise((r) => setTimeout(r, 0))
76
+ expect(query!.isFetchingNextPage()).toBe(true)
77
+ expect(query!.isFetching()).toBe(true)
78
+
79
+ // Resolve and verify
80
+ resolveNextPage("page-1")
81
+ await nextPromise
82
+ await new Promise((r) => setTimeout(r, 10))
83
+ expect(query!.isFetchingNextPage()).toBe(false)
84
+ expect(query!.data()?.pages).toEqual(["page-0", "page-1"])
85
+
86
+ unmount()
87
+ el.remove()
88
+ })
89
+
90
+ it("hasNextPage is false when getNextPageParam returns undefined", async () => {
91
+ let query: ReturnType<typeof useInfiniteQuery<string>> | undefined
92
+ const el = document.createElement("div")
93
+ document.body.appendChild(el)
94
+ const unmount = mount(
95
+ <QueryClientProvider client={client}>
96
+ {() => {
97
+ query = useInfiniteQuery(() => ({
98
+ queryKey: ["inf-no-next"],
99
+ queryFn: () => Promise.resolve("only-page"),
100
+ initialPageParam: 0,
101
+ getNextPageParam: () => undefined, // No more pages
102
+ }))
103
+ return null
104
+ }}
105
+ </QueryClientProvider>,
106
+ el,
107
+ )
108
+
109
+ await new Promise((r) => setTimeout(r, 20))
110
+ expect(query!.hasNextPage()).toBe(false)
111
+ expect(query!.data()?.pages).toEqual(["only-page"])
112
+ unmount()
113
+ el.remove()
114
+ })
115
+
116
+ it("multiple fetchNextPage calls accumulate pages", async () => {
117
+ let query: ReturnType<typeof useInfiniteQuery<string>> | undefined
118
+ const el = document.createElement("div")
119
+ document.body.appendChild(el)
120
+ const unmount = mount(
121
+ <QueryClientProvider client={client}>
122
+ {() => {
123
+ query = useInfiniteQuery(() => ({
124
+ queryKey: ["inf-multi-fetch"],
125
+ queryFn: ({ pageParam }: { pageParam: number }) => Promise.resolve(`p${pageParam}`),
126
+ initialPageParam: 0,
127
+ getNextPageParam: (_last: string, _all: string[], lastParam: number) =>
128
+ lastParam < 4 ? lastParam + 1 : undefined,
129
+ }))
130
+ return null
131
+ }}
132
+ </QueryClientProvider>,
133
+ el,
134
+ )
135
+
136
+ await new Promise((r) => setTimeout(r, 20))
137
+ expect(query!.data()?.pages).toEqual(["p0"])
138
+
139
+ // Fetch pages 1, 2, 3 sequentially
140
+ for (let i = 1; i <= 3; i++) {
141
+ await query!.fetchNextPage()
142
+ await new Promise((r) => setTimeout(r, 10))
143
+ }
144
+
145
+ expect(query!.data()?.pages).toEqual(["p0", "p1", "p2", "p3"])
146
+ expect(query!.hasNextPage()).toBe(true) // page 4 is available
147
+
148
+ await query!.fetchNextPage()
149
+ await new Promise((r) => setTimeout(r, 10))
150
+ expect(query!.data()?.pages).toEqual(["p0", "p1", "p2", "p3", "p4"])
151
+ expect(query!.hasNextPage()).toBe(false) // No more pages
152
+ unmount()
153
+ el.remove()
154
+ })
155
+ })
156
+
157
+ // ─── useSuspenseQuery — additional ───────────────────────────────────────────
158
+
159
+ describe("useSuspenseQuery — suspense behavior", () => {
160
+ let client: QueryClient
161
+ beforeEach(() => {
162
+ client = makeClient()
163
+ })
164
+
165
+ it("isPending is true while query is loading", async () => {
166
+ const { promise, resolve } = deferred<string>()
167
+ let query: ReturnType<typeof useSuspenseQuery<string>> | undefined
168
+
169
+ const el = document.createElement("div")
170
+ document.body.appendChild(el)
171
+ const unmount = mount(
172
+ <QueryClientProvider client={client}>
173
+ {() => {
174
+ query = useSuspenseQuery(() => ({
175
+ queryKey: ["suspense-pending"],
176
+ queryFn: () => promise,
177
+ }))
178
+ return null
179
+ }}
180
+ </QueryClientProvider>,
181
+ el,
182
+ )
183
+
184
+ expect(query!.isPending()).toBe(true)
185
+ expect(query!.isSuccess()).toBe(false)
186
+
187
+ resolve("loaded")
188
+ await promise
189
+ await new Promise((r) => setTimeout(r, 10))
190
+
191
+ expect(query!.isPending()).toBe(false)
192
+ expect(query!.isSuccess()).toBe(true)
193
+ expect(query!.data()).toBe("loaded")
194
+ unmount()
195
+ el.remove()
196
+ })
197
+
198
+ it("QuerySuspense with multiple queries waits for all", async () => {
199
+ const d1 = deferred<string>()
200
+ const d2 = deferred<number>()
201
+ let childCalled = false
202
+
203
+ const el = document.createElement("div")
204
+ document.body.appendChild(el)
205
+ const unmount = mount(
206
+ <QueryClientProvider client={client}>
207
+ {() => {
208
+ const q1 = useSuspenseQuery(() => ({
209
+ queryKey: ["multi-s1"],
210
+ queryFn: () => d1.promise,
211
+ }))
212
+ const q2 = useSuspenseQuery(() => ({
213
+ queryKey: ["multi-s2"],
214
+ queryFn: () => d2.promise,
215
+ }))
216
+ return (
217
+ <QuerySuspense query={[q1, q2]} fallback="loading...">
218
+ {() => {
219
+ childCalled = true
220
+ return null
221
+ }}
222
+ </QuerySuspense>
223
+ )
224
+ }}
225
+ </QueryClientProvider>,
226
+ el,
227
+ )
228
+
229
+ // Only first resolves — children should not render
230
+ d1.resolve("first")
231
+ await d1.promise
232
+ await new Promise((r) => setTimeout(r, 10))
233
+ expect(childCalled).toBe(false)
234
+
235
+ // Both resolved — children should render
236
+ d2.resolve(42)
237
+ await d2.promise
238
+ await new Promise((r) => setTimeout(r, 10))
239
+ expect(childCalled).toBe(true)
240
+ unmount()
241
+ el.remove()
242
+ })
243
+
244
+ it("QuerySuspense renders null fallback when not provided", async () => {
245
+ let query: ReturnType<typeof useSuspenseQuery<string>> | undefined
246
+
247
+ const el = document.createElement("div")
248
+ document.body.appendChild(el)
249
+ const unmount = mount(
250
+ <QueryClientProvider client={client}>
251
+ {() => {
252
+ query = useSuspenseQuery(() => ({
253
+ queryKey: ["suspense-no-fallback"],
254
+ queryFn: () =>
255
+ new Promise(() => {
256
+ /* never resolves */
257
+ }),
258
+ }))
259
+ return <QuerySuspense query={query!}>{() => null}</QuerySuspense>
260
+ }}
261
+ </QueryClientProvider>,
262
+ el,
263
+ )
264
+
265
+ expect(query!.isPending()).toBe(true)
266
+ unmount()
267
+ el.remove()
268
+ })
269
+ })
270
+
271
+ // ─── QueryClientProvider context ─────────────────────────────────────────────
272
+
273
+ describe("QueryClientProvider — context behavior", () => {
274
+ it("useQueryClient returns the provided client", () => {
275
+ const client = makeClient()
276
+ let received: QueryClient | null = null
277
+
278
+ const el = document.createElement("div")
279
+ document.body.appendChild(el)
280
+ const unmount = mount(
281
+ <QueryClientProvider client={client}>
282
+ {() => {
283
+ received = useQueryClient()
284
+ return null
285
+ }}
286
+ </QueryClientProvider>,
287
+ el,
288
+ )
289
+
290
+ expect(received).toBe(client)
291
+ unmount()
292
+ el.remove()
293
+ })
294
+
295
+ it("nested providers override outer client", () => {
296
+ const outerClient = makeClient()
297
+ const innerClient = makeClient()
298
+ let outerReceived: QueryClient | null = null
299
+ let innerReceived: QueryClient | null = null
300
+
301
+ const el = document.createElement("div")
302
+ document.body.appendChild(el)
303
+ const unmount = mount(
304
+ <QueryClientProvider client={outerClient}>
305
+ {() => {
306
+ outerReceived = useQueryClient()
307
+ return (
308
+ <QueryClientProvider client={innerClient}>
309
+ {() => {
310
+ innerReceived = useQueryClient()
311
+ return null
312
+ }}
313
+ </QueryClientProvider>
314
+ )
315
+ }}
316
+ </QueryClientProvider>,
317
+ el,
318
+ )
319
+
320
+ expect(outerReceived).toBe(outerClient)
321
+ expect(innerReceived).toBe(innerClient)
322
+ expect(outerReceived).not.toBe(innerReceived)
323
+ unmount()
324
+ el.remove()
325
+ })
326
+
327
+ it("useQueryClient throws descriptive error without provider", () => {
328
+ expect(() => useQueryClient()).toThrow("No QueryClient found")
329
+ })
330
+ })
331
+
332
+ // ─── useIsFetching / useIsMutating — additional ──────────────────────────────
333
+
334
+ describe("useIsFetching — additional", () => {
335
+ it("counts multiple concurrent queries", async () => {
336
+ const client = makeClient()
337
+ const d1 = deferred<string>()
338
+ const d2 = deferred<string>()
339
+ let isFetching: (() => number) | undefined
340
+
341
+ const el = document.createElement("div")
342
+ document.body.appendChild(el)
343
+ const unmount = mount(
344
+ <QueryClientProvider client={client}>
345
+ {() => {
346
+ isFetching = useIsFetching()
347
+ useQuery(() => ({
348
+ queryKey: ["concurrent-1"],
349
+ queryFn: () => d1.promise,
350
+ }))
351
+ useQuery(() => ({
352
+ queryKey: ["concurrent-2"],
353
+ queryFn: () => d2.promise,
354
+ }))
355
+ return null
356
+ }}
357
+ </QueryClientProvider>,
358
+ el,
359
+ )
360
+
361
+ await new Promise((r) => setTimeout(r, 0))
362
+ // Both queries should be fetching
363
+ expect(isFetching!()).toBeGreaterThanOrEqual(2)
364
+
365
+ d1.resolve("done1")
366
+ await d1.promise
367
+ await new Promise((r) => setTimeout(r, 10))
368
+ // One still fetching
369
+ expect(isFetching!()).toBeGreaterThanOrEqual(1)
370
+
371
+ d2.resolve("done2")
372
+ await d2.promise
373
+ await new Promise((r) => setTimeout(r, 10))
374
+ expect(isFetching!()).toBe(0)
375
+
376
+ unmount()
377
+ el.remove()
378
+ })
379
+
380
+ it("useIsFetching with query key filter", async () => {
381
+ const client = makeClient()
382
+ const d1 = deferred<string>()
383
+ const d2 = deferred<string>()
384
+ let allFetching: (() => number) | undefined
385
+ let userFetching: (() => number) | undefined
386
+
387
+ const el = document.createElement("div")
388
+ document.body.appendChild(el)
389
+ const unmount = mount(
390
+ <QueryClientProvider client={client}>
391
+ {() => {
392
+ allFetching = useIsFetching()
393
+ userFetching = useIsFetching({ queryKey: ["user"] })
394
+ useQuery(() => ({
395
+ queryKey: ["user", "1"],
396
+ queryFn: () => d1.promise,
397
+ }))
398
+ useQuery(() => ({
399
+ queryKey: ["posts"],
400
+ queryFn: () => d2.promise,
401
+ }))
402
+ return null
403
+ }}
404
+ </QueryClientProvider>,
405
+ el,
406
+ )
407
+
408
+ await new Promise((r) => setTimeout(r, 0))
409
+ expect(allFetching!()).toBeGreaterThanOrEqual(2)
410
+ expect(userFetching!()).toBe(1) // Only the user query
411
+
412
+ d1.resolve("user-data")
413
+ await d1.promise
414
+ await new Promise((r) => setTimeout(r, 10))
415
+ expect(userFetching!()).toBe(0)
416
+ expect(allFetching!()).toBeGreaterThanOrEqual(1) // posts still fetching
417
+
418
+ d2.resolve("posts-data")
419
+ await d2.promise
420
+ await new Promise((r) => setTimeout(r, 10))
421
+ expect(allFetching!()).toBe(0)
422
+
423
+ unmount()
424
+ el.remove()
425
+ })
426
+ })
427
+
428
+ describe("useIsMutating — additional", () => {
429
+ it("counts multiple concurrent mutations", async () => {
430
+ const client = makeClient()
431
+ const d1 = deferred<void>()
432
+ const d2 = deferred<void>()
433
+ let isMutating: (() => number) | undefined
434
+ let mut1: ReturnType<typeof useMutation<void, Error, void>> | undefined
435
+ let mut2: ReturnType<typeof useMutation<void, Error, void>> | undefined
436
+
437
+ const el = document.createElement("div")
438
+ document.body.appendChild(el)
439
+ const unmount = mount(
440
+ <QueryClientProvider client={client}>
441
+ {() => {
442
+ isMutating = useIsMutating()
443
+ mut1 = useMutation<void, Error, void>({ mutationFn: () => d1.promise })
444
+ mut2 = useMutation<void, Error, void>({ mutationFn: () => d2.promise })
445
+ return null
446
+ }}
447
+ </QueryClientProvider>,
448
+ el,
449
+ )
450
+
451
+ expect(isMutating!()).toBe(0)
452
+
453
+ mut1!.mutate(undefined)
454
+ mut2!.mutate(undefined)
455
+ await new Promise((r) => setTimeout(r, 0))
456
+ expect(isMutating!()).toBe(2)
457
+
458
+ d1.resolve()
459
+ await d1.promise
460
+ await new Promise((r) => setTimeout(r, 10))
461
+ expect(isMutating!()).toBe(1)
462
+
463
+ d2.resolve()
464
+ await d2.promise
465
+ await new Promise((r) => setTimeout(r, 10))
466
+ expect(isMutating!()).toBe(0)
467
+
468
+ unmount()
469
+ el.remove()
470
+ })
471
+ })
472
+
473
+ // ─── useSSE — lastEventId and readyState (already covered in sse.test.tsx,
474
+ // ─── but we add a few integration tests here) ────────────────────────────────
475
+
476
+ // Note: useSSE lastEventId and readyState are thoroughly tested in sse.test.tsx.
477
+ // This file focuses on query/mutation/infinite/suspense/provider scenarios.