@codeleap/query 5.8.3 → 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,920 @@
1
+ import { describe, it, expect, beforeEach, jest, spyOn } from 'bun:test'
2
+ import { renderHook, waitFor } from '@testing-library/react'
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
4
+ import React from 'react'
5
+ import { QueryManager } from '../lib/QueryManager'
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('QueryManager', () => {
17
+ let queryClient: ReturnType<typeof createTestQueryClient>
18
+ let mockApi: ReturnType<typeof createMockApiFunctions>
19
+ let queryManager: QueryManager<TestUser, TestUserFilters>
20
+
21
+ beforeEach(() => {
22
+ queryClient = createTestQueryClient()
23
+ mockApi = createMockApiFunctions()
24
+
25
+ queryManager = new QueryManager({
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: 10
34
+ })
35
+ })
36
+
37
+ describe('constructor', () => {
38
+ it('should create QueryManager with correct configuration', () => {
39
+ expect(queryManager.name).toBe('users')
40
+ expect(queryManager.functions).toEqual({
41
+ list: mockApi.listFn,
42
+ retrieve: mockApi.retrieveFn,
43
+ create: mockApi.createFn,
44
+ update: mockApi.updateFn,
45
+ delete: mockApi.deleteFn
46
+ })
47
+ expect(queryManager.queryKeys).toBeDefined()
48
+ expect(queryManager.mutations).toBeDefined()
49
+ })
50
+ })
51
+
52
+ describe('useList', () => {
53
+ beforeEach(() => {
54
+ const mockUsers = createMockUsers(25) // More than one page
55
+ mockApi.setUsers(mockUsers)
56
+ })
57
+
58
+ it('should fetch and return list items', async () => {
59
+ const { result } = renderHook(
60
+ () => queryManager.useList(),
61
+ { wrapper: createWrapper(queryClient) }
62
+ )
63
+
64
+ await waitFor(() => {
65
+ expect(result.current.query.isSuccess).toBe(true)
66
+ })
67
+
68
+ expect(result.current.items).toHaveLength(10) // Default limit
69
+ expect(result.current.queryKey).toEqual(['users', 'list'])
70
+ })
71
+
72
+ it('should use custom limit', async () => {
73
+ const { result } = renderHook(
74
+ () => queryManager.useList({ limit: 5 }),
75
+ { wrapper: createWrapper(queryClient) }
76
+ )
77
+
78
+ await waitFor(() => {
79
+ expect(result.current.query.isSuccess).toBe(true)
80
+ })
81
+
82
+ expect(result.current.items).toHaveLength(5)
83
+ })
84
+
85
+ it('should apply filters', async () => {
86
+ const activeUsers = createMockUsers(3).map(user => ({ ...user, status: 'active' as const }))
87
+ const inactiveUsers = createMockUsers(2).map(user => ({ ...user, status: 'inactive' as const }))
88
+ mockApi.setUsers([...activeUsers, ...inactiveUsers])
89
+
90
+ const { result } = renderHook(
91
+ () => queryManager.useList({ filters: { status: 'active' } }),
92
+ { wrapper: createWrapper(queryClient) }
93
+ )
94
+
95
+ await waitFor(() => {
96
+ expect(result.current.query.isSuccess).toBe(true)
97
+ })
98
+
99
+ expect(result.current.items).toHaveLength(3)
100
+ expect(result.current.items.every(user => user.status === 'active')).toBe(true)
101
+ })
102
+
103
+ it('should handle infinite scroll pagination', async () => {
104
+ const { result } = renderHook(
105
+ () => queryManager.useList({ limit: 5 }),
106
+ { wrapper: createWrapper(queryClient) }
107
+ )
108
+
109
+ await waitFor(() => {
110
+ expect(result.current.query.isSuccess).toBe(true)
111
+ })
112
+
113
+ expect(result.current.items).toHaveLength(5)
114
+ expect(result.current.query.hasNextPage).toBe(true)
115
+
116
+ // Fetch next page
117
+ result.current.query.fetchNextPage()
118
+
119
+ await waitFor(() => {
120
+ expect(result.current.items).toHaveLength(10)
121
+ })
122
+ })
123
+
124
+ it('should call useListEffect if provided', async () => {
125
+ const useListEffect = jest.fn()
126
+ const queryManagerWithEffect = new QueryManager({
127
+ name: 'users',
128
+ queryClient,
129
+ listFn: mockApi.listFn,
130
+ retrieveFn: mockApi.retrieveFn,
131
+ createFn: mockApi.createFn,
132
+ updateFn: mockApi.updateFn,
133
+ deleteFn: mockApi.deleteFn,
134
+ useListEffect
135
+ })
136
+
137
+ renderHook(
138
+ () => queryManagerWithEffect.useList(),
139
+ { wrapper: createWrapper(queryClient) }
140
+ )
141
+
142
+ await waitFor(() => {
143
+ expect(useListEffect).toHaveBeenCalled()
144
+ })
145
+ })
146
+
147
+ it('should handle empty results', async () => {
148
+ mockApi.setUsers([])
149
+
150
+ const { result } = renderHook(
151
+ () => queryManager.useList(),
152
+ { wrapper: createWrapper(queryClient) }
153
+ )
154
+
155
+ await waitFor(() => {
156
+ expect(result.current.query.isSuccess).toBe(true)
157
+ })
158
+
159
+ expect(result.current.items).toEqual([])
160
+ })
161
+
162
+ it('should determine next page correctly', async () => {
163
+ const { result } = renderHook(
164
+ () => queryManager.useList({ limit: 10 }),
165
+ { wrapper: createWrapper(queryClient) }
166
+ )
167
+
168
+ await waitFor(() => {
169
+ expect(result.current.query.isSuccess).toBe(true)
170
+ })
171
+
172
+ // Has next page when more items available
173
+ expect(result.current.query.hasNextPage).toBe(true)
174
+
175
+ // Fetch all pages
176
+ while (result.current.query.hasNextPage) {
177
+ result.current.query.fetchNextPage()
178
+ await waitFor(() => {
179
+ expect(result.current.query.isFetchingNextPage).toBe(false)
180
+ })
181
+ }
182
+
183
+ expect(result.current.query.hasNextPage).toBe(false)
184
+ })
185
+ })
186
+
187
+ describe('useRetrieve', () => {
188
+ beforeEach(() => {
189
+ const mockUsers = createMockUsers(5)
190
+ mockApi.setUsers(mockUsers)
191
+ })
192
+
193
+ it('should fetch and return single item', async () => {
194
+ const users = mockApi.getUsersArray()
195
+ const targetUser = users[0]
196
+
197
+ const { result } = renderHook(
198
+ () => queryManager.useRetrieve(targetUser.id),
199
+ { wrapper: createWrapper(queryClient) }
200
+ )
201
+
202
+ await waitFor(() => {
203
+ expect(result.current.query.isSuccess).toBe(true)
204
+ })
205
+
206
+ expect(result.current.item).toEqual(targetUser)
207
+ expect(result.current.queryKey).toEqual(['users', 'retrieve', targetUser.id])
208
+ })
209
+
210
+ it('should use initial data from list cache', async () => {
211
+ const users = createMockUsers(3)
212
+ mockApi.setUsers(users)
213
+
214
+ // First, populate list cache
215
+ const { result: listResult } = renderHook(
216
+ () => queryManager.useList(),
217
+ { wrapper: createWrapper(queryClient) }
218
+ )
219
+
220
+ await waitFor(() => {
221
+ expect(listResult.current.query.isSuccess).toBe(true)
222
+ })
223
+
224
+ // Then retrieve specific item - should use cache as initial data
225
+ const { result: retrieveResult } = renderHook(
226
+ () => queryManager.useRetrieve(users[0].id),
227
+ { wrapper: createWrapper(queryClient) }
228
+ )
229
+
230
+ // Should have initial data immediately
231
+ expect(retrieveResult.current.item).toEqual(users[0])
232
+ })
233
+
234
+ it('should handle item not found error', async () => {
235
+ const { result } = renderHook(
236
+ () => queryManager.useRetrieve('non-existent-id'),
237
+ { wrapper: createWrapper(queryClient) }
238
+ )
239
+
240
+ await waitFor(() => {
241
+ expect(result.current.query.isError).toBe(true)
242
+ })
243
+
244
+ expect(result.current.item).toBeUndefined()
245
+ expect(result.current.query.error).toEqual(new Error('User not found'))
246
+ })
247
+
248
+ it('should call custom select function', async () => {
249
+ const users = mockApi.getUsersArray()
250
+ const targetUser = users[0]
251
+ const customSelect = jest.fn((user: TestUser) => ({ ...user, selected: true }))
252
+
253
+ const { result } = renderHook(
254
+ () => queryManager.useRetrieve(targetUser.id, { select: customSelect }),
255
+ { wrapper: createWrapper(queryClient) }
256
+ )
257
+
258
+ await waitFor(() => {
259
+ expect(result.current.query.isSuccess).toBe(true)
260
+ })
261
+
262
+ expect(customSelect).toHaveBeenCalledWith(targetUser)
263
+ })
264
+
265
+ it('should update list cache when retrieve data changes', async () => {
266
+ const users = createMockUsers(3)
267
+ mockApi.setUsers(users)
268
+
269
+ // Populate list cache first
270
+ const { result: listResult } = renderHook(
271
+ () => queryManager.useList(),
272
+ { wrapper: createWrapper(queryClient) }
273
+ )
274
+
275
+ await waitFor(() => {
276
+ expect(listResult.current.query.isSuccess).toBe(true)
277
+ })
278
+
279
+ // Update a user via API BEFORE retrieving
280
+ const updatedUser = { ...users[0], name: 'Updated Name' }
281
+ mockApi.setUsers([updatedUser, ...users.slice(1)])
282
+
283
+ await queryManager.queryKeys.refetchList()
284
+
285
+ // Retrieve the updated user
286
+ const { result: retrieveResult } = renderHook(
287
+ () => queryManager.useRetrieve(users[0].id),
288
+ { wrapper: createWrapper(queryClient) }
289
+ )
290
+
291
+ await waitFor(() => {
292
+ expect(retrieveResult.current.query.isSuccess).toBe(true)
293
+ })
294
+
295
+ expect(retrieveResult.current.item).toEqual(updatedUser)
296
+ })
297
+ })
298
+
299
+ describe('useCreate', () => {
300
+ beforeEach(() => {
301
+ mockApi.setUsers(createMockUsers(3))
302
+ })
303
+
304
+ it('should create new item', async () => {
305
+ const { result } = renderHook(
306
+ () => queryManager.useCreate(),
307
+ { wrapper: createWrapper(queryClient) }
308
+ )
309
+
310
+ const newUserData = { name: 'New User', email: 'new@example.com', status: 'active' as const }
311
+
312
+ result.current.mutate(newUserData)
313
+
314
+ await waitFor(() => {
315
+ expect(result.current.isSuccess).toBe(true)
316
+ })
317
+
318
+ expect(result.current.data).toMatchObject(newUserData)
319
+ expect(mockApi.getUsersArray()).toHaveLength(4)
320
+ })
321
+
322
+ it('should handle optimistic updates', async () => {
323
+ // First, populate list cache
324
+ const { result: listResult } = renderHook(
325
+ () => queryManager.useList(),
326
+ { wrapper: createWrapper(queryClient) }
327
+ )
328
+
329
+ await waitFor(() => {
330
+ expect(listResult.current.query.isSuccess).toBe(true)
331
+ })
332
+
333
+ const { result: createResult } = renderHook(
334
+ () => queryManager.useCreate({ optimistic: true }),
335
+ { wrapper: createWrapper(queryClient) }
336
+ )
337
+
338
+ const newUserData = { name: 'New User', email: 'new@example.com', status: 'active' as const }
339
+
340
+ createResult.current.mutate(newUserData)
341
+
342
+ // Should immediately appear in list cache
343
+ await waitFor(() => {
344
+ expect(listResult.current.items.some(item => item.name === 'New User')).toBe(true)
345
+ })
346
+
347
+ await waitFor(() => {
348
+ expect(createResult.current.isSuccess).toBe(true)
349
+ })
350
+
351
+ // Should replace temp item with real data
352
+ expect(listResult.current.items.some(item =>
353
+ item.name === 'New User' && !item.id.startsWith('temp-')
354
+ )).toBe(true)
355
+ })
356
+
357
+ // it('should rollback optimistic update on error', async () => {
358
+ // // Mock API to throw error BEFORE creating hook
359
+ // const originalCreateFn = mockApi.createFn
360
+ // mockApi.createFn = jest.fn().mockRejectedValue(new Error('Creation failed'))
361
+
362
+ // // First, populate list cache
363
+ // const { result: listResult } = renderHook(
364
+ // () => queryManager.useList(),
365
+ // { wrapper: createWrapper(queryClient) }
366
+ // )
367
+
368
+ // await waitFor(() => {
369
+ // expect(listResult.current.query.isSuccess).toBe(true)
370
+ // })
371
+
372
+ // const initialItemCount = listResult.current.items.length
373
+
374
+ // const { result: createResult } = renderHook(
375
+ // () => queryManager.useCreate({
376
+ // optimistic: true,
377
+ // onError: (error) => {
378
+ // // Handle error silently for test
379
+ // console.log('Expected error:', error.message)
380
+ // }
381
+ // }),
382
+ // { wrapper: createWrapper(queryClient) }
383
+ // )
384
+
385
+ // const newUserData = { name: 'New User', email: 'new@example.com', status: 'active' as const }
386
+
387
+ // await createResult.current.mutateAsync(newUserData)
388
+
389
+ // // Should temporarily appear in list
390
+ // await waitFor(() => {
391
+ // expect(listResult.current.items.length).toBeGreaterThan(initialItemCount)
392
+ // })
393
+
394
+ // // Should be removed on error
395
+ // await waitFor(() => {
396
+ // expect(createResult.current.isError).toBe(true)
397
+ // })
398
+
399
+ // expect(listResult.current.items).toHaveLength(initialItemCount)
400
+
401
+ // // Restore original function
402
+ // mockApi.createFn = originalCreateFn
403
+ // })
404
+
405
+ it('should call custom mutation callbacks', async () => {
406
+ const onMutate = jest.fn()
407
+ const onSuccess = jest.fn()
408
+ const onError = jest.fn()
409
+
410
+ const { result } = renderHook(
411
+ () => queryManager.useCreate({ onMutate, onSuccess, onError }),
412
+ { wrapper: createWrapper(queryClient) }
413
+ )
414
+
415
+ const newUserData = { name: 'New User', email: 'new@example.com', status: 'active' as const }
416
+
417
+ await result.current.mutateAsync(newUserData)
418
+
419
+ await waitFor(() => {
420
+ expect(result.current.isSuccess).toBe(true)
421
+ })
422
+
423
+ expect(onMutate).toHaveBeenCalled()
424
+ expect(onSuccess).toHaveBeenCalled()
425
+ expect(onError).not.toHaveBeenCalled()
426
+ })
427
+
428
+ it('should append to start by default', async () => {
429
+ // First, populate list cache
430
+ const { result: listResult } = renderHook(
431
+ () => queryManager.useList(),
432
+ { wrapper: createWrapper(queryClient) }
433
+ )
434
+
435
+ await waitFor(() => {
436
+ expect(listResult.current.query.isSuccess).toBe(true)
437
+ })
438
+
439
+ const { result: createResult } = renderHook(
440
+ () => queryManager.useCreate({ optimistic: true }),
441
+ { wrapper: createWrapper(queryClient) }
442
+ )
443
+
444
+ const newUserData = { name: 'New User', email: 'new@example.com', status: 'active' as const }
445
+
446
+ createResult.current.mutate(newUserData)
447
+
448
+ await waitFor(() => {
449
+ expect(listResult.current.items[0].name).toBe('New User')
450
+ })
451
+ })
452
+
453
+ it('should append to end when specified', async () => {
454
+ // First, populate list cache
455
+ const { result: listResult } = renderHook(
456
+ () => queryManager.useList(),
457
+ { wrapper: createWrapper(queryClient) }
458
+ )
459
+
460
+ await waitFor(() => {
461
+ expect(listResult.current.query.isSuccess).toBe(true)
462
+ })
463
+
464
+ const { result: createResult } = renderHook(
465
+ () => queryManager.useCreate({ optimistic: true, appendTo: 'end' }),
466
+ { wrapper: createWrapper(queryClient) }
467
+ )
468
+
469
+ const newUserData = { name: 'New User', email: 'new@example.com', status: 'active' as const }
470
+
471
+ createResult.current.mutate(newUserData)
472
+
473
+ await waitFor(() => {
474
+ const items = listResult.current.items
475
+ expect(items[items.length - 1].name).toBe('New User')
476
+ })
477
+ })
478
+
479
+ it('should handle list filters', async () => {
480
+ // First, populate filtered list cache
481
+ const { result: listResult } = renderHook(
482
+ () => queryManager.useList({ filters: { status: 'active' } }),
483
+ { wrapper: createWrapper(queryClient) }
484
+ )
485
+
486
+ await waitFor(() => {
487
+ expect(listResult.current.query.isSuccess).toBe(true)
488
+ })
489
+
490
+ const { result: createResult } = renderHook(
491
+ () => queryManager.useCreate({
492
+ optimistic: true,
493
+ listFilters: { status: 'active' }
494
+ }),
495
+ { wrapper: createWrapper(queryClient) }
496
+ )
497
+
498
+ const newUserData = { name: 'New User', email: 'new@example.com', status: 'active' as const }
499
+
500
+ createResult.current.mutate(newUserData)
501
+
502
+ await waitFor(() => {
503
+ expect(listResult.current.items.some(item => item.name === 'New User')).toBe(true)
504
+ })
505
+ })
506
+ })
507
+
508
+ describe('useUpdate', () => {
509
+ let existingUsers: TestUser[]
510
+
511
+ beforeEach(() => {
512
+ existingUsers = createMockUsers(3)
513
+ mockApi.setUsers(existingUsers)
514
+ })
515
+
516
+ it('should update existing item', async () => {
517
+ const { result } = renderHook(
518
+ () => queryManager.useUpdate(),
519
+ { wrapper: createWrapper(queryClient) }
520
+ )
521
+
522
+ const updateData = { id: existingUsers[0].id, name: 'Updated Name' }
523
+
524
+ result.current.mutate(updateData)
525
+
526
+ await waitFor(() => {
527
+ expect(result.current.isSuccess).toBe(true)
528
+ })
529
+
530
+ expect(result.current.data).toMatchObject(updateData)
531
+ const updatedUsers = mockApi.getUsersArray()
532
+ expect(updatedUsers.find(u => u.id === existingUsers[0].id)?.name).toBe('Updated Name')
533
+ })
534
+
535
+ it('should handle optimistic updates', async () => {
536
+ // First, populate retrieve cache
537
+ const { result: retrieveResult } = renderHook(
538
+ () => queryManager.useRetrieve(existingUsers[0].id),
539
+ { wrapper: createWrapper(queryClient) }
540
+ )
541
+
542
+ await waitFor(() => {
543
+ expect(retrieveResult.current.query.isSuccess).toBe(true)
544
+ })
545
+
546
+ const { result: updateResult } = renderHook(
547
+ () => queryManager.useUpdate({ optimistic: true }),
548
+ { wrapper: createWrapper(queryClient) }
549
+ )
550
+
551
+ const updateData = { id: existingUsers[0].id, name: 'Optimistically Updated' }
552
+
553
+ updateResult.current.mutate(updateData)
554
+
555
+ // Should immediately appear updated
556
+ await waitFor(() => {
557
+ expect(retrieveResult.current.item?.name).toBe('Optimistically Updated')
558
+ })
559
+
560
+ await waitFor(() => {
561
+ expect(updateResult.current.isSuccess).toBe(true)
562
+ })
563
+ })
564
+
565
+ // it('should rollback optimistic update on error', async () => {
566
+ // // Mock API to throw error BEFORE creating hook
567
+ // const originalUpdateFn = mockApi.updateFn
568
+ // mockApi.updateFn = jest.fn().mockRejectedValue(new Error('Update failed'))
569
+
570
+ // // First, populate retrieve cache
571
+ // const { result: retrieveResult } = renderHook(
572
+ // () => queryManager.useRetrieve(existingUsers[0].id),
573
+ // { wrapper: createWrapper(queryClient) }
574
+ // )
575
+
576
+ // await waitFor(() => {
577
+ // expect(retrieveResult.current.query.isSuccess).toBe(true)
578
+ // })
579
+
580
+ // const originalName = retrieveResult.current.item?.name
581
+
582
+ // const { result: updateResult } = renderHook(
583
+ // () => queryManager.useUpdate({
584
+ // optimistic: true,
585
+ // onError: (error) => {
586
+ // console.log('Expected error:', error.message)
587
+ // }
588
+ // }),
589
+ // { wrapper: createWrapper(queryClient) }
590
+ // )
591
+
592
+ // const updateData = { id: existingUsers[0].id, name: 'Failed Update' }
593
+
594
+ // updateResult.current.mutate(updateData)
595
+
596
+ // // Should temporarily show updated name
597
+ // await waitFor(() => {
598
+ // expect(retrieveResult.current.item?.name).toBe('Failed Update')
599
+ // })
600
+
601
+ // // Should rollback to original name on error
602
+ // await waitFor(() => {
603
+ // expect(updateResult.current.isError).toBe(true)
604
+ // })
605
+
606
+ // expect(retrieveResult.current.item?.name).toBe(originalName!)
607
+
608
+ // // Restore original function
609
+ // mockApi.updateFn = originalUpdateFn
610
+ // })
611
+
612
+ it('should call custom mutation callbacks', async () => {
613
+ const onMutate = jest.fn()
614
+ const onSuccess = jest.fn()
615
+ const onError = jest.fn()
616
+
617
+ const { result } = renderHook(
618
+ () => queryManager.useUpdate({ onMutate, onSuccess, onError }),
619
+ { wrapper: createWrapper(queryClient) }
620
+ )
621
+
622
+ const updateData = { id: existingUsers[0].id, name: 'Updated Name' }
623
+
624
+ result.current.mutate(updateData)
625
+
626
+ await waitFor(() => {
627
+ expect(result.current.isSuccess).toBe(true)
628
+ })
629
+
630
+ expect(onMutate).toHaveBeenCalled()
631
+ expect(onSuccess).toHaveBeenCalled()
632
+ expect(onError).not.toHaveBeenCalled()
633
+ })
634
+ })
635
+
636
+ describe('useDelete', () => {
637
+ let existingUsers: TestUser[]
638
+
639
+ beforeEach(() => {
640
+ existingUsers = createMockUsers(3)
641
+ mockApi.setUsers(existingUsers)
642
+ })
643
+
644
+ it('should delete existing item', async () => {
645
+ const { result } = renderHook(
646
+ () => queryManager.useDelete(),
647
+ { wrapper: createWrapper(queryClient) }
648
+ )
649
+
650
+ const targetId = existingUsers[0].id
651
+
652
+ result.current.mutate(targetId)
653
+
654
+ await waitFor(() => {
655
+ expect(result.current.isSuccess).toBe(true)
656
+ })
657
+
658
+ expect(result.current.data).toBe(targetId)
659
+ expect(mockApi.getUsersArray()).toHaveLength(2)
660
+ expect(mockApi.getUsersArray().find(u => u.id === targetId)).toBeUndefined()
661
+ })
662
+
663
+ it('should handle optimistic updates', async () => {
664
+ // First, populate list cache
665
+ const { result: listResult } = renderHook(
666
+ () => queryManager.useList(),
667
+ { wrapper: createWrapper(queryClient) }
668
+ )
669
+
670
+ await waitFor(() => {
671
+ expect(listResult.current.query.isSuccess).toBe(true)
672
+ })
673
+
674
+ const { result: deleteResult } = renderHook(
675
+ () => queryManager.useDelete({ optimistic: true }),
676
+ { wrapper: createWrapper(queryClient) }
677
+ )
678
+
679
+ const targetId = existingUsers[0].id
680
+
681
+ deleteResult.current.mutate(targetId)
682
+
683
+ // Should immediately disappear from list
684
+ await waitFor(() => {
685
+ expect(listResult.current.items.find(item => item.id === targetId)).toBeUndefined()
686
+ })
687
+
688
+ await waitFor(() => {
689
+ expect(deleteResult.current.isSuccess).toBe(true)
690
+ })
691
+ })
692
+
693
+ // it('should rollback optimistic delete on error', async () => {
694
+ // // Mock API to throw error BEFORE creating hook
695
+ // const originalDeleteFn = mockApi.deleteFn
696
+ // mockApi.deleteFn = jest.fn().mockRejectedValue(new Error('Delete failed'))
697
+
698
+ // // First, populate list cache
699
+ // const { result: listResult } = renderHook(
700
+ // () => queryManager.useList(),
701
+ // { wrapper: createWrapper(queryClient) }
702
+ // )
703
+
704
+ // await waitFor(() => {
705
+ // expect(listResult.current.query.isSuccess).toBe(true)
706
+ // })
707
+
708
+ // const initialItemCount = listResult.current.items.length
709
+ // const targetId = existingUsers[0].id
710
+
711
+ // const { result: deleteResult } = renderHook(
712
+ // () => queryManager.useDelete({
713
+ // optimistic: true,
714
+ // onError: (error) => {
715
+ // console.log('Expected error:', error.message)
716
+ // }
717
+ // }),
718
+ // { wrapper: createWrapper(queryClient) }
719
+ // )
720
+
721
+ // deleteResult.current.mutate(targetId)
722
+
723
+ // // Should temporarily disappear from list
724
+ // await waitFor(() => {
725
+ // expect(listResult.current.items.length).toBeLessThan(initialItemCount)
726
+ // })
727
+
728
+ // // Should reappear on error
729
+ // await waitFor(() => {
730
+ // expect(deleteResult.current.isError).toBe(true)
731
+ // })
732
+
733
+ // expect(listResult.current.items).toHaveLength(initialItemCount)
734
+ // expect(listResult.current.items.find(item => item.id === targetId)).toBeDefined()
735
+
736
+ // // Restore original function
737
+ // mockApi.deleteFn = originalDeleteFn
738
+ // })
739
+
740
+ // it('should handle error in delete operation', async () => {
741
+ // // Mock API to throw error BEFORE creating hook
742
+ // const originalDeleteFn = mockApi.deleteFn
743
+ // mockApi.deleteFn = jest.fn().mockRejectedValue(new Error('Delete failed'))
744
+
745
+ // const { result } = renderHook(
746
+ // () => queryManager.useDelete({
747
+ // onError: (error) => {
748
+ // console.log('Expected error:', error.message)
749
+ // }
750
+ // }),
751
+ // { wrapper: createWrapper(queryClient) }
752
+ // )
753
+
754
+ // const targetId = existingUsers[0].id
755
+
756
+ // result.current.mutate(targetId)
757
+
758
+ // await waitFor(() => {
759
+ // expect(result.current.isError).toBe(true)
760
+ // })
761
+
762
+ // expect(result.current.error).toEqual(new Error('Delete failed'))
763
+
764
+ // // Restore original function
765
+ // mockApi.deleteFn = originalDeleteFn
766
+ // })
767
+
768
+ it('should call custom mutation callbacks', async () => {
769
+ const onMutate = jest.fn()
770
+ const onSuccess = jest.fn()
771
+ const onError = jest.fn()
772
+
773
+ const { result } = renderHook(
774
+ () => queryManager.useDelete({ onMutate, onSuccess, onError }),
775
+ { wrapper: createWrapper(queryClient) }
776
+ )
777
+
778
+ const targetId = existingUsers[0].id
779
+
780
+ result.current.mutate(targetId)
781
+
782
+ await waitFor(() => {
783
+ expect(result.current.isSuccess).toBe(true)
784
+ })
785
+
786
+ expect(onMutate).toHaveBeenCalled()
787
+ expect(onSuccess).toHaveBeenCalled()
788
+ expect(onError).not.toHaveBeenCalled()
789
+ })
790
+
791
+ // it('should restore item to original position on rollback', async () => {
792
+ // // Mock API to throw error BEFORE creating hook
793
+ // const originalDeleteFn = mockApi.deleteFn
794
+ // mockApi.deleteFn = jest.fn().mockRejectedValue(new Error('Delete failed'))
795
+
796
+ // // First, populate list cache
797
+ // const { result: listResult } = renderHook(
798
+ // () => queryManager.useList(),
799
+ // { wrapper: createWrapper(queryClient) }
800
+ // )
801
+
802
+ // await waitFor(() => {
803
+ // expect(listResult.current.query.isSuccess).toBe(true)
804
+ // })
805
+
806
+ // const originalItems = [...listResult.current.items]
807
+ // const targetId = existingUsers[1].id // Delete middle item
808
+
809
+ // const { result: deleteResult } = renderHook(
810
+ // () => queryManager.useDelete({
811
+ // optimistic: true,
812
+ // onError: (error) => {
813
+ // console.log('Expected error:', error.message)
814
+ // }
815
+ // }),
816
+ // { wrapper: createWrapper(queryClient) }
817
+ // )
818
+
819
+ // deleteResult.current.mutate(targetId)
820
+
821
+ // // Wait for error and rollback
822
+ // await waitFor(() => {
823
+ // expect(deleteResult.current.isError).toBe(true)
824
+ // })
825
+
826
+ // // Items should be in original order
827
+ // expect(listResult.current.items.length).toBe(originalItems.length)
828
+ // expect(listResult.current.items.find(item => item.id === targetId)).toBeDefined()
829
+
830
+ // // Restore original function
831
+ // mockApi.deleteFn = originalDeleteFn
832
+ // })
833
+ })
834
+
835
+ describe('prefetchRetrieve', () => {
836
+ beforeEach(() => {
837
+ const mockUsers = createMockUsers(3)
838
+ mockApi.setUsers(mockUsers)
839
+ })
840
+
841
+ it('should prefetch retrieve query', async () => {
842
+ const users = mockApi.getUsersArray()
843
+ const targetUser = users[0]
844
+ const prefetchSpy = spyOn(queryClient, 'prefetchQuery')
845
+
846
+ await queryManager.prefetchRetrieve(targetUser.id, { staleTime: 5000 })
847
+
848
+ expect(prefetchSpy).toHaveBeenCalledWith({
849
+ staleTime: 5000,
850
+ queryKey: ['users', 'retrieve', targetUser.id],
851
+ queryFn: expect.any(Function)
852
+ })
853
+ })
854
+
855
+ it('should execute prefetch query function correctly', async () => {
856
+ const users = mockApi.getUsersArray()
857
+ const targetUser = users[0]
858
+
859
+ await queryManager.prefetchRetrieve(targetUser.id)
860
+
861
+ // Verify data was prefetched correctly
862
+ const cachedData = queryClient.getQueryData(['users', 'retrieve', targetUser.id])
863
+ expect(cachedData).toEqual(targetUser)
864
+ })
865
+ })
866
+
867
+ describe('integration scenarios', () => {
868
+ beforeEach(() => {
869
+ const mockUsers = createMockUsers(10)
870
+ mockApi.setUsers(mockUsers)
871
+ })
872
+
873
+ it('should synchronize data between list and retrieve caches', async () => {
874
+ // This test needs to ensure proper synchronization implementation
875
+ // The issue is likely that updating one cache doesn't automatically sync the other
876
+
877
+ // First, populate list cache
878
+ const { result: listResult } = renderHook(
879
+ () => queryManager.useList(),
880
+ { wrapper: createWrapper(queryClient) }
881
+ )
882
+
883
+ await waitFor(() => {
884
+ expect(listResult.current.query.isSuccess).toBe(true)
885
+ })
886
+
887
+ const targetUser = listResult.current.items[0]
888
+
889
+ // Then retrieve specific item
890
+ const { result: retrieveResult } = renderHook(
891
+ () => queryManager.useRetrieve(targetUser.id),
892
+ { wrapper: createWrapper(queryClient) }
893
+ )
894
+
895
+ await waitFor(() => {
896
+ expect(retrieveResult.current.query.isSuccess).toBe(true)
897
+ })
898
+
899
+ // Update the user via mutations (not direct API)
900
+ const { result: updateResult } = renderHook(
901
+ () => queryManager.useUpdate({ optimistic: true }),
902
+ { wrapper: createWrapper(queryClient) }
903
+ )
904
+
905
+ const updateData = { id: targetUser.id, name: 'Synchronized Update' }
906
+
907
+ updateResult.current.mutate(updateData)
908
+
909
+ await waitFor(() => {
910
+ expect(updateResult.current.isSuccess).toBe(true)
911
+ })
912
+
913
+ // Both caches should reflect the update
914
+ await waitFor(() => {
915
+ expect(retrieveResult.current.item?.name).toBe('Synchronized Update')
916
+ expect(listResult.current.items.find(u => u.id === targetUser.id)?.name).toBe('Synchronized Update')
917
+ })
918
+ })
919
+ })
920
+ })