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

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

Added visualizations

  • Property mode set to 100644
File size: 12.5 KB
Line 
1import { createApi } from '@reduxjs/toolkit/query/react'
2import { act, renderHook } from '@testing-library/react'
3import { delay } from 'msw'
4import {
5 actionsReducer,
6 hookWaitFor,
7 setupApiStore,
8} from '../../tests/utils/helpers'
9import type { InvalidationState } from '../core/apiState'
10
11interface Post {
12 id: string
13 title: string
14 contents: string
15}
16
17const baseQuery = vi.fn()
18beforeEach(() => {
19 baseQuery.mockReset()
20})
21
22const api = createApi({
23 baseQuery: (...args: any[]) => {
24 const result = baseQuery(...args)
25 if (typeof result === 'object' && 'then' in result)
26 return result
27 .then((data: any) => ({ data, meta: 'meta' }))
28 .catch((e: any) => ({ error: e }))
29 return { data: result, meta: 'meta' }
30 },
31 tagTypes: ['Post'],
32 endpoints: (build) => ({
33 post: build.query<Post, string>({
34 query: (id) => `post/${id}`,
35 providesTags: ['Post'],
36 }),
37 listPosts: build.query<Post[], void>({
38 query: () => `posts`,
39 providesTags: (result) => [
40 ...(result?.map(({ id }) => ({ type: 'Post' as const, id })) ?? []),
41 'Post',
42 ],
43 }),
44 updatePost: build.mutation<void, Pick<Post, 'id'> & Partial<Post>>({
45 query: ({ id, ...patch }) => ({
46 url: `post/${id}`,
47 method: 'PATCH',
48 body: patch,
49 }),
50 async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
51 const { undo } = dispatch(
52 api.util.updateQueryData('post', id, (draft) => {
53 Object.assign(draft, patch)
54 }),
55 )
56 queryFulfilled.catch(undo)
57 },
58 invalidatesTags: (result) => (result ? ['Post'] : []),
59 }),
60 }),
61})
62
63const storeRef = setupApiStore(api, { ...actionsReducer })
64
65describe('basic lifecycle', () => {
66 let onStart = vi.fn(),
67 onError = vi.fn(),
68 onSuccess = vi.fn()
69
70 const extendedApi = api.injectEndpoints({
71 endpoints: (build) => ({
72 test: build.mutation({
73 query: (x) => x,
74 async onQueryStarted(arg, api) {
75 onStart(arg)
76 try {
77 const result = await api.queryFulfilled
78 onSuccess(result)
79 } catch (e) {
80 onError(e)
81 }
82 },
83 }),
84 }),
85 overrideExisting: true,
86 })
87
88 beforeEach(() => {
89 onStart.mockReset()
90 onError.mockReset()
91 onSuccess.mockReset()
92 })
93
94 test('success', async () => {
95 const { result } = renderHook(
96 () => extendedApi.endpoints.test.useMutation(),
97 { wrapper: storeRef.wrapper },
98 )
99
100 baseQuery.mockResolvedValue('success')
101
102 expect(onStart).not.toHaveBeenCalled()
103 expect(baseQuery).not.toHaveBeenCalled()
104 act(() => void result.current[0]('arg'))
105 expect(onStart).toHaveBeenCalledWith('arg')
106 expect(baseQuery).toHaveBeenCalledWith('arg', expect.any(Object), undefined)
107
108 expect(onError).not.toHaveBeenCalled()
109 expect(onSuccess).not.toHaveBeenCalled()
110 await act(() => delay(5))
111 expect(onError).not.toHaveBeenCalled()
112 expect(onSuccess).toHaveBeenCalledWith({ data: 'success', meta: 'meta' })
113 })
114
115 test('error', async () => {
116 const { result } = renderHook(
117 () => extendedApi.endpoints.test.useMutation(),
118 { wrapper: storeRef.wrapper },
119 )
120
121 baseQuery.mockRejectedValueOnce('error')
122 expect(onStart).not.toHaveBeenCalled()
123 expect(baseQuery).not.toHaveBeenCalled()
124
125 act(() => void result.current[0]('arg'))
126 expect(onStart).toHaveBeenCalledWith('arg')
127 expect(baseQuery).toHaveBeenCalledWith('arg', expect.any(Object), undefined)
128 expect(onError).not.toHaveBeenCalled()
129 expect(onSuccess).not.toHaveBeenCalled()
130 await act(() => delay(5))
131 expect(onError).toHaveBeenCalledWith({
132 error: 'error',
133 isUnhandledError: false,
134 meta: undefined,
135 })
136 expect(onSuccess).not.toHaveBeenCalled()
137 })
138})
139
140describe('updateQueryData', () => {
141 test('updates cache values, can apply inverse patch', async () => {
142 baseQuery
143 .mockResolvedValueOnce({
144 id: '3',
145 title: 'All about cheese.',
146 contents: 'TODO',
147 })
148 // TODO I have no idea why the query is getting called multiple times,
149 // but passing an additional mocked value (_any_ value)
150 // seems to silence some annoying "got an undefined result" logging
151 .mockResolvedValueOnce(42)
152 const { result } = renderHook(() => api.endpoints.post.useQuery('3'), {
153 wrapper: storeRef.wrapper,
154 })
155 await hookWaitFor(() => expect(result.current.isSuccess).toBeTruthy())
156
157 const dataBefore = result.current.data
158 expect(dataBefore).toEqual({
159 id: '3',
160 title: 'All about cheese.',
161 contents: 'TODO',
162 })
163
164 let returnValue!: ReturnType<ReturnType<typeof api.util.updateQueryData>>
165 act(() => {
166 returnValue = storeRef.store.dispatch(
167 api.util.updateQueryData('post', '3', (draft) => {
168 draft.contents = 'I love cheese!'
169 }),
170 )
171 })
172
173 expect(result.current.data).not.toBe(dataBefore)
174 expect(result.current.data).toEqual({
175 id: '3',
176 title: 'All about cheese.',
177 contents: 'I love cheese!',
178 })
179
180 expect(returnValue).toEqual({
181 inversePatches: [{ op: 'replace', path: ['contents'], value: 'TODO' }],
182 patches: [{ op: 'replace', path: ['contents'], value: 'I love cheese!' }],
183 undo: expect.any(Function),
184 })
185
186 act(() => {
187 storeRef.store.dispatch(
188 api.util.patchQueryData('post', '3', returnValue.inversePatches),
189 )
190 })
191
192 expect(result.current.data).toEqual(dataBefore)
193 })
194
195 test('updates (list) cache values including provided tags, undos that', async () => {
196 baseQuery
197 .mockResolvedValueOnce([
198 { id: '3', title: 'All about cheese.', contents: 'TODO' },
199 ])
200 .mockResolvedValueOnce(42)
201 const { result } = renderHook(() => api.endpoints.listPosts.useQuery(), {
202 wrapper: storeRef.wrapper,
203 })
204 await hookWaitFor(() => expect(result.current.isSuccess).toBeTruthy())
205
206 let provided!: InvalidationState<'Post'>
207 act(() => {
208 provided = storeRef.store.getState().api.provided
209 })
210
211 const provided3 = provided.tags.Post['3']
212
213 let returnValue!: ReturnType<ReturnType<typeof api.util.updateQueryData>>
214 act(() => {
215 returnValue = storeRef.store.dispatch(
216 api.util.updateQueryData(
217 'listPosts',
218 undefined,
219 (draft) => {
220 draft.push({
221 id: '4',
222 title: 'Mostly about cheese.',
223 contents: 'TODO',
224 })
225 },
226 true,
227 ),
228 )
229 })
230
231 act(() => {
232 provided = storeRef.store.getState().api.provided
233 })
234
235 const provided4 = provided.tags.Post['4']
236
237 expect(provided4).toEqual(provided3)
238
239 act(() => {
240 returnValue.undo()
241 })
242
243 act(() => {
244 provided = storeRef.store.getState().api.provided
245 })
246
247 const provided4Next = provided.tags.Post['4']
248
249 expect(provided4Next).toEqual([])
250 })
251
252 test('updates (list) cache values excluding provided tags, undoes that', async () => {
253 baseQuery
254 .mockResolvedValueOnce([
255 { id: '3', title: 'All about cheese.', contents: 'TODO' },
256 ])
257 .mockResolvedValueOnce(42)
258 const { result } = renderHook(() => api.endpoints.listPosts.useQuery(), {
259 wrapper: storeRef.wrapper,
260 })
261 await hookWaitFor(() => expect(result.current.isSuccess).toBeTruthy())
262
263 let provided!: InvalidationState<'Post'>
264 act(() => {
265 provided = storeRef.store.getState().api.provided
266 })
267
268 let returnValue!: ReturnType<ReturnType<typeof api.util.updateQueryData>>
269 act(() => {
270 returnValue = storeRef.store.dispatch(
271 api.util.updateQueryData(
272 'listPosts',
273 undefined,
274 (draft) => {
275 draft.push({
276 id: '4',
277 title: 'Mostly about cheese.',
278 contents: 'TODO',
279 })
280 },
281 false,
282 ),
283 )
284 })
285
286 act(() => {
287 provided = storeRef.store.getState().api.provided
288 })
289
290 const provided4 = provided.tags.Post['4']
291
292 expect(provided4).toEqual(undefined)
293
294 act(() => {
295 returnValue.undo()
296 })
297
298 act(() => {
299 provided = storeRef.store.getState().api.provided
300 })
301
302 const provided4Next = provided.tags.Post['4']
303
304 expect(provided4Next).toEqual(undefined)
305 })
306
307 test('does not update non-existing values', async () => {
308 baseQuery
309 .mockImplementationOnce(async () => ({
310 id: '3',
311 title: 'All about cheese.',
312 contents: 'TODO',
313 }))
314 .mockResolvedValueOnce(42)
315
316 const { result } = renderHook(() => api.endpoints.post.useQuery('3'), {
317 wrapper: storeRef.wrapper,
318 })
319 await hookWaitFor(() => expect(result.current.isSuccess).toBeTruthy())
320
321 const dataBefore = result.current.data
322 expect(dataBefore).toEqual({
323 id: '3',
324 title: 'All about cheese.',
325 contents: 'TODO',
326 })
327
328 let returnValue!: ReturnType<ReturnType<typeof api.util.updateQueryData>>
329 act(() => {
330 returnValue = storeRef.store.dispatch(
331 api.util.updateQueryData('post', '4', (draft) => {
332 draft.contents = 'I love cheese!'
333 }),
334 )
335 })
336
337 expect(result.current.data).toBe(dataBefore)
338
339 expect(returnValue).toEqual({
340 inversePatches: [],
341 patches: [],
342 undo: expect.any(Function),
343 })
344 })
345})
346
347describe('full integration', () => {
348 test('success case', async () => {
349 baseQuery
350 .mockResolvedValueOnce({
351 id: '3',
352 title: 'All about cheese.',
353 contents: 'TODO',
354 })
355 .mockResolvedValueOnce({
356 id: '3',
357 title: 'Meanwhile, this changed server-side.',
358 contents: 'Delicious cheese!',
359 })
360 .mockResolvedValueOnce({
361 id: '3',
362 title: 'Meanwhile, this changed server-side.',
363 contents: 'Delicious cheese!',
364 })
365 .mockResolvedValueOnce(42)
366 const { result } = renderHook(
367 () => ({
368 query: api.endpoints.post.useQuery('3'),
369 mutation: api.endpoints.updatePost.useMutation(),
370 }),
371 { wrapper: storeRef.wrapper },
372 )
373 await hookWaitFor(() => expect(result.current.query.isSuccess).toBeTruthy())
374
375 expect(result.current.query.data).toEqual({
376 id: '3',
377 title: 'All about cheese.',
378 contents: 'TODO',
379 })
380
381 act(() => {
382 result.current.mutation[0]({ id: '3', contents: 'Delicious cheese!' })
383 })
384
385 expect(result.current.query.data).toEqual({
386 id: '3',
387 title: 'All about cheese.',
388 contents: 'Delicious cheese!',
389 })
390
391 await hookWaitFor(() =>
392 expect(result.current.query.data).toEqual({
393 id: '3',
394 title: 'Meanwhile, this changed server-side.',
395 contents: 'Delicious cheese!',
396 }),
397 )
398 })
399
400 test('error case', async () => {
401 baseQuery
402 .mockResolvedValueOnce({
403 id: '3',
404 title: 'All about cheese.',
405 contents: 'TODO',
406 })
407 .mockRejectedValueOnce('some error!')
408 .mockResolvedValueOnce({
409 id: '3',
410 title: 'Meanwhile, this changed server-side.',
411 contents: 'TODO',
412 })
413 .mockResolvedValueOnce(42)
414
415 const { result } = renderHook(
416 () => ({
417 query: api.endpoints.post.useQuery('3'),
418 mutation: api.endpoints.updatePost.useMutation(),
419 }),
420 { wrapper: storeRef.wrapper },
421 )
422 await hookWaitFor(() => expect(result.current.query.isSuccess).toBeTruthy())
423
424 expect(result.current.query.data).toEqual({
425 id: '3',
426 title: 'All about cheese.',
427 contents: 'TODO',
428 })
429
430 act(() => {
431 result.current.mutation[0]({ id: '3', contents: 'Delicious cheese!' })
432 })
433
434 // optimistic update
435 expect(result.current.query.data).toEqual({
436 id: '3',
437 title: 'All about cheese.',
438 contents: 'Delicious cheese!',
439 })
440
441 // rollback
442 await hookWaitFor(() =>
443 expect(result.current.query.data).toEqual({
444 id: '3',
445 title: 'All about cheese.',
446 contents: 'TODO',
447 }),
448 )
449
450 // mutation failed - will not invalidate query and not refetch data from the server
451 await expect(() =>
452 hookWaitFor(
453 () =>
454 expect(result.current.query.data).toEqual({
455 id: '3',
456 title: 'Meanwhile, this changed server-side.',
457 contents: 'TODO',
458 }),
459 50,
460 ),
461 ).rejects.toBeTruthy()
462
463 act(() => void result.current.query.refetch())
464
465 // manually refetching gives up-to-date data
466 await hookWaitFor(
467 () =>
468 expect(result.current.query.data).toEqual({
469 id: '3',
470 title: 'Meanwhile, this changed server-side.',
471 contents: 'TODO',
472 }),
473 50,
474 )
475 })
476})
Note: See TracBrowser for help on using the repository browser.