@codeleap/query 5.8.3 → 5.8.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 +6 -10
- package/package.json.bak +3 -7
- package/src/factors/createQueryManager.ts +38 -0
- package/src/factors/createQueryOperations.ts +37 -0
- package/src/factors/index.ts +2 -0
- package/src/index.ts +2 -8
- package/src/lib/Mutations.ts +280 -0
- package/src/{queryClient.ts → lib/QueryClientEnhanced/index.ts} +24 -72
- package/src/lib/QueryClientEnhanced/types.ts +38 -0
- package/src/lib/QueryKeys.ts +319 -0
- package/src/lib/QueryManager.ts +488 -0
- package/src/lib/QueryOperations/index.ts +351 -0
- package/src/lib/QueryOperations/types.ts +47 -0
- package/src/lib/index.ts +5 -0
- package/src/tests/Mutations.spec.tsx +458 -0
- package/src/tests/QueryManager.spec.tsx +920 -0
- package/src/tests/QueryOperations.spec.tsx +109 -0
- package/src/tests/integration.spec.tsx +551 -0
- package/src/tests/setup.ts +119 -0
- package/src/types/core.ts +33 -0
- package/src/types/create.ts +16 -0
- package/src/types/delete.ts +15 -0
- package/src/types/index.ts +7 -0
- package/src/types/list.ts +24 -0
- package/src/types/retrieve.ts +7 -0
- package/src/types/update.ts +14 -0
- package/src/types/utility.ts +22 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/misc.ts +43 -0
- package/src/QueryManager.ts +0 -954
- package/src/types.ts +0 -199
|
@@ -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
|
+
})
|