source: node_modules/@reduxjs/toolkit/src/query/tests/buildThunks.test.tsx@ a762898

Last change on this file since a762898 was a762898, checked in by istevanoska <ilinastevanoska@…>, 5 months ago

Added visualizations

  • Property mode set to 100644
File size: 12.2 KB
Line 
1import { configureStore } from '@reduxjs/toolkit'
2import { createApi } from '@reduxjs/toolkit/query/react'
3import { renderHook, waitFor } from '@testing-library/react'
4import {
5 actionsReducer,
6 setupApiStore,
7 withProvider,
8} from '../../tests/utils/helpers'
9import type { BaseQueryApi } from '../baseQueryTypes'
10
11describe('baseline thunk behavior', () => {
12 test('handles a non-async baseQuery without error', async () => {
13 const baseQuery = (args?: any) => ({ data: args })
14 const api = createApi({
15 baseQuery,
16 endpoints: (build) => ({
17 getUser: build.query<unknown, number>({
18 query(id) {
19 return { url: `user/${id}` }
20 },
21 }),
22 }),
23 })
24 const { getUser } = api.endpoints
25 const store = configureStore({
26 reducer: {
27 [api.reducerPath]: api.reducer,
28 },
29 middleware: (gDM) => gDM().concat(api.middleware),
30 })
31
32 const promise = store.dispatch(getUser.initiate(1))
33 const { data } = await promise
34
35 expect(data).toEqual({
36 url: 'user/1',
37 })
38
39 const storeResult = getUser.select(1)(store.getState())
40 expect(storeResult).toEqual({
41 data: {
42 url: 'user/1',
43 },
44 endpointName: 'getUser',
45 isError: false,
46 isLoading: false,
47 isSuccess: true,
48 isUninitialized: false,
49 originalArgs: 1,
50 requestId: expect.any(String),
51 status: 'fulfilled',
52 startedTimeStamp: expect.any(Number),
53 fulfilledTimeStamp: expect.any(Number),
54 })
55 })
56
57 test('passes the extraArgument property to the baseQueryApi', async () => {
58 const baseQuery = (_args: any, api: BaseQueryApi) => ({ data: api.extra })
59 const api = createApi({
60 baseQuery,
61 endpoints: (build) => ({
62 getUser: build.query<unknown, void>({
63 query: () => '',
64 }),
65 }),
66 })
67 const store = configureStore({
68 reducer: {
69 [api.reducerPath]: api.reducer,
70 },
71 middleware: (gDM) =>
72 gDM({ thunk: { extraArgument: 'cakes' } }).concat(api.middleware),
73 })
74 const { getUser } = api.endpoints
75 const { data } = await store.dispatch(getUser.initiate())
76 expect(data).toBe('cakes')
77 })
78
79 test('only triggers transformResponse when a query method is actually used', async () => {
80 const baseQuery = (args?: any) => ({ data: args })
81 const transformResponse = vi.fn((response: any) => response)
82 const api = createApi({
83 baseQuery,
84 endpoints: (build) => ({
85 hasQuery: build.query<string, string>({
86 query: (arg) => 'test',
87 transformResponse,
88 }),
89 hasQueryFn: build.query<string, void>(
90 // @ts-expect-error
91 {
92 queryFn: () => ({ data: 'test' }),
93 transformResponse,
94 },
95 ),
96 }),
97 })
98
99 const store = configureStore({
100 reducer: {
101 [api.reducerPath]: api.reducer,
102 },
103 middleware: (gDM) =>
104 gDM({ thunk: { extraArgument: 'cakes' } }).concat(api.middleware),
105 })
106
107 await store.dispatch(api.util.upsertQueryData('hasQuery', 'a', 'test'))
108 expect(transformResponse).not.toHaveBeenCalled()
109
110 transformResponse.mockReset()
111
112 await store.dispatch(api.endpoints.hasQuery.initiate('b'))
113 expect(transformResponse).toHaveBeenCalledTimes(1)
114
115 transformResponse.mockReset()
116
117 await store.dispatch(api.endpoints.hasQueryFn.initiate())
118 expect(transformResponse).not.toHaveBeenCalled()
119 })
120})
121
122describe('re-triggering behavior on arg change', () => {
123 const api = createApi({
124 baseQuery: () => ({ data: null }),
125 endpoints: (build) => ({
126 getUser: build.query<any, any>({
127 query: (obj) => obj,
128 }),
129 }),
130 })
131 const { getUser } = api.endpoints
132 const store = configureStore({
133 reducer: { [api.reducerPath]: api.reducer },
134 middleware: (gDM) => gDM().concat(api.middleware),
135 })
136
137 const spy = vi.spyOn(getUser, 'initiate')
138 beforeEach(() => void spy.mockClear())
139
140 test('re-trigger on literal value change', async () => {
141 const { result, rerender } = renderHook(
142 (props) => getUser.useQuery(props),
143 {
144 wrapper: withProvider(store),
145 initialProps: 5,
146 },
147 )
148
149 await waitFor(() => {
150 expect(result.current.status).not.toBe('pending')
151 })
152
153 expect(spy).toHaveBeenCalledOnce()
154
155 for (let x = 1; x < 3; x++) {
156 rerender(6)
157 await waitFor(() => {
158 expect(result.current.status).not.toBe('pending')
159 })
160 expect(spy).toHaveBeenCalledTimes(2)
161 }
162
163 for (let x = 1; x < 3; x++) {
164 rerender(7)
165 await waitFor(() => {
166 expect(result.current.status).not.toBe('pending')
167 })
168 expect(spy).toHaveBeenCalledTimes(3)
169 }
170 })
171
172 test('only re-trigger on shallow-equal arg change', async () => {
173 const { result, rerender } = renderHook(
174 (props) => getUser.useQuery(props),
175 {
176 wrapper: withProvider(store),
177 initialProps: { name: 'Bob', likes: 'iceCream' },
178 },
179 )
180
181 await waitFor(() => {
182 expect(result.current.status).not.toBe('pending')
183 })
184 expect(spy).toHaveBeenCalledOnce()
185
186 for (let x = 1; x < 3; x++) {
187 rerender({ name: 'Bob', likes: 'waffles' })
188 await waitFor(() => {
189 expect(result.current.status).not.toBe('pending')
190 })
191 expect(spy).toHaveBeenCalledTimes(2)
192 }
193
194 for (let x = 1; x < 3; x++) {
195 rerender({ name: 'Alice', likes: 'waffles' })
196 await waitFor(() => {
197 expect(result.current.status).not.toBe('pending')
198 })
199 expect(spy).toHaveBeenCalledTimes(3)
200 }
201 })
202
203 test('re-triggers every time on deeper value changes', async () => {
204 const name = 'Tim'
205
206 const { result, rerender } = renderHook(
207 (props) => getUser.useQuery(props),
208 {
209 wrapper: withProvider(store),
210 initialProps: { person: { name } },
211 },
212 )
213
214 await waitFor(() => {
215 expect(result.current.status).not.toBe('pending')
216 })
217 expect(spy).toHaveBeenCalledOnce()
218
219 for (let x = 1; x < 3; x++) {
220 rerender({ person: { name: name + x } })
221 await waitFor(() => {
222 expect(result.current.status).not.toBe('pending')
223 })
224 expect(spy).toHaveBeenCalledTimes(x + 1)
225 }
226 })
227
228 test('do not re-trigger if the order of keys change while maintaining the same values', async () => {
229 const { result, rerender } = renderHook(
230 (props) => getUser.useQuery(props),
231 {
232 wrapper: withProvider(store),
233 initialProps: { name: 'Tim', likes: 'Bananas' },
234 },
235 )
236
237 await waitFor(() => {
238 expect(result.current.status).not.toBe('pending')
239 })
240 expect(spy).toHaveBeenCalledOnce()
241
242 for (let x = 1; x < 3; x++) {
243 rerender({ likes: 'Bananas', name: 'Tim' })
244 await waitFor(() => {
245 expect(result.current.status).not.toBe('pending')
246 })
247 expect(spy).toHaveBeenCalledOnce()
248 }
249 })
250})
251
252describe('prefetch', () => {
253 const baseQuery = () => ({ data: { name: 'Test User' } })
254
255 const api = createApi({
256 baseQuery,
257 tagTypes: ['User'],
258 endpoints: (build) => ({
259 getUser: build.query<any, number>({
260 query: (id) => ({ url: `user/${id}` }),
261 providesTags: (result, error, id) => [{ type: 'User', id }],
262 }),
263 updateUser: build.mutation<any, { id: number; name: string }>({
264 query: ({ id, name }) => ({
265 url: `user/${id}`,
266 method: 'PUT',
267 body: { name },
268 }),
269 invalidatesTags: (result, error, { id }) => [{ type: 'User', id }],
270 }),
271 }),
272 keepUnusedDataFor: 0.1, // 100ms for faster test cleanup
273 })
274
275 let storeRef = setupApiStore(
276 api,
277 { ...actionsReducer },
278 {
279 withoutListeners: true,
280 },
281 )
282
283 let getSubscriptions: () => Map<string, any>
284 let getSubscriptionCount: (queryCacheKey: string) => number
285
286 beforeEach(() => {
287 storeRef = setupApiStore(
288 api,
289 { ...actionsReducer },
290 {
291 withoutListeners: true,
292 },
293 )
294 // Get subscription helpers
295 const subscriptionSelectors = storeRef.store.dispatch(
296 api.internalActions.internal_getRTKQSubscriptions(),
297 ) as any
298 getSubscriptions = subscriptionSelectors.getSubscriptions
299 getSubscriptionCount = subscriptionSelectors.getSubscriptionCount
300 })
301
302 describe('subscription behavior', () => {
303 it('prefetch should NOT create a subscription', async () => {
304 const queryCacheKey = 'getUser(1)'
305
306 // Initially no subscriptions
307 expect(getSubscriptionCount(queryCacheKey)).toBe(0)
308
309 // Dispatch prefetch
310 storeRef.store.dispatch(api.util.prefetch('getUser', 1, {}))
311 await Promise.all(
312 storeRef.store.dispatch(api.util.getRunningQueriesThunk()),
313 )
314
315 expect(getSubscriptionCount(queryCacheKey)).toBe(0)
316 })
317
318 it('prefetch allows cache cleanup after keepUnusedDataFor', async () => {
319 const queryCacheKey = 'getUser(1)'
320
321 // Prefetch the data
322 storeRef.store.dispatch(api.util.prefetch('getUser', 1, {}))
323 await Promise.all(
324 storeRef.store.dispatch(api.util.getRunningQueriesThunk()),
325 )
326
327 // Verify data is in cache
328 let state = api.endpoints.getUser.select(1)(storeRef.store.getState())
329 expect(state.data).toEqual({ name: 'Test User' })
330
331 // Wait longer than keepUnusedDataFor
332 await new Promise((resolve) => setTimeout(resolve, 150))
333
334 state = api.endpoints.getUser.select(1)(storeRef.store.getState())
335 expect(state.status).toBe('uninitialized')
336 expect(state.data).toBeUndefined()
337 })
338
339 it('prefetch does NOT trigger refetch on tag invalidation', async () => {
340 // Prefetch user 1
341 storeRef.store.dispatch(api.util.prefetch('getUser', 1, {}))
342 await Promise.all(
343 storeRef.store.dispatch(api.util.getRunningQueriesThunk()),
344 )
345
346 // Verify data is in cache
347 let state = api.endpoints.getUser.select(1)(storeRef.store.getState())
348 expect(state.data).toEqual({ name: 'Test User' })
349
350 // Invalidate the tag by updating the user
351 await storeRef.store.dispatch(
352 api.endpoints.updateUser.initiate({ id: 1, name: 'Updated' }),
353 )
354
355 // Since there's no subscription, the cache entry gets removed on invalidation
356 await Promise.all(
357 storeRef.store.dispatch(api.util.getRunningQueriesThunk()),
358 )
359
360 // Cache entry should be cleared (no subscription to keep it alive)
361 state = api.endpoints.getUser.select(1)(storeRef.store.getState())
362 expect(state.status).toBe('uninitialized')
363 expect(state.data).toBeUndefined()
364 })
365
366 it('multiple prefetches do not accumulate subscriptions', async () => {
367 const queryCacheKey = 'getUser(1)'
368
369 expect(getSubscriptionCount(queryCacheKey)).toBe(0)
370
371 // First prefetch
372 storeRef.store.dispatch(api.util.prefetch('getUser', 1, {}))
373 await Promise.all(
374 storeRef.store.dispatch(api.util.getRunningQueriesThunk()),
375 )
376 expect(getSubscriptionCount(queryCacheKey)).toBe(0)
377
378 // Second prefetch (force refetch)
379 storeRef.store.dispatch(api.util.prefetch('getUser', 1, { force: true }))
380 await Promise.all(
381 storeRef.store.dispatch(api.util.getRunningQueriesThunk()),
382 )
383
384 // Still no subscriptions
385 expect(getSubscriptionCount(queryCacheKey)).toBe(0)
386
387 // Third prefetch
388 storeRef.store.dispatch(api.util.prefetch('getUser', 1, { force: true }))
389 await Promise.all(
390 storeRef.store.dispatch(api.util.getRunningQueriesThunk()),
391 )
392 expect(getSubscriptionCount(queryCacheKey)).toBe(0)
393 })
394
395 it('prefetch followed by regular query should work correctly', async () => {
396 const queryCacheKey = 'getUser(1)'
397
398 // Prefetch first
399 storeRef.store.dispatch(api.util.prefetch('getUser', 1, {}))
400 await Promise.all(
401 storeRef.store.dispatch(api.util.getRunningQueriesThunk()),
402 )
403
404 // No subscription from prefetch
405 expect(getSubscriptionCount(queryCacheKey)).toBe(0)
406
407 // Now create a real subscription via initiate
408 const promise = storeRef.store.dispatch(api.endpoints.getUser.initiate(1))
409
410 // Should have 1 subscription from the initiate call
411 expect(getSubscriptionCount(queryCacheKey)).toBe(1)
412
413 // Unsubscribe
414 promise.unsubscribe()
415
416 // Subscription should be cleaned up
417 expect(getSubscriptionCount(queryCacheKey)).toBe(0)
418 })
419 })
420})
Note: See TracBrowser for help on using the repository browser.