@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.
- 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,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
|
+
})
|