@codeleap/query 5.8.2 → 5.8.4

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.
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect, beforeEach, mock, spyOn } from "bun:test"
2
+ import React from "react"
3
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
4
+ import { renderHook, act, waitFor } from "@testing-library/react"
5
+ import { QueryOperations } from "../lib/QueryOperations"
6
+
7
+ const createWrapper = (queryClient: QueryClient) => {
8
+ return ({ children }: { children: React.ReactNode }) => (
9
+ <QueryClientProvider client={queryClient}>
10
+ {children}
11
+ </QueryClientProvider>
12
+ )
13
+ }
14
+
15
+ describe("QueryOperations", () => {
16
+ let queryClient: QueryClient
17
+ let operations: QueryOperations<{}, {}>
18
+
19
+ beforeEach(() => {
20
+ queryClient = new QueryClient()
21
+ operations = new QueryOperations({ queryClient })
22
+ })
23
+
24
+ it("registers a mutation correctly", () => {
25
+ const fn = mock(() => Promise.resolve("ok"))
26
+ const ops = operations.mutation("createUser", fn)
27
+
28
+ expect(Object.keys(ops.mutations)).toContain("createUser")
29
+ expect(typeof ops.mutations.createUser).toBe("function")
30
+ })
31
+
32
+ it("registers a query correctly", () => {
33
+ const fn = mock((id: string) => Promise.resolve({ id }))
34
+ const ops = operations.query("getUser", fn)
35
+
36
+ expect(Object.keys(ops.queries)).toContain("getUser")
37
+ expect(typeof ops.queries.getUser).toBe("function")
38
+ })
39
+
40
+ it("getQueryKey includes params when passed", () => {
41
+ const ops = operations.query("getUser", async (id: string) => ({ id }))
42
+ expect(ops.getQueryKey("getUser", "123")).toEqual(["getUser", "123"])
43
+ expect(ops.getQueryKey("getUser")).toEqual(["getUser"])
44
+ })
45
+
46
+ it("getMutationKey returns mutation key", () => {
47
+ const ops = operations.mutation("createUser", async (data: any) => data)
48
+ expect(ops.getMutationKey("createUser")).toEqual(["createUser"])
49
+ })
50
+
51
+ it("prefetchQuery calls queryClient.prefetchQuery", async () => {
52
+ const fn = mock((id: string) => Promise.resolve({ id }))
53
+ const ops = operations.query("getUser", fn)
54
+
55
+ const spy = spyOn(queryClient, "prefetchQuery")
56
+ await ops.prefetchQuery("getUser", "abc")
57
+
58
+ expect(spy).toHaveBeenCalled()
59
+ expect(spy.mock.calls[0][0].queryKey).toEqual(["getUser", "abc"])
60
+ })
61
+
62
+ it("getQueryData fetches from cache or prefetches", async () => {
63
+ const fn = async (id: string) => ({ id })
64
+ const ops = operations.query("getUser", fn)
65
+
66
+ const spyPrefetch = spyOn(queryClient, "prefetchQuery")
67
+
68
+ const data = await ops.getQueryData("getUser", "123")
69
+ expect(data).toBeDefined()
70
+ expect(spyPrefetch).toHaveBeenCalled()
71
+ })
72
+
73
+ describe("useMutation hook", () => {
74
+ it("runs a mutation and returns expected result", async () => {
75
+ const ops = operations.mutation("createUser", async (user: { name: string }) => {
76
+ return { id: "1", ...user }
77
+ })
78
+
79
+ const { result } = renderHook(
80
+ () => ops.useMutation("createUser"),
81
+ { wrapper: createWrapper(queryClient) }
82
+ )
83
+
84
+ let response: any
85
+ await act(async () => {
86
+ response = await result.current.mutateAsync({ name: "John" })
87
+ })
88
+
89
+ expect(response).toEqual({ id: "1", name: "John" })
90
+ })
91
+ })
92
+
93
+ describe("useQuery hook", () => {
94
+ it("fetches query result successfully", async () => {
95
+ const ops = operations.query("getUser", async (id: string) => {
96
+ return { id, name: "Alice" }
97
+ })
98
+
99
+ const { result } = renderHook(
100
+ () => ops.useQuery("getUser", "42", { enabled: true }),
101
+ { wrapper: createWrapper(queryClient) }
102
+ )
103
+
104
+ await waitFor(() => {
105
+ expect(result.current.data).toEqual({ id: "42", name: "Alice" })
106
+ })
107
+ })
108
+ })
109
+ })
@@ -0,0 +1,551 @@
1
+ import React from 'react'
2
+ import { describe, it, expect, beforeEach, jest } from 'bun:test'
3
+ import { renderHook, waitFor, act } from '@testing-library/react'
4
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
5
+ import { createQueryManager } from '../factors/createQueryManager'
6
+ import { createTestQueryClient, TestUser, TestUserFilters, createMockUsers, createMockApiFunctions } from './setup'
7
+
8
+ const createWrapper = (queryClient: QueryClient) => {
9
+ return ({ children }: { children: React.ReactNode }) => (
10
+ <QueryClientProvider client={queryClient}>
11
+ {children}
12
+ </QueryClientProvider>
13
+ )
14
+ }
15
+
16
+ describe('Integration Tests', () => {
17
+ let queryClient: ReturnType<typeof createTestQueryClient>
18
+ let mockApi: ReturnType<typeof createMockApiFunctions>
19
+ let queryManager: ReturnType<typeof createQueryManager<TestUser, TestUserFilters>>
20
+
21
+ beforeEach(() => {
22
+ queryClient = createTestQueryClient()
23
+ mockApi = createMockApiFunctions()
24
+
25
+ queryManager = createQueryManager<TestUser, TestUserFilters>({
26
+ name: 'users',
27
+ queryClient,
28
+ listFn: mockApi.listFn,
29
+ retrieveFn: mockApi.retrieveFn,
30
+ createFn: mockApi.createFn,
31
+ updateFn: mockApi.updateFn,
32
+ deleteFn: mockApi.deleteFn,
33
+ listLimit: 5
34
+ })
35
+ })
36
+
37
+ describe('Complete CRUD Workflow', () => {
38
+ it('should handle complete user lifecycle', async () => {
39
+ // Start with some existing users
40
+ const initialUsers = createMockUsers(3)
41
+ mockApi.setUsers(initialUsers)
42
+
43
+ // 1. Load initial list
44
+ const { result: listResult } = renderHook(
45
+ () => queryManager.useList(),
46
+ { wrapper: createWrapper(queryClient) }
47
+ )
48
+
49
+ await waitFor(() => {
50
+ expect(listResult.current.query.isSuccess).toBe(true)
51
+ })
52
+
53
+ expect(listResult.current.items).toHaveLength(3)
54
+
55
+ // 2. Retrieve specific user
56
+ const targetUser = listResult.current.items[0]
57
+ const { result: retrieveResult } = renderHook(
58
+ () => queryManager.useRetrieve(targetUser.id),
59
+ { wrapper: createWrapper(queryClient) }
60
+ )
61
+
62
+ await waitFor(() => {
63
+ expect(retrieveResult.current.query.isSuccess).toBe(true)
64
+ })
65
+
66
+ expect(retrieveResult.current.item).toEqual(targetUser)
67
+
68
+ // 3. Create new user
69
+ const { result: createResult } = renderHook(
70
+ () => queryManager.useCreate({ optimistic: true }),
71
+ { wrapper: createWrapper(queryClient) }
72
+ )
73
+
74
+ act(() => {
75
+ createResult.current.mutate({
76
+ name: 'New User',
77
+ email: 'new@example.com',
78
+ status: 'active'
79
+ })
80
+ })
81
+
82
+ await waitFor(() => {
83
+ expect(createResult.current.isSuccess).toBe(true)
84
+ })
85
+
86
+ expect(listResult.current.items).toHaveLength(4)
87
+ expect(listResult.current.items[0].name).toBe('New User')
88
+
89
+ // 4. Update existing user
90
+ const { result: updateResult } = renderHook(
91
+ () => queryManager.useUpdate({ optimistic: true }),
92
+ { wrapper: createWrapper(queryClient) }
93
+ )
94
+
95
+ act(() => {
96
+ updateResult.current.mutate({
97
+ id: targetUser.id,
98
+ name: 'Updated Name',
99
+ status: 'inactive'
100
+ })
101
+ })
102
+
103
+ await waitFor(() => {
104
+ expect(updateResult.current.isSuccess).toBe(true)
105
+ })
106
+
107
+ expect(retrieveResult.current.item?.name).toBe('Updated Name')
108
+ expect(retrieveResult.current.item?.status).toBe('inactive')
109
+
110
+ // 5. Delete user
111
+ const { result: deleteResult } = renderHook(
112
+ () => queryManager.useDelete({ optimistic: true }),
113
+ { wrapper: createWrapper(queryClient) }
114
+ )
115
+
116
+ const userToDelete = listResult.current.items[1]
117
+
118
+ act(() => {
119
+ deleteResult.current.mutate(userToDelete.id)
120
+ })
121
+
122
+ await waitFor(() => {
123
+ expect(deleteResult.current.isSuccess).toBe(true)
124
+ })
125
+
126
+ expect(listResult.current.items).toHaveLength(3)
127
+ expect(listResult.current.items.find(u => u.id === userToDelete.id)).toBeUndefined()
128
+ })
129
+ })
130
+
131
+ describe('Pagination and Infinite Scroll', () => {
132
+ beforeEach(() => {
133
+ const manyUsers = createMockUsers(25)
134
+ mockApi.setUsers(manyUsers)
135
+ })
136
+
137
+ it('should handle pagination correctly', async () => {
138
+ const { result } = renderHook(
139
+ () => queryManager.useList({ limit: 5 }),
140
+ { wrapper: createWrapper(queryClient) }
141
+ )
142
+
143
+ await waitFor(() => {
144
+ expect(result.current.query.isSuccess).toBe(true)
145
+ })
146
+
147
+ // First page
148
+ expect(result.current.items).toHaveLength(5)
149
+ expect(result.current.query.hasNextPage).toBe(true)
150
+
151
+ // Load next page
152
+ act(() => {
153
+ result.current.query.fetchNextPage()
154
+ })
155
+
156
+ await waitFor(() => {
157
+ expect(result.current.items).toHaveLength(10)
158
+ })
159
+
160
+ // Load more pages
161
+ act(() => {
162
+ result.current.query.fetchNextPage()
163
+ })
164
+
165
+ await waitFor(() => {
166
+ expect(result.current.items).toHaveLength(15)
167
+ })
168
+
169
+ // Continue until all pages loaded
170
+ while (result.current.query.hasNextPage) {
171
+ act(() => {
172
+ result.current.query.fetchNextPage()
173
+ })
174
+
175
+ await waitFor(() => {
176
+ expect(result.current.query.isFetchingNextPage).toBe(false)
177
+ })
178
+ }
179
+
180
+ expect(result.current.items).toHaveLength(25)
181
+ expect(result.current.query.hasNextPage).toBe(false)
182
+ })
183
+
184
+ it('should maintain pagination after mutations', async () => {
185
+ const { result: listResult } = renderHook(
186
+ () => queryManager.useList({ limit: 5 }),
187
+ { wrapper: createWrapper(queryClient) }
188
+ )
189
+
190
+ await waitFor(() => {
191
+ expect(listResult.current.query.isSuccess).toBe(true)
192
+ })
193
+
194
+ // Load multiple pages
195
+ act(() => {
196
+ listResult.current.query.fetchNextPage()
197
+ })
198
+
199
+ await waitFor(() => {
200
+ expect(listResult.current.items).toHaveLength(10)
201
+ })
202
+
203
+ // Create new item
204
+ const { result: createResult } = renderHook(
205
+ () => queryManager.useCreate({ optimistic: true }),
206
+ { wrapper: createWrapper(queryClient) }
207
+ )
208
+
209
+ act(() => {
210
+ createResult.current.mutate({
211
+ name: 'Paginated New User',
212
+ email: 'paginated@example.com',
213
+ status: 'active'
214
+ })
215
+ })
216
+
217
+ await waitFor(() => {
218
+ expect(createResult.current.isSuccess).toBe(true)
219
+ })
220
+
221
+ // Should maintain pagination structure
222
+ expect(listResult.current.items).toHaveLength(11) // +1 new item
223
+ expect(listResult.current.items[0].name).toBe('Paginated New User')
224
+ })
225
+ })
226
+
227
+ describe('Filtering and Search', () => {
228
+ beforeEach(() => {
229
+ const users = [
230
+ ...createMockUsers(5).map(u => ({ ...u, status: 'active' as const })),
231
+ ...createMockUsers(3).map(u => ({ ...u, status: 'inactive' as const }))
232
+ ]
233
+ mockApi.setUsers(users)
234
+ })
235
+
236
+ it('should filter list results correctly', async () => {
237
+ const { result: activeResult } = renderHook(
238
+ () => queryManager.useList({ filters: { status: 'active' } }),
239
+ { wrapper: createWrapper(queryClient) }
240
+ )
241
+
242
+ const { result: inactiveResult } = renderHook(
243
+ () => queryManager.useList({ filters: { status: 'inactive' } }),
244
+ { wrapper: createWrapper(queryClient) }
245
+ )
246
+
247
+ await waitFor(() => {
248
+ expect(activeResult.current.query.isSuccess).toBe(true)
249
+ expect(inactiveResult.current.query.isSuccess).toBe(true)
250
+ })
251
+
252
+ expect(activeResult.current.items).toHaveLength(5)
253
+ expect(inactiveResult.current.items).toHaveLength(3)
254
+ expect(activeResult.current.items.every(u => u.status === 'active')).toBe(true)
255
+ expect(inactiveResult.current.items.every(u => u.status === 'inactive')).toBe(true)
256
+ })
257
+
258
+ it('should handle mutations with filtered lists', async () => {
259
+ const { result: activeListResult } = renderHook(
260
+ () => queryManager.useList({ filters: { status: 'active' } }),
261
+ { wrapper: createWrapper(queryClient) }
262
+ )
263
+
264
+ await waitFor(() => {
265
+ expect(activeListResult.current.query.isSuccess).toBe(true)
266
+ })
267
+
268
+ // Create with matching filter
269
+ const { result: createResult } = renderHook(
270
+ () => queryManager.useCreate({
271
+ optimistic: true,
272
+ listFilters: { status: 'active' }
273
+ }),
274
+ { wrapper: createWrapper(queryClient) }
275
+ )
276
+
277
+ act(() => {
278
+ createResult.current.mutate({
279
+ name: 'New Active User',
280
+ email: 'active@example.com',
281
+ status: 'active'
282
+ })
283
+ })
284
+
285
+ await waitFor(() => {
286
+ expect(createResult.current.isSuccess).toBe(true)
287
+ })
288
+
289
+ expect(activeListResult.current.items).toHaveLength(6)
290
+ expect(activeListResult.current.items[0].name).toBe('New Active User')
291
+ })
292
+
293
+ it('should handle search filtering', async () => {
294
+ const users = createMockUsers(10).map((user, index) => ({
295
+ ...user,
296
+ name: index % 2 === 0 ? `John ${index}` : `Jane ${index}`,
297
+ email: index % 2 === 0 ? `john${index}@example.com` : `jane${index}@example.com`
298
+ }))
299
+ mockApi.setUsers(users)
300
+
301
+ const { result } = renderHook(
302
+ () => queryManager.useList({ filters: { search: 'John' } }),
303
+ { wrapper: createWrapper(queryClient) }
304
+ )
305
+
306
+ await waitFor(() => {
307
+ expect(result.current.query.isSuccess).toBe(true)
308
+ })
309
+
310
+ expect(result.current.items.every(u => u.name.includes('John'))).toBe(true)
311
+ })
312
+ })
313
+
314
+ describe('Cache Synchronization', () => {
315
+ beforeEach(() => {
316
+ const users = createMockUsers(10)
317
+ mockApi.setUsers(users)
318
+ })
319
+
320
+ it('should keep list and retrieve caches synchronized', async () => {
321
+ // Load list first
322
+ const { result: listResult } = renderHook(
323
+ () => queryManager.useList(),
324
+ { wrapper: createWrapper(queryClient) }
325
+ )
326
+
327
+ await waitFor(() => {
328
+ expect(listResult.current.query.isSuccess).toBe(true)
329
+ })
330
+
331
+ const targetUser = listResult.current.items[2]
332
+
333
+ // Load specific user
334
+ const { result: retrieveResult } = renderHook(
335
+ () => queryManager.useRetrieve(targetUser.id),
336
+ { wrapper: createWrapper(queryClient) }
337
+ )
338
+
339
+ await waitFor(() => {
340
+ expect(retrieveResult.current.query.isSuccess).toBe(true)
341
+ })
342
+
343
+ // Update the user through retrieve cache
344
+ const { result: updateResult } = renderHook(
345
+ () => queryManager.useUpdate({ optimistic: true }),
346
+ { wrapper: createWrapper(queryClient) }
347
+ )
348
+
349
+ act(() => {
350
+ updateResult.current.mutate({
351
+ id: targetUser.id,
352
+ name: 'Synchronized Update'
353
+ })
354
+ })
355
+
356
+ await waitFor(() => {
357
+ expect(updateResult.current.isSuccess).toBe(true)
358
+ })
359
+
360
+ // Both caches should reflect the change
361
+ expect(retrieveResult.current.item?.name).toBe('Synchronized Update')
362
+ expect(listResult.current.items.find(u => u.id === targetUser.id)?.name).toBe('Synchronized Update')
363
+ })
364
+
365
+ it('should handle multiple filtered lists correctly', async () => {
366
+ const users = [
367
+ { id: '1', name: 'Active User 1', email: 'a1@test.com', status: 'active' as const, createdAt: '2024-01-01' },
368
+ { id: '2', name: 'Active User 2', email: 'a2@test.com', status: 'active' as const, createdAt: '2024-01-02' },
369
+ { id: '3', name: 'Inactive User', email: 'i1@test.com', status: 'inactive' as const, createdAt: '2024-01-03' }
370
+ ]
371
+ mockApi.setUsers(users)
372
+
373
+ // Load different filtered lists
374
+ const { result: allListResult } = renderHook(
375
+ () => queryManager.useList(),
376
+ { wrapper: createWrapper(queryClient) }
377
+ )
378
+
379
+ const { result: activeListResult } = renderHook(
380
+ () => queryManager.useList({ filters: { status: 'active' } }),
381
+ { wrapper: createWrapper(queryClient) }
382
+ )
383
+
384
+ await waitFor(() => {
385
+ expect(allListResult.current.query.isSuccess).toBe(true)
386
+ expect(activeListResult.current.query.isSuccess).toBe(true)
387
+ })
388
+
389
+ // Update a user
390
+ const { result: updateResult } = renderHook(
391
+ () => queryManager.useUpdate({ optimistic: true }),
392
+ { wrapper: createWrapper(queryClient) }
393
+ )
394
+
395
+ act(() => {
396
+ updateResult.current.mutate({
397
+ id: '1',
398
+ status: 'inactive'
399
+ })
400
+ })
401
+
402
+ await waitFor(() => {
403
+ expect(updateResult.current.isSuccess).toBe(true)
404
+ })
405
+
406
+ // All lists should be updated
407
+ const updatedUserInAll = allListResult.current.items.find(u => u.id === '1')
408
+ const updatedUserInActive = activeListResult.current.items.find(u => u.id === '1')
409
+
410
+ expect(updatedUserInAll?.status).toBe('inactive')
411
+ expect(updatedUserInActive?.status).toBe('inactive')
412
+ })
413
+ })
414
+
415
+ describe('Performance and Memory', () => {
416
+ it('should handle large datasets efficiently', async () => {
417
+ // Create a large dataset
418
+ const largeDataset = createMockUsers(1000)
419
+ mockApi.setUsers(largeDataset)
420
+
421
+ const { result } = renderHook(
422
+ () => queryManager.useList({ limit: 50 }),
423
+ { wrapper: createWrapper(queryClient) }
424
+ )
425
+
426
+ await waitFor(() => {
427
+ expect(result.current.query.isSuccess).toBe(true)
428
+ })
429
+
430
+ // Should load only the requested amount
431
+ expect(result.current.items).toHaveLength(50)
432
+
433
+ // Load next page efficiently
434
+ const startTime = performance.now()
435
+
436
+ act(() => {
437
+ result.current.query.fetchNextPage()
438
+ })
439
+
440
+ await waitFor(() => {
441
+ expect(result.current.items).toHaveLength(100)
442
+ })
443
+
444
+ const endTime = performance.now()
445
+ const loadTime = endTime - startTime
446
+
447
+ // Should be reasonably fast (less than 100ms)
448
+ expect(loadTime).toBeLessThan(100)
449
+ })
450
+
451
+ it('should properly clean up resources', async () => {
452
+ const { result, unmount } = renderHook(
453
+ () => queryManager.useList(),
454
+ { wrapper: createWrapper(queryClient) }
455
+ )
456
+
457
+ await waitFor(() => {
458
+ expect(result.current.query.isSuccess).toBe(true)
459
+ })
460
+
461
+ // Unmount component
462
+ unmount()
463
+
464
+ // Query should still be in cache but can be garbage collected
465
+ expect(queryClient.getQueryCache().getAll()).toHaveLength(1)
466
+ })
467
+ })
468
+
469
+ describe('Real-world Scenarios', () => {
470
+ it('should handle concurrent user sessions', async () => {
471
+ const users = createMockUsers(5)
472
+ mockApi.setUsers(users)
473
+
474
+ // Simulate two different user sessions
475
+ const queryClient1 = createTestQueryClient()
476
+ const queryClient2 = createTestQueryClient()
477
+
478
+ const queryManager1 = createQueryManager<TestUser, TestUserFilters>({
479
+ name: 'users',
480
+ queryClient: queryClient1,
481
+ listFn: mockApi.listFn,
482
+ retrieveFn: mockApi.retrieveFn,
483
+ createFn: mockApi.createFn,
484
+ updateFn: mockApi.updateFn,
485
+ deleteFn: mockApi.deleteFn
486
+ })
487
+
488
+ const queryManager2 = createQueryManager<TestUser, TestUserFilters>({
489
+ name: 'users',
490
+ queryClient: queryClient2,
491
+ listFn: mockApi.listFn,
492
+ retrieveFn: mockApi.retrieveFn,
493
+ createFn: mockApi.createFn,
494
+ updateFn: mockApi.updateFn,
495
+ deleteFn: mockApi.deleteFn
496
+ })
497
+
498
+ // Load data in both sessions
499
+ const { result: session1Result } = renderHook(
500
+ () => queryManager1.useList(),
501
+ { wrapper: createWrapper(queryClient1) }
502
+ )
503
+
504
+ const { result: session2Result } = renderHook(
505
+ () => queryManager2.useList(),
506
+ { wrapper: createWrapper(queryClient2) }
507
+ )
508
+
509
+ await waitFor(() => {
510
+ expect(session1Result.current.query.isSuccess).toBe(true)
511
+ expect(session2Result.current.query.isSuccess).toBe(true)
512
+ })
513
+
514
+ // Both sessions should have independent caches
515
+ expect(session1Result.current.items).toEqual(session2Result.current.items)
516
+
517
+ // Create in session 1
518
+ const { result: createResult } = renderHook(
519
+ () => queryManager1.useCreate({ optimistic: true }),
520
+ { wrapper: createWrapper(queryClient1) }
521
+ )
522
+
523
+ act(() => {
524
+ createResult.current.mutate({
525
+ name: 'Session 1 User',
526
+ email: 'session1@example.com',
527
+ status: 'active'
528
+ })
529
+ })
530
+
531
+ await waitFor(() => {
532
+ expect(createResult.current.isSuccess).toBe(true)
533
+ })
534
+
535
+ // Session 1 should see the new user
536
+ expect(session1Result.current.items.some(u => u.name === 'Session 1 User')).toBe(true)
537
+
538
+ // Session 2 should not see it until refetch
539
+ expect(session2Result.current.items.some(u => u.name === 'Session 1 User')).toBe(false)
540
+
541
+ // Refetch in session 2
542
+ act(() => {
543
+ session2Result.current.query.refetch()
544
+ })
545
+
546
+ await waitFor(() => {
547
+ expect(session2Result.current.items.some(u => u.name === 'Session 1 User')).toBe(true)
548
+ })
549
+ })
550
+ })
551
+ })