source: node_modules/@reduxjs/toolkit/src/query/tests/errorHandling.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: 17.8 KB
Line 
1import type { ThunkDispatch, UnknownAction } from '@reduxjs/toolkit'
2import type { BaseQueryFn, BaseQueryApi } from '@reduxjs/toolkit/query/react'
3import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
4import {
5 act,
6 fireEvent,
7 render,
8 renderHook,
9 screen,
10 waitFor,
11} from '@testing-library/react'
12import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'
13import axios from 'axios'
14import { HttpResponse, http } from 'msw'
15import * as React from 'react'
16import { useDispatch } from 'react-redux'
17import { hookWaitFor, setupApiStore } from '@internal/tests/utils/helpers'
18import { server } from '@internal/query/tests/mocks/server'
19
20const baseQuery = fetchBaseQuery({ baseUrl: 'https://example.com' })
21
22const api = createApi({
23 baseQuery,
24 endpoints(build) {
25 return {
26 query: build.query({ query: () => '/query' }),
27 mutation: build.mutation({
28 query: () => ({ url: '/mutation', method: 'POST' }),
29 }),
30 }
31 },
32})
33
34const storeRef = setupApiStore(api)
35
36const failQueryOnce = http.get(
37 '/query',
38 () => HttpResponse.json({ value: 'failed' }, { status: 500 }),
39 { once: true },
40)
41
42describe('fetchBaseQuery', () => {
43 let commonBaseQueryApiArgs: BaseQueryApi = {} as any
44 beforeEach(() => {
45 const abortController = new AbortController()
46 commonBaseQueryApiArgs = {
47 signal: abortController.signal,
48 abort: (reason) =>
49 //@ts-ignore
50 abortController.abort(reason),
51 dispatch: storeRef.store.dispatch,
52 getState: storeRef.store.getState,
53 extra: undefined,
54 type: 'query',
55 endpoint: 'doesntmatterhere',
56 }
57 })
58 test('success', async () => {
59 await expect(
60 baseQuery('/success', commonBaseQueryApiArgs, {}),
61 ).resolves.toEqual({
62 data: { value: 'success' },
63 meta: {
64 request: expect.any(Object),
65 response: expect.any(Object),
66 },
67 })
68 })
69 test('error', async () => {
70 server.use(failQueryOnce)
71 await expect(
72 baseQuery('/error', commonBaseQueryApiArgs, {}),
73 ).resolves.toEqual({
74 error: {
75 data: { value: 'error' },
76 status: 500,
77 },
78 meta: {
79 request: expect.any(Object),
80 response: expect.any(Object),
81 },
82 })
83 })
84})
85
86describe('query error handling', () => {
87 test('success', async () => {
88 server.use(
89 http.get('https://example.com/query', () =>
90 HttpResponse.json({ value: 'success' }),
91 ),
92 )
93 const { result } = renderHook(() => api.endpoints.query.useQuery({}), {
94 wrapper: storeRef.wrapper,
95 })
96
97 await hookWaitFor(() => expect(result.current.isFetching).toBeFalsy())
98 expect(result.current).toEqual(
99 expect.objectContaining({
100 isLoading: false,
101 isError: false,
102 isSuccess: true,
103 data: { value: 'success' },
104 }),
105 )
106 })
107
108 test('error', async () => {
109 server.use(
110 http.get('https://example.com/query', () =>
111 HttpResponse.json({ value: 'error' }, { status: 500 }),
112 ),
113 )
114 const { result } = renderHook(() => api.endpoints.query.useQuery({}), {
115 wrapper: storeRef.wrapper,
116 })
117
118 await hookWaitFor(() => expect(result.current.isFetching).toBeFalsy())
119 expect(result.current).toEqual(
120 expect.objectContaining({
121 isLoading: false,
122 isError: true,
123 isSuccess: false,
124 error: {
125 status: 500,
126 data: { value: 'error' },
127 },
128 }),
129 )
130 })
131
132 test('success -> error', async () => {
133 server.use(
134 http.get('https://example.com/query', () =>
135 HttpResponse.json({ value: 'success' }),
136 ),
137 )
138 const { result } = renderHook(() => api.endpoints.query.useQuery({}), {
139 wrapper: storeRef.wrapper,
140 })
141
142 await hookWaitFor(() => expect(result.current.isFetching).toBeFalsy())
143 expect(result.current).toEqual(
144 expect.objectContaining({
145 isLoading: false,
146 isError: false,
147 isSuccess: true,
148 data: { value: 'success' },
149 }),
150 )
151
152 server.use(
153 http.get(
154 'https://example.com/query',
155 () => HttpResponse.json({ value: 'error' }, { status: 500 }),
156 { once: true },
157 ),
158 )
159
160 act(() => void result.current.refetch())
161
162 await hookWaitFor(() => expect(result.current.isFetching).toBeFalsy())
163 expect(result.current).toEqual(
164 expect.objectContaining({
165 isLoading: false,
166 isError: true,
167 isSuccess: false,
168 error: {
169 status: 500,
170 data: { value: 'error' },
171 },
172 // last data will stay available
173 data: { value: 'success' },
174 }),
175 )
176 })
177
178 test('error -> success', async () => {
179 server.use(
180 http.get('https://example.com/query', () =>
181 HttpResponse.json({ value: 'success' }),
182 ),
183 )
184 server.use(
185 http.get(
186 'https://example.com/query',
187 () => HttpResponse.json({ value: 'error' }, { status: 500 }),
188 { once: true },
189 ),
190 )
191 const { result } = renderHook(() => api.endpoints.query.useQuery({}), {
192 wrapper: storeRef.wrapper,
193 })
194
195 await hookWaitFor(() => expect(result.current.isFetching).toBeFalsy())
196 expect(result.current).toEqual(
197 expect.objectContaining({
198 isLoading: false,
199 isError: true,
200 isSuccess: false,
201 error: {
202 status: 500,
203 data: { value: 'error' },
204 },
205 }),
206 )
207
208 act(() => void result.current.refetch())
209
210 await hookWaitFor(() => expect(result.current.isFetching).toBeFalsy())
211 expect(result.current).toEqual(
212 expect.objectContaining({
213 isLoading: false,
214 isError: false,
215 isSuccess: true,
216 data: { value: 'success' },
217 }),
218 )
219 })
220})
221
222describe('mutation error handling', () => {
223 test('success', async () => {
224 server.use(
225 http.post('https://example.com/mutation', () =>
226 HttpResponse.json({ value: 'success' }),
227 ),
228 )
229 const { result } = renderHook(() => api.endpoints.mutation.useMutation(), {
230 wrapper: storeRef.wrapper,
231 })
232
233 const [trigger] = result.current
234
235 act(() => void trigger({}))
236
237 await hookWaitFor(() => expect(result.current[1].isLoading).toBeFalsy())
238 expect(result.current[1]).toEqual(
239 expect.objectContaining({
240 isLoading: false,
241 isError: false,
242 isSuccess: true,
243 data: { value: 'success' },
244 }),
245 )
246 })
247
248 test('error', async () => {
249 server.use(
250 http.post('https://example.com/mutation', () =>
251 HttpResponse.json({ value: 'error' }, { status: 500 }),
252 ),
253 )
254 const { result } = renderHook(() => api.endpoints.mutation.useMutation(), {
255 wrapper: storeRef.wrapper,
256 })
257
258 const [trigger] = result.current
259
260 act(() => void trigger({}))
261
262 await hookWaitFor(() => expect(result.current[1].isLoading).toBeFalsy())
263 expect(result.current[1]).toEqual(
264 expect.objectContaining({
265 isLoading: false,
266 isError: true,
267 isSuccess: false,
268 error: {
269 status: 500,
270 data: { value: 'error' },
271 },
272 }),
273 )
274 })
275
276 test('success -> error', async () => {
277 server.use(
278 http.post('https://example.com/mutation', () =>
279 HttpResponse.json({ value: 'success' }),
280 ),
281 )
282 const { result } = renderHook(() => api.endpoints.mutation.useMutation(), {
283 wrapper: storeRef.wrapper,
284 })
285
286 {
287 const [trigger] = result.current
288
289 act(() => void trigger({}))
290
291 await hookWaitFor(() => expect(result.current[1].isLoading).toBeFalsy())
292 expect(result.current[1]).toEqual(
293 expect.objectContaining({
294 isLoading: false,
295 isError: false,
296 isSuccess: true,
297 data: { value: 'success' },
298 }),
299 )
300 }
301
302 server.use(
303 http.post(
304 'https://example.com/mutation',
305 () => HttpResponse.json({ value: 'error' }, { status: 500 }),
306 { once: true },
307 ),
308 )
309
310 {
311 const [trigger] = result.current
312
313 act(() => void trigger({}))
314
315 await hookWaitFor(() => expect(result.current[1].isLoading).toBeFalsy())
316 expect(result.current[1]).toEqual(
317 expect.objectContaining({
318 isLoading: false,
319 isError: true,
320 isSuccess: false,
321 error: {
322 status: 500,
323 data: { value: 'error' },
324 },
325 }),
326 )
327 expect(result.current[1].data).toBeUndefined()
328 }
329 })
330
331 test('error -> success', async () => {
332 server.use(
333 http.post('https://example.com/mutation', () =>
334 HttpResponse.json({ value: 'success' }),
335 ),
336 )
337 server.use(
338 http.post(
339 'https://example.com/mutation',
340 () => HttpResponse.json({ value: 'error' }, { status: 500 }),
341 { once: true },
342 ),
343 )
344
345 const { result } = renderHook(() => api.endpoints.mutation.useMutation(), {
346 wrapper: storeRef.wrapper,
347 })
348
349 {
350 const [trigger] = result.current
351
352 act(() => void trigger({}))
353
354 await hookWaitFor(() => expect(result.current[1].isLoading).toBeFalsy())
355 expect(result.current[1]).toEqual(
356 expect.objectContaining({
357 isLoading: false,
358 isError: true,
359 isSuccess: false,
360 error: {
361 status: 500,
362 data: { value: 'error' },
363 },
364 }),
365 )
366 }
367
368 {
369 const [trigger] = result.current
370
371 act(() => void trigger({}))
372
373 await hookWaitFor(() => expect(result.current[1].isLoading).toBeFalsy())
374 expect(result.current[1]).toEqual(
375 expect.objectContaining({
376 isLoading: false,
377 isError: false,
378 isSuccess: true,
379 }),
380 )
381 expect(result.current[1].error).toBeUndefined()
382 }
383 })
384})
385
386describe('custom axios baseQuery', () => {
387 const axiosBaseQuery =
388 (
389 { baseUrl }: { baseUrl: string } = { baseUrl: '' },
390 ): BaseQueryFn<
391 {
392 url: string
393 method?: AxiosRequestConfig['method']
394 data?: AxiosRequestConfig['data']
395 },
396 unknown,
397 unknown,
398 unknown,
399 { response: AxiosResponse; request: AxiosRequestConfig }
400 > =>
401 async ({ url, method, data }) => {
402 const config = { url: baseUrl + url, method, data }
403 try {
404 const result = await axios(config)
405 return {
406 data: result.data,
407 meta: { request: config, response: result },
408 }
409 } catch (axiosError) {
410 const err = axiosError as AxiosError
411 return {
412 error: {
413 status: err.response?.status,
414 data: err.response?.data,
415 },
416 meta: { request: config, response: err.response as AxiosResponse },
417 }
418 }
419 }
420
421 type SuccessResponse = { value: 'success' }
422 const api = createApi({
423 baseQuery: axiosBaseQuery({
424 baseUrl: 'https://example.com',
425 }),
426 endpoints(build) {
427 return {
428 query: build.query<SuccessResponse, void>({
429 query: () => ({ url: '/success', method: 'get' }),
430 transformResponse: (result: SuccessResponse, meta) => {
431 return { ...result, metaResponseData: meta?.response.data }
432 },
433 }),
434 mutation: build.mutation<SuccessResponse, any>({
435 query: () => ({ url: '/success', method: 'post' }),
436 }),
437 }
438 },
439 })
440
441 const storeRef = setupApiStore(api)
442
443 test('axiosBaseQuery transformResponse uses its custom meta format', async () => {
444 const result = await storeRef.store.dispatch(api.endpoints.query.initiate())
445
446 expect(result.data).toEqual({
447 value: 'success',
448 metaResponseData: { value: 'success' },
449 })
450 })
451
452 test('axios errors behave as expected', async () => {
453 server.use(
454 http.get('https://example.com/success', () =>
455 HttpResponse.json({ value: 'error' }, { status: 500 }),
456 ),
457 )
458 const { result } = renderHook(() => api.endpoints.query.useQuery(), {
459 wrapper: storeRef.wrapper,
460 })
461
462 await hookWaitFor(() => expect(result.current.isFetching).toBeFalsy())
463 expect(result.current).toEqual(
464 expect.objectContaining({
465 isLoading: false,
466 isError: true,
467 isSuccess: false,
468 error: { status: 500, data: { value: 'error' } },
469 }),
470 )
471 })
472})
473
474describe('error handling in a component', () => {
475 const mockErrorResponse = { value: 'error', very: 'mean' }
476 const mockSuccessResponse = { value: 'success' }
477
478 const api = createApi({
479 baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
480 endpoints: (build) => ({
481 update: build.mutation<typeof mockSuccessResponse, any>({
482 query: () => ({ url: 'success' }),
483 }),
484 failedUpdate: build.mutation<typeof mockSuccessResponse, any>({
485 query: () => ({ url: 'error' }),
486 }),
487 }),
488 })
489 const storeRef = setupApiStore(api)
490
491 test('a mutation is unwrappable and has the correct types', async () => {
492 server.use(
493 http.get(
494 'https://example.com/success',
495 () => HttpResponse.json(mockErrorResponse, { status: 500 }),
496 { once: true },
497 ),
498 )
499
500 function User() {
501 const [manualError, setManualError] = React.useState<any>()
502 const [update, { isLoading, data, error }] =
503 api.endpoints.update.useMutation()
504
505 return (
506 <div>
507 <div data-testid="isLoading">{String(isLoading)}</div>
508 <div data-testid="data">{JSON.stringify(data)}</div>
509 <div data-testid="error">{JSON.stringify(error)}</div>
510 <div data-testid="manuallySetError">
511 {JSON.stringify(manualError)}
512 </div>
513 <button
514 onClick={() => {
515 update({ name: 'hello' })
516 .unwrap()
517 .then((result) => {
518 setManualError(undefined)
519 })
520 .catch((error) => act(() => setManualError(error)))
521 }}
522 >
523 Update User
524 </button>
525 </div>
526 )
527 }
528
529 render(<User />, { wrapper: storeRef.wrapper })
530
531 await waitFor(() =>
532 expect(screen.getByTestId('isLoading').textContent).toBe('false'),
533 )
534 fireEvent.click(screen.getByText('Update User'))
535 expect(screen.getByTestId('isLoading').textContent).toBe('true')
536 await waitFor(() =>
537 expect(screen.getByTestId('isLoading').textContent).toBe('false'),
538 )
539
540 // Make sure the hook and the unwrapped action return the same things in an error state
541 await waitFor(() =>
542 expect(screen.getByTestId('error').textContent).toEqual(
543 screen.getByTestId('manuallySetError').textContent,
544 ),
545 )
546
547 fireEvent.click(screen.getByText('Update User'))
548 expect(screen.getByTestId('isLoading').textContent).toBe('true')
549 await waitFor(() =>
550 expect(screen.getByTestId('isLoading').textContent).toBe('false'),
551 )
552 await waitFor(() =>
553 expect(screen.getByTestId('error').textContent).toBeFalsy(),
554 )
555 await waitFor(() =>
556 expect(screen.getByTestId('manuallySetError').textContent).toBeFalsy(),
557 )
558 await waitFor(() =>
559 expect(screen.getByTestId('data').textContent).toEqual(
560 JSON.stringify(mockSuccessResponse),
561 ),
562 )
563 })
564
565 for (const track of [true, false]) {
566 test(`an un-subscribed mutation will still return something useful (success case, track: ${track})`, async () => {
567 const hook = renderHook(useDispatch, { wrapper: storeRef.wrapper })
568
569 const dispatch = hook.result.current as ThunkDispatch<
570 any,
571 any,
572 UnknownAction
573 >
574 let mutationqueryFulfilled: ReturnType<
575 ReturnType<typeof api.endpoints.update.initiate>
576 >
577 act(() => {
578 mutationqueryFulfilled = dispatch(
579 api.endpoints.update.initiate({}, { track }),
580 )
581 })
582 const result = await mutationqueryFulfilled!
583 expect(result).toMatchObject({
584 data: { value: 'success' },
585 })
586 })
587
588 test(`an un-subscribed mutation will still return something useful (error case, track: ${track})`, async () => {
589 const hook = renderHook(useDispatch, { wrapper: storeRef.wrapper })
590
591 const dispatch = hook.result.current as ThunkDispatch<
592 any,
593 any,
594 UnknownAction
595 >
596 let mutationqueryFulfilled: ReturnType<
597 ReturnType<typeof api.endpoints.failedUpdate.initiate>
598 >
599 act(() => {
600 mutationqueryFulfilled = dispatch(
601 api.endpoints.failedUpdate.initiate({}, { track }),
602 )
603 })
604 const result = await mutationqueryFulfilled!
605 expect(result).toMatchObject({
606 error: {
607 status: 500,
608 data: { value: 'error' },
609 },
610 })
611 })
612 test(`an un-subscribed mutation will still be unwrappable (success case), track: ${track}`, async () => {
613 const hook = renderHook(useDispatch, { wrapper: storeRef.wrapper })
614
615 const dispatch = hook.result.current as ThunkDispatch<
616 any,
617 any,
618 UnknownAction
619 >
620 let mutationqueryFulfilled: ReturnType<
621 ReturnType<typeof api.endpoints.update.initiate>
622 >
623 act(() => {
624 mutationqueryFulfilled = dispatch(
625 api.endpoints.update.initiate({}, { track }),
626 )
627 })
628 const result = await mutationqueryFulfilled!.unwrap()
629 expect(result).toMatchObject({
630 value: 'success',
631 })
632 })
633
634 test(`an un-subscribed mutation will still be unwrappable (error case, track: ${track})`, async () => {
635 const hook = renderHook(useDispatch, { wrapper: storeRef.wrapper })
636
637 const dispatch = hook.result.current as ThunkDispatch<
638 any,
639 any,
640 UnknownAction
641 >
642 let mutationqueryFulfilled: ReturnType<
643 ReturnType<typeof api.endpoints.failedUpdate.initiate>
644 >
645 act(() => {
646 mutationqueryFulfilled = dispatch(
647 api.endpoints.failedUpdate.initiate({}, { track }),
648 )
649 })
650 const unwrappedPromise = mutationqueryFulfilled!.unwrap()
651 await expect(unwrappedPromise).rejects.toMatchObject({
652 status: 500,
653 data: { value: 'error' },
654 })
655 })
656 }
657})
Note: See TracBrowser for help on using the repository browser.