source: node_modules/@reduxjs/toolkit/src/query/tests/createApi.test.ts

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

Added visualizations

  • Property mode set to 100644
File size: 55.8 KB
Line 
1import { noop } from '@internal/listenerMiddleware/utils'
2import { server } from '@internal/query/tests/mocks/server'
3import {
4 getSerializedHeaders,
5 setupApiStore,
6} from '@internal/tests/utils/helpers'
7import type { SerializedError } from '@reduxjs/toolkit'
8import { configureStore, createAction, createReducer } from '@reduxjs/toolkit'
9import type {
10 DefinitionsFromApi,
11 FetchBaseQueryError,
12 FetchBaseQueryMeta,
13 OverrideResultType,
14 SchemaFailureConverter,
15 SchemaType,
16 SerializeQueryArgs,
17 TagTypesFromApi,
18} from '@reduxjs/toolkit/query'
19import {
20 createApi,
21 fetchBaseQuery,
22 NamedSchemaError,
23} from '@reduxjs/toolkit/query'
24import { HttpResponse, delay, http } from 'msw'
25import nodeFetch from 'node-fetch'
26import * as v from 'valibot'
27import type { SchemaFailureHandler } from '../endpointDefinitions'
28
29beforeAll(() => {
30 vi.stubEnv('NODE_ENV', 'development')
31
32 return vi.unstubAllEnvs
33})
34
35const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(noop)
36
37afterEach(() => {
38 vi.clearAllMocks()
39 server.resetHandlers()
40})
41
42afterAll(() => {
43 vi.restoreAllMocks()
44})
45
46function paginate<T>(array: T[], page_size: number, page_number: number) {
47 // human-readable page numbers usually start with 1, so we reduce 1 in the first argument
48 return array.slice((page_number - 1) * page_size, page_number * page_size)
49}
50
51test('sensible defaults', () => {
52 const api = createApi({
53 baseQuery: fetchBaseQuery(),
54 endpoints: (build) => ({
55 getUser: build.query<unknown, void>({
56 query(id) {
57 return { url: `user/${id}` }
58 },
59 }),
60 updateUser: build.mutation<unknown, void>({
61 query: () => '',
62 }),
63 }),
64 })
65 configureStore({
66 reducer: {
67 [api.reducerPath]: api.reducer,
68 },
69 middleware: (gDM) => gDM().concat(api.middleware),
70 })
71 expect(api.reducerPath).toBe('api')
72
73 expect(api.endpoints.getUser.name).toBe('getUser')
74 expect(api.endpoints.updateUser.name).toBe('updateUser')
75})
76
77describe('wrong tagTypes log errors', () => {
78 const baseQuery = vi.fn()
79 const api = createApi({
80 baseQuery,
81 tagTypes: ['User'],
82 endpoints: (build) => ({
83 provideNothing: build.query<unknown, void>({
84 query: () => '',
85 }),
86 provideTypeString: build.query<unknown, void>({
87 query: () => '',
88 providesTags: ['User'],
89 }),
90 provideTypeWithId: build.query<unknown, void>({
91 query: () => '',
92 providesTags: [{ type: 'User', id: 5 }],
93 }),
94 provideTypeWithIdAndCallback: build.query<unknown, void>({
95 query: () => '',
96 providesTags: () => [{ type: 'User', id: 5 }],
97 }),
98 provideWrongTypeString: build.query<unknown, void>({
99 query: () => '',
100 // @ts-expect-error
101 providesTags: ['Users'],
102 }),
103 provideWrongTypeWithId: build.query<unknown, void>({
104 query: () => '',
105 // @ts-expect-error
106 providesTags: [{ type: 'Users', id: 5 }],
107 }),
108 provideWrongTypeWithIdAndCallback: build.query<unknown, void>({
109 query: () => '',
110 // @ts-expect-error
111 providesTags: () => [{ type: 'Users', id: 5 }],
112 }),
113 invalidateNothing: build.query<unknown, void>({
114 query: () => '',
115 }),
116 invalidateTypeString: build.mutation<unknown, void>({
117 query: () => '',
118 invalidatesTags: ['User'],
119 }),
120 invalidateTypeWithId: build.mutation<unknown, void>({
121 query: () => '',
122 invalidatesTags: [{ type: 'User', id: 5 }],
123 }),
124 invalidateTypeWithIdAndCallback: build.mutation<unknown, void>({
125 query: () => '',
126 invalidatesTags: () => [{ type: 'User', id: 5 }],
127 }),
128
129 invalidateWrongTypeString: build.mutation<unknown, void>({
130 query: () => '',
131 // @ts-expect-error
132 invalidatesTags: ['Users'],
133 }),
134 invalidateWrongTypeWithId: build.mutation<unknown, void>({
135 query: () => '',
136 // @ts-expect-error
137 invalidatesTags: [{ type: 'Users', id: 5 }],
138 }),
139 invalidateWrongTypeWithIdAndCallback: build.mutation<unknown, void>({
140 query: () => '',
141 // @ts-expect-error
142 invalidatesTags: () => [{ type: 'Users', id: 5 }],
143 }),
144 }),
145 })
146 const store = configureStore({
147 reducer: {
148 [api.reducerPath]: api.reducer,
149 },
150 middleware: (gDM) => gDM().concat(api.middleware),
151 })
152
153 beforeEach(() => {
154 baseQuery.mockResolvedValue({ data: 'foo' })
155 })
156
157 test.each<[keyof typeof api.endpoints, boolean?]>([
158 ['provideNothing', false],
159 ['provideTypeString', false],
160 ['provideTypeWithId', false],
161 ['provideTypeWithIdAndCallback', false],
162 ['provideWrongTypeString', true],
163 ['provideWrongTypeWithId', true],
164 ['provideWrongTypeWithIdAndCallback', true],
165 ['invalidateNothing', false],
166 ['invalidateTypeString', false],
167 ['invalidateTypeWithId', false],
168 ['invalidateTypeWithIdAndCallback', false],
169 ['invalidateWrongTypeString', true],
170 ['invalidateWrongTypeWithId', true],
171 ['invalidateWrongTypeWithIdAndCallback', true],
172 ])(`endpoint %s should log an error? %s`, async (endpoint, shouldError) => {
173 vi.stubEnv('NODE_ENV', 'development')
174
175 // @ts-ignore
176 store.dispatch(api.endpoints[endpoint].initiate())
177 let result: { status: string }
178 do {
179 await delay(5)
180 // @ts-ignore
181 result = api.endpoints[endpoint].select()(store.getState())
182 } while (result.status === 'pending')
183
184 if (shouldError) {
185 expect(consoleErrorSpy).toHaveBeenLastCalledWith(
186 "Tag type 'Users' was used, but not specified in `tagTypes`!",
187 )
188 } else {
189 expect(consoleErrorSpy).not.toHaveBeenCalled()
190 }
191 })
192})
193
194describe('endpoint definition typings', () => {
195 const api = createApi({
196 baseQuery: (from: 'From'): { data: 'To' } | Promise<{ data: 'To' }> => ({
197 data: 'To',
198 }),
199 endpoints: () => ({}),
200 tagTypes: ['typeA', 'typeB'],
201 })
202 test('query: query & transformResponse types', () => {
203 api.injectEndpoints({
204 endpoints: (build) => ({
205 query: build.query<'RetVal', 'Arg'>({
206 query: (x: 'Arg') => 'From' as const,
207 transformResponse(r: 'To') {
208 return 'RetVal' as const
209 },
210 }),
211 query1: build.query<'RetVal', 'Arg'>({
212 // @ts-expect-error
213 query: (x: 'Error') => 'From' as const,
214 transformResponse(r: 'To') {
215 return 'RetVal' as const
216 },
217 }),
218 query2: build.query<'RetVal', 'Arg'>({
219 // @ts-expect-error
220 query: (x: 'Arg') => 'Error' as const,
221 transformResponse(r: 'To') {
222 return 'RetVal' as const
223 },
224 }),
225 query3: build.query<'RetVal', 'Arg'>({
226 query: (x: 'Arg') => 'From' as const,
227 // @ts-expect-error
228 transformResponse(r: 'Error') {
229 return 'RetVal' as const
230 },
231 }),
232 query4: build.query<'RetVal', 'Arg'>({
233 query: (x: 'Arg') => 'From' as const,
234 // @ts-expect-error
235 transformResponse(r: 'To') {
236 return 'Error' as const
237 },
238 }),
239 queryInference1: build.query<'RetVal', 'Arg'>({
240 query: (x) => {
241 return 'From'
242 },
243 transformResponse(r) {
244 return 'RetVal'
245 },
246 }),
247 queryInference2: (() => {
248 const query = build.query({
249 query: (x: 'Arg') => 'From' as const,
250 transformResponse(r: 'To') {
251 return 'RetVal' as const
252 },
253 })
254 return query
255 })(),
256 }),
257 })
258 })
259 test('mutation: query & transformResponse types', () => {
260 api.injectEndpoints({
261 endpoints: (build) => ({
262 query: build.mutation<'RetVal', 'Arg'>({
263 query: (x: 'Arg') => 'From' as const,
264 transformResponse(r: 'To') {
265 return 'RetVal' as const
266 },
267 }),
268 query1: build.mutation<'RetVal', 'Arg'>({
269 // @ts-expect-error
270 query: (x: 'Error') => 'From' as const,
271 transformResponse(r: 'To') {
272 return 'RetVal' as const
273 },
274 }),
275 query2: build.mutation<'RetVal', 'Arg'>({
276 // @ts-expect-error
277 query: (x: 'Arg') => 'Error' as const,
278 transformResponse(r: 'To') {
279 return 'RetVal' as const
280 },
281 }),
282 query3: build.mutation<'RetVal', 'Arg'>({
283 query: (x: 'Arg') => 'From' as const,
284 // @ts-expect-error
285 transformResponse(r: 'Error') {
286 return 'RetVal' as const
287 },
288 }),
289 query4: build.mutation<'RetVal', 'Arg'>({
290 query: (x: 'Arg') => 'From' as const,
291 // @ts-expect-error
292 transformResponse(r: 'To') {
293 return 'Error' as const
294 },
295 }),
296 mutationInference1: build.mutation<'RetVal', 'Arg'>({
297 query: (x) => {
298 return 'From'
299 },
300 transformResponse(r) {
301 return 'RetVal'
302 },
303 }),
304 mutationInference2: (() => {
305 const query = build.mutation({
306 query: (x: 'Arg') => 'From' as const,
307 transformResponse(r: 'To') {
308 return 'RetVal' as const
309 },
310 })
311 return query
312 })(),
313 }),
314 })
315 })
316
317 describe('enhancing endpoint definitions', () => {
318 const baseQuery = vi.fn((x: string) => ({ data: 'success' }))
319 const commonBaseQueryApi = {
320 dispatch: expect.any(Function),
321 endpoint: expect.any(String),
322 abort: expect.any(Function),
323 extra: undefined,
324 forced: expect.any(Boolean),
325 getState: expect.any(Function),
326 signal: expect.any(Object),
327 type: expect.any(String),
328 queryCacheKey: expect.any(String),
329 }
330 beforeEach(() => {
331 baseQuery.mockClear()
332 })
333 function getNewApi() {
334 return createApi({
335 baseQuery,
336 tagTypes: ['old'],
337 endpoints: (build) => ({
338 query1: build.query<'out1', 'in1'>({ query: (id) => `${id}` }),
339 query2: build.query<'out2', 'in2'>({ query: (id) => `${id}` }),
340 mutation1: build.mutation<'out1', 'in1'>({ query: (id) => `${id}` }),
341 mutation2: build.mutation<'out2', 'in2'>({ query: (id) => `${id}` }),
342 }),
343 })
344 }
345 let api = getNewApi()
346 beforeEach(() => {
347 api = getNewApi()
348 })
349
350 test('pre-modification behavior', async () => {
351 const storeRef = setupApiStore(api, undefined, {
352 withoutTestLifecycles: true,
353 })
354 storeRef.store.dispatch(api.endpoints.query1.initiate('in1'))
355 storeRef.store.dispatch(api.endpoints.query2.initiate('in2'))
356 storeRef.store.dispatch(api.endpoints.mutation1.initiate('in1'))
357 storeRef.store.dispatch(api.endpoints.mutation2.initiate('in2'))
358
359 expect(baseQuery.mock.calls).toEqual([
360 [
361 'in1',
362 {
363 dispatch: expect.any(Function),
364 endpoint: expect.any(String),
365 getState: expect.any(Function),
366 signal: expect.any(Object),
367 abort: expect.any(Function),
368 forced: expect.any(Boolean),
369 type: expect.any(String),
370 queryCacheKey: expect.any(String),
371 },
372 undefined,
373 ],
374 [
375 'in2',
376 {
377 dispatch: expect.any(Function),
378 endpoint: expect.any(String),
379 getState: expect.any(Function),
380 signal: expect.any(Object),
381 abort: expect.any(Function),
382 forced: expect.any(Boolean),
383 type: expect.any(String),
384 queryCacheKey: expect.any(String),
385 },
386 undefined,
387 ],
388 [
389 'in1',
390 {
391 dispatch: expect.any(Function),
392 endpoint: expect.any(String),
393 getState: expect.any(Function),
394 signal: expect.any(Object),
395 abort: expect.any(Function),
396 // forced: undefined,
397 type: expect.any(String),
398 },
399 undefined,
400 ],
401 [
402 'in2',
403 {
404 dispatch: expect.any(Function),
405 endpoint: expect.any(String),
406 getState: expect.any(Function),
407 signal: expect.any(Object),
408 abort: expect.any(Function),
409 // forced: undefined,
410 type: expect.any(String),
411 },
412 undefined,
413 ],
414 ])
415 })
416
417 test('warn on wrong tagType', async () => {
418 vi.stubEnv('NODE_ENV', 'development')
419
420 const storeRef = setupApiStore(api, undefined, {
421 withoutTestLifecycles: true,
422 })
423 // only type-test this part
424 if (2 > 1) {
425 api.enhanceEndpoints({
426 endpoints: {
427 query1: {
428 // @ts-expect-error
429 providesTags: ['new'],
430 },
431 query2: {
432 // @ts-expect-error
433 providesTags: ['missing'],
434 },
435 },
436 })
437 }
438
439 const enhanced = api.enhanceEndpoints({
440 addTagTypes: ['new'],
441 endpoints: {
442 query1: {
443 providesTags: ['new'],
444 },
445 query2: {
446 // @ts-expect-error
447 providesTags: ['missing'],
448 },
449 },
450 })
451
452 storeRef.store.dispatch(api.endpoints.query1.initiate('in1'))
453 await delay(1)
454 expect(consoleErrorSpy).not.toHaveBeenCalled()
455
456 storeRef.store.dispatch(api.endpoints.query2.initiate('in2'))
457 await delay(1)
458
459 expect(consoleErrorSpy).toHaveBeenCalledOnce()
460
461 expect(consoleErrorSpy).toHaveBeenLastCalledWith(
462 "Tag type 'missing' was used, but not specified in `tagTypes`!",
463 )
464
465 // only type-test this part
466 if (2 > 1) {
467 enhanced.enhanceEndpoints({
468 endpoints: {
469 query1: {
470 // returned `enhanced` api contains "new" enitityType
471 providesTags: ['new'],
472 },
473 query2: {
474 // @ts-expect-error
475 providesTags: ['missing'],
476 },
477 },
478 })
479 }
480 })
481
482 test('modify', () => {
483 const storeRef = setupApiStore(api, undefined, {
484 withoutTestLifecycles: true,
485 })
486 api.enhanceEndpoints({
487 endpoints: {
488 query1: {
489 query: (x) => {
490 return 'modified1'
491 },
492 },
493 query2(definition) {
494 definition.query = (x) => {
495 return 'modified2'
496 }
497 },
498 mutation1: {
499 query: (x) => {
500 return 'modified1'
501 },
502 },
503 mutation2(definition) {
504 definition.query = (x) => {
505 return 'modified2'
506 }
507 },
508 // @ts-expect-error
509 nonExisting: {},
510 },
511 })
512
513 storeRef.store.dispatch(api.endpoints.query1.initiate('in1'))
514 storeRef.store.dispatch(api.endpoints.query2.initiate('in2'))
515 storeRef.store.dispatch(api.endpoints.mutation1.initiate('in1'))
516 storeRef.store.dispatch(api.endpoints.mutation2.initiate('in2'))
517
518 expect(baseQuery.mock.calls).toEqual([
519 ['modified1', commonBaseQueryApi, undefined],
520 ['modified2', commonBaseQueryApi, undefined],
521 [
522 'modified1',
523 {
524 ...commonBaseQueryApi,
525 forced: undefined,
526 queryCacheKey: undefined,
527 },
528 undefined,
529 ],
530 [
531 'modified2',
532 {
533 ...commonBaseQueryApi,
534 forced: undefined,
535 queryCacheKey: undefined,
536 },
537 undefined,
538 ],
539 ])
540 })
541
542 test('updated transform response types', async () => {
543 const baseApi = createApi({
544 baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
545 tagTypes: ['old'],
546 endpoints: (build) => ({
547 query1: build.query<'out1', void>({ query: () => 'success' }),
548 mutation1: build.mutation<'out1', void>({ query: () => 'success' }),
549 }),
550 })
551
552 type Transformed = { value: string }
553
554 type Definitions = DefinitionsFromApi<typeof api>
555 type TagTypes = TagTypesFromApi<typeof api>
556
557 type Q1Definition = OverrideResultType<Definitions['query1'], Transformed>
558 type M1Definition = OverrideResultType<
559 Definitions['mutation1'],
560 Transformed
561 >
562
563 type UpdatedDefitions = Omit<Definitions, 'query1' | 'mutation1'> & {
564 query1: Q1Definition
565 mutation1: M1Definition
566 }
567
568 const enhancedApi = baseApi.enhanceEndpoints<TagTypes, UpdatedDefitions>({
569 endpoints: {
570 query1: {
571 transformResponse: (a, b, c) => ({
572 value: 'transformed',
573 }),
574 },
575 mutation1: {
576 transformResponse: (a, b, c) => ({
577 value: 'transformed',
578 }),
579 },
580 },
581 })
582
583 const storeRef = setupApiStore(enhancedApi, undefined, {
584 withoutTestLifecycles: true,
585 })
586
587 const queryResponse = await storeRef.store.dispatch(
588 enhancedApi.endpoints.query1.initiate(),
589 )
590 expect(queryResponse.data).toEqual({ value: 'transformed' })
591
592 const mutationResponse = await storeRef.store.dispatch(
593 enhancedApi.endpoints.mutation1.initiate(),
594 )
595 expect('data' in mutationResponse && mutationResponse.data).toEqual({
596 value: 'transformed',
597 })
598 })
599 })
600})
601
602describe('additional transformResponse behaviors', () => {
603 type SuccessResponse = { value: 'success' }
604 type EchoResponseData = { banana: 'bread' }
605 type ErrorResponse = { value: 'error' }
606 const api = createApi({
607 baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
608 endpoints: (build) => ({
609 echo: build.mutation({
610 query: () => ({ method: 'PUT', url: '/echo' }),
611 }),
612 mutation: build.mutation({
613 query: () => ({
614 url: '/echo',
615 method: 'POST',
616 body: { nested: { banana: 'bread' } },
617 }),
618 transformResponse: (response: { body: { nested: EchoResponseData } }) =>
619 response.body.nested,
620 }),
621 mutationWithError: build.mutation({
622 query: () => ({
623 url: '/error',
624 method: 'POST',
625 }),
626 transformErrorResponse: (response) => {
627 const data = response.data as ErrorResponse
628 return data.value
629 },
630 }),
631 mutationWithMeta: build.mutation({
632 query: () => ({
633 url: '/echo',
634 method: 'POST',
635 body: { nested: { banana: 'bread' } },
636 }),
637 transformResponse: (
638 response: { body: { nested: EchoResponseData } },
639 meta,
640 ) => {
641 return {
642 ...response.body.nested,
643 meta: {
644 request: { headers: getSerializedHeaders(meta?.request.headers) },
645 response: {
646 headers: getSerializedHeaders(meta?.response?.headers),
647 },
648 },
649 }
650 },
651 }),
652 query: build.query<SuccessResponse & EchoResponseData, void>({
653 query: () => '/success',
654 transformResponse: async (response: SuccessResponse) => {
655 const res: any = await nodeFetch('https://example.com/echo', {
656 method: 'POST',
657 body: JSON.stringify({ banana: 'bread' }),
658 }).then((res) => res.json())
659
660 const additionalData = res.body as EchoResponseData
661 return { ...response, ...additionalData }
662 },
663 }),
664 queryWithMeta: build.query<SuccessResponse, void>({
665 query: () => '/success',
666 transformResponse: async (response: SuccessResponse, meta) => {
667 return {
668 ...response,
669 meta: {
670 request: { headers: getSerializedHeaders(meta?.request.headers) },
671 response: {
672 headers: getSerializedHeaders(meta?.response?.headers),
673 },
674 },
675 }
676 },
677 }),
678 }),
679 })
680
681 const storeRef = setupApiStore(api)
682
683 test('transformResponse handles an async transformation and returns the merged data (query)', async () => {
684 const result = await storeRef.store.dispatch(api.endpoints.query.initiate())
685
686 expect(result.data).toEqual({ value: 'success', banana: 'bread' })
687 })
688
689 test('transformResponse transforms a response from a mutation', async () => {
690 const result = await storeRef.store.dispatch(
691 api.endpoints.mutation.initiate({}),
692 )
693
694 expect('data' in result && result.data).toEqual({ banana: 'bread' })
695 })
696
697 test('transformResponse transforms a response from a mutation with an error', async () => {
698 const result = await storeRef.store.dispatch(
699 api.endpoints.mutationWithError.initiate({}),
700 )
701
702 expect('error' in result && result.error).toEqual('error')
703 })
704
705 test('transformResponse can inject baseQuery meta into the end result from a mutation', async () => {
706 const result = await storeRef.store.dispatch(
707 api.endpoints.mutationWithMeta.initiate({}),
708 )
709
710 expect('data' in result && result.data).toEqual({
711 banana: 'bread',
712 meta: {
713 request: {
714 headers: {
715 accept: 'application/json',
716 'content-type': 'application/json',
717 },
718 },
719 response: {
720 headers: {
721 'content-type': 'application/json',
722 },
723 },
724 },
725 })
726 })
727
728 test('transformResponse can inject baseQuery meta into the end result from a query', async () => {
729 const result = await storeRef.store.dispatch(
730 api.endpoints.queryWithMeta.initiate(),
731 )
732
733 expect(result.data).toEqual({
734 value: 'success',
735 meta: {
736 request: {
737 headers: {
738 accept: 'application/json',
739 },
740 },
741 response: {
742 headers: {
743 'content-type': 'application/json',
744 },
745 },
746 },
747 })
748 })
749})
750
751describe('query endpoint lifecycles - onStart, onSuccess, onError', () => {
752 const initialState = {
753 count: null as null | number,
754 }
755 const setCount = createAction<number>('setCount')
756 const testReducer = createReducer(initialState, (builder) => {
757 builder.addCase(setCount, (state, action) => {
758 state.count = action.payload
759 })
760 })
761
762 type SuccessResponse = { value: 'success' }
763 const api = createApi({
764 baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
765 endpoints: (build) => ({
766 echo: build.mutation({
767 query: () => ({ method: 'PUT', url: '/echo' }),
768 }),
769 query: build.query<SuccessResponse, void>({
770 query: () => '/success',
771 async onQueryStarted(_, api) {
772 api.dispatch(setCount(0))
773 try {
774 await api.queryFulfilled
775 api.dispatch(setCount(1))
776 } catch {
777 api.dispatch(setCount(-1))
778 }
779 },
780 }),
781 mutation: build.mutation<SuccessResponse, void>({
782 query: () => ({ url: '/success', method: 'POST' }),
783 async onQueryStarted(_, api) {
784 api.dispatch(setCount(0))
785 try {
786 await api.queryFulfilled
787 api.dispatch(setCount(1))
788 } catch {
789 api.dispatch(setCount(-1))
790 }
791 },
792 }),
793 }),
794 })
795
796 const storeRef = setupApiStore(api, { testReducer })
797
798 test('query lifecycle events fire properly', async () => {
799 // We intentionally fail the first request so we can test all lifecycles
800 server.use(
801 http.get(
802 'https://example.com/success',
803 () => HttpResponse.json({ value: 'failed' }, { status: 500 }),
804 { once: true },
805 ),
806 )
807
808 expect(storeRef.store.getState().testReducer.count).toBe(null)
809 const failAttempt = storeRef.store.dispatch(api.endpoints.query.initiate())
810 expect(storeRef.store.getState().testReducer.count).toBe(0)
811 await failAttempt
812 await delay(10)
813 expect(storeRef.store.getState().testReducer.count).toBe(-1)
814
815 const successAttempt = storeRef.store.dispatch(
816 api.endpoints.query.initiate(),
817 )
818 expect(storeRef.store.getState().testReducer.count).toBe(0)
819 await successAttempt
820 await delay(10)
821 expect(storeRef.store.getState().testReducer.count).toBe(1)
822 })
823
824 test('mutation lifecycle events fire properly', async () => {
825 // We intentionally fail the first request so we can test all lifecycles
826 server.use(
827 http.post(
828 'https://example.com/success',
829 () => HttpResponse.json({ value: 'failed' }, { status: 500 }),
830 { once: true },
831 ),
832 )
833
834 expect(storeRef.store.getState().testReducer.count).toBe(null)
835 const failAttempt = storeRef.store.dispatch(
836 api.endpoints.mutation.initiate(),
837 )
838 expect(storeRef.store.getState().testReducer.count).toBe(0)
839 await failAttempt
840 expect(storeRef.store.getState().testReducer.count).toBe(-1)
841
842 const successAttempt = storeRef.store.dispatch(
843 api.endpoints.mutation.initiate(),
844 )
845 expect(storeRef.store.getState().testReducer.count).toBe(0)
846 await successAttempt
847 expect(storeRef.store.getState().testReducer.count).toBe(1)
848 })
849})
850
851test('providesTags and invalidatesTags can use baseQueryMeta', async () => {
852 let _meta: FetchBaseQueryMeta | undefined
853
854 type SuccessResponse = { value: 'success' }
855
856 const api = createApi({
857 baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
858 tagTypes: ['success'],
859 endpoints: (build) => ({
860 query: build.query<SuccessResponse, void>({
861 query: () => '/success',
862 providesTags: (_result, _error, _arg, meta) => {
863 _meta = meta
864 return ['success']
865 },
866 }),
867 mutation: build.mutation<SuccessResponse, void>({
868 query: () => ({ url: '/success', method: 'POST' }),
869 invalidatesTags: (_result, _error, _arg, meta) => {
870 _meta = meta
871 return ['success']
872 },
873 }),
874 }),
875 })
876
877 const storeRef = setupApiStore(api, undefined, {
878 withoutTestLifecycles: true,
879 })
880
881 await storeRef.store.dispatch(api.endpoints.query.initiate())
882 expect('request' in _meta! && 'response' in _meta!).toBe(true)
883
884 _meta = undefined
885
886 await storeRef.store.dispatch(api.endpoints.mutation.initiate())
887
888 expect('request' in _meta! && 'response' in _meta!).toBe(true)
889})
890
891describe('structuralSharing flag behaviors', () => {
892 type SuccessResponse = { value: 'success' }
893
894 const api = createApi({
895 baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
896 tagTypes: ['success'],
897 endpoints: (build) => ({
898 enabled: build.query<SuccessResponse, void>({
899 query: () => '/success',
900 }),
901 disabled: build.query<SuccessResponse, void>({
902 query: () => ({ url: '/success' }),
903 structuralSharing: false,
904 }),
905 }),
906 })
907
908 const storeRef = setupApiStore(api)
909
910 it('enables structural sharing for query endpoints by default', async () => {
911 await storeRef.store.dispatch(api.endpoints.enabled.initiate())
912 const firstRef = api.endpoints.enabled.select()(storeRef.store.getState())
913
914 await storeRef.store.dispatch(
915 api.endpoints.enabled.initiate(undefined, { forceRefetch: true }),
916 )
917
918 const secondRef = api.endpoints.enabled.select()(storeRef.store.getState())
919
920 expect(firstRef.requestId).not.toEqual(secondRef.requestId)
921 expect(firstRef.data === secondRef.data).toBeTruthy()
922 })
923
924 it('allows a query endpoint to opt-out of structural sharing', async () => {
925 await storeRef.store.dispatch(api.endpoints.disabled.initiate())
926 const firstRef = api.endpoints.disabled.select()(storeRef.store.getState())
927
928 await storeRef.store.dispatch(
929 api.endpoints.disabled.initiate(undefined, { forceRefetch: true }),
930 )
931
932 const secondRef = api.endpoints.disabled.select()(storeRef.store.getState())
933
934 expect(firstRef.requestId).not.toEqual(secondRef.requestId)
935 expect(firstRef.data === secondRef.data).toBeFalsy()
936 })
937})
938
939describe('custom serializeQueryArgs per endpoint', () => {
940 const customArgsSerializer: SerializeQueryArgs<number> = ({
941 endpointName,
942 queryArgs,
943 }) => `${endpointName}-${queryArgs}`
944
945 type SuccessResponse = { value: 'success' }
946
947 const serializer1 = vi.fn(customArgsSerializer)
948
949 interface MyApiClient {
950 fetchPost: (id: string) => Promise<SuccessResponse>
951 }
952
953 const dummyClient: MyApiClient = {
954 async fetchPost() {
955 return { value: 'success' }
956 },
957 }
958
959 const api = createApi({
960 baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
961 serializeQueryArgs: ({ endpointName, queryArgs }) =>
962 `base-${endpointName}-${queryArgs}`,
963 endpoints: (build) => ({
964 queryWithNoSerializer: build.query<SuccessResponse, number>({
965 query: (arg) => `${arg}`,
966 }),
967 queryWithCustomSerializer: build.query<SuccessResponse, number>({
968 query: (arg) => `${arg}`,
969 serializeQueryArgs: serializer1,
970 }),
971 queryWithCustomObjectSerializer: build.query<
972 SuccessResponse,
973 { id: number; client: MyApiClient }
974 >({
975 query: (arg) => `${arg.id}`,
976 serializeQueryArgs: ({
977 endpointDefinition,
978 endpointName,
979 queryArgs,
980 }) => {
981 const { id } = queryArgs
982 return { id }
983 },
984 }),
985 queryWithCustomNumberSerializer: build.query<
986 SuccessResponse,
987 { id: number; client: MyApiClient }
988 >({
989 query: (arg) => `${arg.id}`,
990 serializeQueryArgs: ({
991 endpointDefinition,
992 endpointName,
993 queryArgs,
994 }) => {
995 const { id } = queryArgs
996 return id
997 },
998 }),
999 listItems: build.query<string[], number>({
1000 query: (pageNumber) => `/listItems?page=${pageNumber}`,
1001 serializeQueryArgs: ({ endpointName }) => {
1002 return endpointName
1003 },
1004 merge: (currentCache, newItems) => {
1005 currentCache.push(...newItems)
1006 },
1007 forceRefetch({ currentArg, previousArg }) {
1008 return currentArg !== previousArg
1009 },
1010 }),
1011 listItems2: build.query<{ items: string[]; meta?: any }, number>({
1012 query: (pageNumber) => `/listItems2?page=${pageNumber}`,
1013 serializeQueryArgs: ({ endpointName }) => {
1014 return endpointName
1015 },
1016 transformResponse(items: string[]) {
1017 return { items }
1018 },
1019 merge: (currentCache, newData, meta) => {
1020 currentCache.items.push(...newData.items)
1021 currentCache.meta = meta
1022 },
1023 forceRefetch({ currentArg, previousArg }) {
1024 return currentArg !== previousArg
1025 },
1026 }),
1027 }),
1028 })
1029
1030 const storeRef = setupApiStore(api)
1031
1032 it('Works via createApi', async () => {
1033 await storeRef.store.dispatch(
1034 api.endpoints.queryWithNoSerializer.initiate(99),
1035 )
1036
1037 expect(serializer1).not.toHaveBeenCalled()
1038
1039 await storeRef.store.dispatch(
1040 api.endpoints.queryWithCustomSerializer.initiate(42),
1041 )
1042
1043 expect(serializer1).toHaveBeenCalled()
1044
1045 expect(
1046 storeRef.store.getState().api.queries['base-queryWithNoSerializer-99'],
1047 ).toBeTruthy()
1048
1049 expect(
1050 storeRef.store.getState().api.queries['queryWithCustomSerializer-42'],
1051 ).toBeTruthy()
1052 })
1053
1054 const serializer2 = vi.fn(customArgsSerializer)
1055
1056 const injectedApi = api.injectEndpoints({
1057 endpoints: (build) => ({
1058 injectedQueryWithCustomSerializer: build.query<SuccessResponse, number>({
1059 query: (arg) => `${arg}`,
1060 serializeQueryArgs: serializer2,
1061 }),
1062 }),
1063 })
1064
1065 it('Works via injectEndpoints', async () => {
1066 expect(serializer2).not.toHaveBeenCalled()
1067
1068 await storeRef.store.dispatch(
1069 injectedApi.endpoints.injectedQueryWithCustomSerializer.initiate(5),
1070 )
1071
1072 expect(serializer2).toHaveBeenCalled()
1073 expect(
1074 storeRef.store.getState().api.queries[
1075 'injectedQueryWithCustomSerializer-5'
1076 ],
1077 ).toBeTruthy()
1078 })
1079
1080 test('Serializes a returned object for query args', async () => {
1081 await storeRef.store.dispatch(
1082 api.endpoints.queryWithCustomObjectSerializer.initiate({
1083 id: 42,
1084 client: dummyClient,
1085 }),
1086 )
1087
1088 expect(
1089 storeRef.store.getState().api.queries[
1090 'queryWithCustomObjectSerializer({"id":42})'
1091 ],
1092 ).toBeTruthy()
1093 })
1094
1095 test('Serializes a returned primitive for query args', async () => {
1096 await storeRef.store.dispatch(
1097 api.endpoints.queryWithCustomNumberSerializer.initiate({
1098 id: 42,
1099 client: dummyClient,
1100 }),
1101 )
1102
1103 expect(
1104 storeRef.store.getState().api.queries[
1105 'queryWithCustomNumberSerializer(42)'
1106 ],
1107 ).toBeTruthy()
1108 })
1109
1110 test('serializeQueryArgs + merge allows refetching as args change with same cache key', async () => {
1111 const allItems = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'i']
1112 const PAGE_SIZE = 3
1113
1114 server.use(
1115 http.get('https://example.com/listItems', ({ request }) => {
1116 const url = new URL(request.url)
1117 const pageString = url.searchParams.get('page')
1118 const pageNum = parseInt(pageString || '0')
1119
1120 const results = paginate(allItems, PAGE_SIZE, pageNum)
1121 return HttpResponse.json(results)
1122 }),
1123 )
1124
1125 // Page number shouldn't matter here, because the cache key ignores that.
1126 // We just need to select the only cache entry.
1127 const selectListItems = api.endpoints.listItems.select(0)
1128
1129 await storeRef.store.dispatch(api.endpoints.listItems.initiate(1))
1130
1131 const initialEntry = selectListItems(storeRef.store.getState())
1132 expect(initialEntry.data).toEqual(['a', 'b', 'c'])
1133
1134 await storeRef.store.dispatch(api.endpoints.listItems.initiate(2))
1135 const updatedEntry = selectListItems(storeRef.store.getState())
1136 expect(updatedEntry.data).toEqual(['a', 'b', 'c', 'd', 'e', 'f'])
1137 })
1138
1139 test('merge receives a meta object as an argument', async () => {
1140 const allItems = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'i']
1141 const PAGE_SIZE = 3
1142
1143 server.use(
1144 http.get('https://example.com/listItems2', ({ request }) => {
1145 const url = new URL(request.url)
1146 const pageString = url.searchParams.get('page')
1147 const pageNum = parseInt(pageString || '0')
1148
1149 const results = paginate(allItems, PAGE_SIZE, pageNum)
1150 return HttpResponse.json(results)
1151 }),
1152 )
1153
1154 const selectListItems = api.endpoints.listItems2.select(0)
1155
1156 await storeRef.store.dispatch(api.endpoints.listItems2.initiate(1))
1157 await storeRef.store.dispatch(api.endpoints.listItems2.initiate(2))
1158 const cacheEntry = selectListItems(storeRef.store.getState())
1159
1160 // Should have passed along the third arg from `merge` containing these fields
1161 expect(cacheEntry.data?.meta).toEqual({
1162 requestId: expect.any(String),
1163 fulfilledTimeStamp: expect.any(Number),
1164 arg: 2,
1165 baseQueryMeta: expect.any(Object),
1166 })
1167 })
1168})
1169
1170describe('timeout behavior', () => {
1171 test('triggers TIMEOUT_ERROR', async () => {
1172 const api = createApi({
1173 baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com', timeout: 5 }),
1174 endpoints: (build) => ({
1175 query: build.query<unknown, void>({
1176 query: () => '/success',
1177 }),
1178 }),
1179 })
1180
1181 const storeRef = setupApiStore(api, undefined, {
1182 withoutTestLifecycles: true,
1183 })
1184
1185 server.use(
1186 http.get(
1187 'https://example.com/success',
1188 async () => {
1189 await delay(50)
1190 return HttpResponse.json({ value: 'failed' }, { status: 500 })
1191 },
1192 { once: true },
1193 ),
1194 )
1195
1196 const result = await storeRef.store.dispatch(api.endpoints.query.initiate())
1197
1198 expect(result?.error).toEqual({
1199 status: 'TIMEOUT_ERROR',
1200 error: expect.stringMatching(/^TimeoutError/),
1201 })
1202 })
1203})
1204
1205describe('endpoint schemas', () => {
1206 const schemaConverter: SchemaFailureConverter<
1207 ReturnType<typeof fetchBaseQuery>
1208 > = (error) => {
1209 return {
1210 status: 'CUSTOM_ERROR',
1211 error: error.schemaName + ' failed validation',
1212 data: error.issues,
1213 }
1214 }
1215
1216 const serializedSchemaError = {
1217 name: 'SchemaError',
1218 message: expect.any(String),
1219 stack: expect.any(String),
1220 } satisfies SerializedError
1221
1222 const onSchemaFailureGlobal = vi.fn<SchemaFailureHandler>()
1223 const onSchemaFailureEndpoint = vi.fn<SchemaFailureHandler>()
1224 afterEach(() => {
1225 onSchemaFailureGlobal.mockClear()
1226 onSchemaFailureEndpoint.mockClear()
1227 })
1228
1229 function expectFailureHandlersToHaveBeenCalled({
1230 schemaName,
1231 value,
1232 arg,
1233 }: {
1234 schemaName: `${SchemaType}Schema`
1235 value: unknown
1236 arg: unknown
1237 }) {
1238 for (const handler of [onSchemaFailureGlobal, onSchemaFailureEndpoint]) {
1239 expect(handler).toHaveBeenCalledOnce()
1240 const [namedError, info] = handler.mock.calls[0]
1241 expect(namedError).toBeInstanceOf(NamedSchemaError)
1242 expect(namedError.issues.length).toBeGreaterThan(0)
1243 expect(namedError.value).toEqual(value)
1244 expect(namedError.schemaName).toBe(schemaName)
1245 expect(info.endpoint).toBe('query')
1246 expect(info.type).toBe('query')
1247 expect(info.arg).toEqual(arg)
1248 }
1249 }
1250
1251 interface SkipApiOptions {
1252 globalSkip?: boolean
1253 endpointSkip?: boolean
1254 useArray?: boolean
1255 globalCatch?: boolean
1256 endpointCatch?: boolean
1257 }
1258
1259 const apiOptions = (
1260 type: SchemaType,
1261 { useArray, globalSkip, globalCatch }: SkipApiOptions = {},
1262 ) => ({
1263 onSchemaFailure: onSchemaFailureGlobal,
1264 skipSchemaValidation: useArray ? globalSkip && [type] : globalSkip,
1265 catchSchemaFailure: globalCatch ? schemaConverter : undefined,
1266 })
1267
1268 const endpointOptions = (
1269 type: SchemaType,
1270 { useArray, endpointSkip, endpointCatch }: SkipApiOptions = {},
1271 ) => ({
1272 onSchemaFailure: onSchemaFailureEndpoint,
1273 skipSchemaValidation: useArray ? endpointSkip && [type] : endpointSkip,
1274 catchSchemaFailure: endpointCatch ? schemaConverter : undefined,
1275 })
1276
1277 const skipCases: [string, SkipApiOptions][] = [
1278 ['globally', { globalSkip: true }],
1279 ['on the endpoint', { endpointSkip: true }],
1280 ['globally (array)', { globalSkip: true, useArray: true }],
1281 ['on the endpoint (array)', { endpointSkip: true, useArray: true }],
1282 ]
1283
1284 describe('argSchema', () => {
1285 const makeApi = (opts?: SkipApiOptions) =>
1286 createApi({
1287 baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
1288 ...apiOptions('arg', opts),
1289 endpoints: (build) => ({
1290 query: build.query<unknown, { id: number }>({
1291 query: ({ id }) => `/post/${id}`,
1292 argSchema: v.object({ id: v.number() }),
1293 ...endpointOptions('arg', opts),
1294 }),
1295 }),
1296 })
1297 test("can be used to validate the endpoint's arguments", async () => {
1298 const api = makeApi()
1299
1300 const storeRef = setupApiStore(api, undefined, {
1301 withoutTestLifecycles: true,
1302 })
1303
1304 const result = await storeRef.store.dispatch(
1305 api.endpoints.query.initiate({ id: 1 }),
1306 )
1307
1308 expect(result?.error).toBeUndefined()
1309
1310 const invalidResult = await storeRef.store.dispatch(
1311 // @ts-expect-error
1312 api.endpoints.query.initiate({ id: '1' }),
1313 )
1314
1315 expect(invalidResult?.error).toEqual(serializedSchemaError)
1316 expectFailureHandlersToHaveBeenCalled({
1317 schemaName: 'argSchema',
1318 value: { id: '1' },
1319 arg: { id: '1' },
1320 })
1321 })
1322
1323 test.each(skipCases)('can be skipped %s', async (_, arg) => {
1324 const api = makeApi(arg)
1325
1326 const storeRef = setupApiStore(api, undefined, {
1327 withoutTestLifecycles: true,
1328 })
1329
1330 const result = await storeRef.store.dispatch(
1331 // @ts-expect-error
1332 api.endpoints.query.initiate({ id: '1' }),
1333 )
1334
1335 expect(result?.error).toBeUndefined()
1336 })
1337 // we only need to test this once
1338 test('endpoint overrides global skip', async () => {
1339 const api = makeApi({ globalSkip: true, endpointSkip: false })
1340
1341 const storeRef = setupApiStore(api, undefined, {
1342 withoutTestLifecycles: true,
1343 })
1344
1345 const result = await storeRef.store.dispatch(
1346 // @ts-expect-error
1347 api.endpoints.query.initiate({ id: '1' }),
1348 )
1349
1350 expect(result?.error).toEqual(serializedSchemaError)
1351 })
1352
1353 test('can be converted to a standard error object at global level', async () => {
1354 const api = makeApi({ globalCatch: true })
1355 const storeRef = setupApiStore(api, undefined, {
1356 withoutTestLifecycles: true,
1357 })
1358
1359 const result = await storeRef.store.dispatch(
1360 // @ts-expect-error
1361 api.endpoints.query.initiate({ id: '1' }),
1362 )
1363
1364 expect(result?.error).toEqual({
1365 status: 'CUSTOM_ERROR',
1366 error: 'argSchema failed validation',
1367 data: expect.any(Array),
1368 })
1369
1370 expectFailureHandlersToHaveBeenCalled({
1371 schemaName: 'argSchema',
1372 value: { id: '1' },
1373 arg: { id: '1' },
1374 })
1375 })
1376 test('can be converted to a standard error object at endpoint level', async () => {
1377 const api = makeApi({ endpointCatch: true })
1378 const storeRef = setupApiStore(api, undefined, {
1379 withoutTestLifecycles: true,
1380 })
1381
1382 const result = await storeRef.store.dispatch(
1383 // @ts-expect-error
1384 api.endpoints.query.initiate({ id: '1' }),
1385 )
1386
1387 expect(result?.error).toEqual({
1388 status: 'CUSTOM_ERROR',
1389 error: 'argSchema failed validation',
1390 data: expect.any(Array),
1391 })
1392 expectFailureHandlersToHaveBeenCalled({
1393 schemaName: 'argSchema',
1394 value: { id: '1' },
1395 arg: { id: '1' },
1396 })
1397 })
1398 })
1399 describe('rawResponseSchema', () => {
1400 const makeApi = (opts?: SkipApiOptions) =>
1401 createApi({
1402 baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
1403 ...apiOptions('rawResponse', opts),
1404 endpoints: (build) => ({
1405 query: build.query<{ success: boolean }, void>({
1406 query: () => '/success',
1407 rawResponseSchema: v.object({ value: v.literal('success!') }),
1408 ...endpointOptions('rawResponse', opts),
1409 }),
1410 }),
1411 })
1412 test("can be used to validate the endpoint's raw result", async () => {
1413 const api = makeApi()
1414 const storeRef = setupApiStore(api, undefined, {
1415 withoutTestLifecycles: true,
1416 })
1417 const result = await storeRef.store.dispatch(
1418 api.endpoints.query.initiate(),
1419 )
1420 expect(result?.error).toEqual(serializedSchemaError)
1421 expectFailureHandlersToHaveBeenCalled({
1422 schemaName: 'rawResponseSchema',
1423 value: { value: 'success' },
1424 arg: undefined,
1425 })
1426 })
1427 test.each(skipCases)('can be skipped %s', async (_, arg) => {
1428 const api = makeApi(arg)
1429 const storeRef = setupApiStore(api, undefined, {
1430 withoutTestLifecycles: true,
1431 })
1432 const result = await storeRef.store.dispatch(
1433 api.endpoints.query.initiate(),
1434 )
1435 expect(result?.error).toBeUndefined()
1436 })
1437 test('can be skipped on the endpoint', async () => {
1438 const api = makeApi({ endpointSkip: true })
1439 const storeRef = setupApiStore(api, undefined, {
1440 withoutTestLifecycles: true,
1441 })
1442 const result = await storeRef.store.dispatch(
1443 api.endpoints.query.initiate(),
1444 )
1445 expect(result?.error).toBeUndefined()
1446 })
1447 test('can be converted to a standard error object at global level', async () => {
1448 const api = makeApi({ globalCatch: true })
1449 const storeRef = setupApiStore(api, undefined, {
1450 withoutTestLifecycles: true,
1451 })
1452 const result = await storeRef.store.dispatch(
1453 api.endpoints.query.initiate(),
1454 )
1455 expect(result?.error).toEqual({
1456 status: 'CUSTOM_ERROR',
1457 error: 'rawResponseSchema failed validation',
1458 data: expect.any(Array),
1459 })
1460 expectFailureHandlersToHaveBeenCalled({
1461 schemaName: 'rawResponseSchema',
1462 value: { value: 'success' },
1463 arg: undefined,
1464 })
1465 })
1466 test('can be converted to a standard error object at endpoint level', async () => {
1467 const api = makeApi({ endpointCatch: true })
1468 const storeRef = setupApiStore(api, undefined, {
1469 withoutTestLifecycles: true,
1470 })
1471 const result = await storeRef.store.dispatch(
1472 api.endpoints.query.initiate(),
1473 )
1474 expect(result?.error).toEqual({
1475 status: 'CUSTOM_ERROR',
1476 error: 'rawResponseSchema failed validation',
1477 data: expect.any(Array),
1478 })
1479 expectFailureHandlersToHaveBeenCalled({
1480 schemaName: 'rawResponseSchema',
1481 value: { value: 'success' },
1482 arg: undefined,
1483 })
1484 })
1485 })
1486 describe('responseSchema', () => {
1487 const makeApi = (opts?: SkipApiOptions) =>
1488 createApi({
1489 baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
1490 ...apiOptions('response', opts),
1491 endpoints: (build) => ({
1492 query: build.query<{ success: boolean }, void>({
1493 query: () => '/success',
1494 transformResponse: () => ({ success: false }),
1495 responseSchema: v.object({ success: v.literal(true) }),
1496 ...endpointOptions('response', opts),
1497 }),
1498 }),
1499 })
1500 test("can be used to validate the endpoint's final result", async () => {
1501 const api = makeApi()
1502 const storeRef = setupApiStore(api, undefined, {
1503 withoutTestLifecycles: true,
1504 })
1505 const result = await storeRef.store.dispatch(
1506 api.endpoints.query.initiate(),
1507 )
1508 expect(result?.error).toEqual(serializedSchemaError)
1509
1510 expectFailureHandlersToHaveBeenCalled({
1511 schemaName: 'responseSchema',
1512 value: { success: false },
1513 arg: undefined,
1514 })
1515 })
1516 test.each(skipCases)('can be skipped %s', async (_, arg) => {
1517 const api = makeApi(arg)
1518 const storeRef = setupApiStore(api, undefined, {
1519 withoutTestLifecycles: true,
1520 })
1521 const result = await storeRef.store.dispatch(
1522 api.endpoints.query.initiate(),
1523 )
1524 expect(result?.error).toBeUndefined()
1525 })
1526 test('can be converted to a standard error object at global level', async () => {
1527 const api = makeApi({ globalCatch: true })
1528 const storeRef = setupApiStore(api, undefined, {
1529 withoutTestLifecycles: true,
1530 })
1531 const result = await storeRef.store.dispatch(
1532 api.endpoints.query.initiate(),
1533 )
1534 expect(result?.error).toEqual({
1535 status: 'CUSTOM_ERROR',
1536 error: 'responseSchema failed validation',
1537 data: expect.any(Array),
1538 })
1539 expectFailureHandlersToHaveBeenCalled({
1540 schemaName: 'responseSchema',
1541 value: { success: false },
1542 arg: undefined,
1543 })
1544 })
1545 test('can be converted to a standard error object at endpoint level', async () => {
1546 const api = makeApi({ endpointCatch: true })
1547 const storeRef = setupApiStore(api, undefined, {
1548 withoutTestLifecycles: true,
1549 })
1550 const result = await storeRef.store.dispatch(
1551 api.endpoints.query.initiate(),
1552 )
1553 expect(result?.error).toEqual({
1554 status: 'CUSTOM_ERROR',
1555 error: 'responseSchema failed validation',
1556 data: expect.any(Array),
1557 })
1558 expectFailureHandlersToHaveBeenCalled({
1559 schemaName: 'responseSchema',
1560 value: { success: false },
1561 arg: undefined,
1562 })
1563 })
1564 })
1565 describe('rawErrorResponseSchema', () => {
1566 const makeApi = (opts?: SkipApiOptions) =>
1567 createApi({
1568 baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
1569 ...apiOptions('rawErrorResponse', opts),
1570 endpoints: (build) => ({
1571 query: build.query<{ success: boolean }, void>({
1572 query: () => '/error',
1573 rawErrorResponseSchema: v.object({
1574 status: v.pipe(v.number(), v.minValue(400), v.maxValue(499)),
1575 data: v.unknown(),
1576 }),
1577 ...endpointOptions('rawErrorResponse', opts),
1578 }),
1579 }),
1580 })
1581 test("can be used to validate the endpoint's raw error result", async () => {
1582 const api = makeApi()
1583 const storeRef = setupApiStore(api, undefined, {
1584 withoutTestLifecycles: true,
1585 })
1586 const result = await storeRef.store.dispatch(
1587 api.endpoints.query.initiate(),
1588 )
1589 expect(result?.error).toEqual(serializedSchemaError)
1590 expectFailureHandlersToHaveBeenCalled({
1591 schemaName: 'rawErrorResponseSchema',
1592 value: { status: 500, data: { value: 'error' } },
1593 arg: undefined,
1594 })
1595 })
1596 test.each(skipCases)('can be skipped %s', async (_, arg) => {
1597 const api = makeApi(arg)
1598 const storeRef = setupApiStore(api, undefined, {
1599 withoutTestLifecycles: true,
1600 })
1601 const result = await storeRef.store.dispatch(
1602 api.endpoints.query.initiate(),
1603 )
1604 expect(result?.error).not.toEqual(serializedSchemaError)
1605 })
1606 test('can be converted to a standard error object at global level', async () => {
1607 const api = makeApi({ globalCatch: true })
1608 const storeRef = setupApiStore(api, undefined, {
1609 withoutTestLifecycles: true,
1610 })
1611 const result = await storeRef.store.dispatch(
1612 api.endpoints.query.initiate(),
1613 )
1614 expect(result?.error).toEqual({
1615 status: 'CUSTOM_ERROR',
1616 error: 'rawErrorResponseSchema failed validation',
1617 data: expect.any(Array),
1618 })
1619 expectFailureHandlersToHaveBeenCalled({
1620 schemaName: 'rawErrorResponseSchema',
1621 value: { status: 500, data: { value: 'error' } },
1622 arg: undefined,
1623 })
1624 })
1625 test('can be converted to a standard error object at endpoint level', async () => {
1626 const api = makeApi({ endpointCatch: true })
1627 const storeRef = setupApiStore(api, undefined, {
1628 withoutTestLifecycles: true,
1629 })
1630 const result = await storeRef.store.dispatch(
1631 api.endpoints.query.initiate(),
1632 )
1633 expect(result?.error).toEqual({
1634 status: 'CUSTOM_ERROR',
1635 error: 'rawErrorResponseSchema failed validation',
1636 data: expect.any(Array),
1637 })
1638 expectFailureHandlersToHaveBeenCalled({
1639 schemaName: 'rawErrorResponseSchema',
1640 value: { status: 500, data: { value: 'error' } },
1641 arg: undefined,
1642 })
1643 })
1644 })
1645 describe('errorResponseSchema', () => {
1646 const makeApi = (opts?: SkipApiOptions) =>
1647 createApi({
1648 baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
1649 ...apiOptions('errorResponse', opts),
1650 endpoints: (build) => ({
1651 query: build.query<{ success: boolean }, void>({
1652 query: () => '/error',
1653 transformErrorResponse: (error): FetchBaseQueryError => ({
1654 status: 'CUSTOM_ERROR',
1655 data: error,
1656 error: 'whoops',
1657 }),
1658 errorResponseSchema: v.object({
1659 status: v.literal('CUSTOM_ERROR'),
1660 error: v.literal('oh no'),
1661 data: v.unknown(),
1662 }),
1663 ...endpointOptions('errorResponse', opts),
1664 }),
1665 }),
1666 })
1667 test("can be used to validate the endpoint's final error result", async () => {
1668 const api = makeApi()
1669 const storeRef = setupApiStore(api, undefined, {
1670 withoutTestLifecycles: true,
1671 })
1672 const result = await storeRef.store.dispatch(
1673 api.endpoints.query.initiate(),
1674 )
1675 expect(result?.error).toEqual(serializedSchemaError)
1676 expectFailureHandlersToHaveBeenCalled({
1677 schemaName: 'errorResponseSchema',
1678 value: {
1679 status: 'CUSTOM_ERROR',
1680 error: 'whoops',
1681 data: { status: 500, data: { value: 'error' } },
1682 },
1683 arg: undefined,
1684 })
1685 })
1686 test.each(skipCases)('can be skipped %s', async (_, arg) => {
1687 const api = makeApi(arg)
1688 const storeRef = setupApiStore(api, undefined, {
1689 withoutTestLifecycles: true,
1690 })
1691 const result = await storeRef.store.dispatch(
1692 api.endpoints.query.initiate(),
1693 )
1694 expect(result?.error).not.toEqual(serializedSchemaError)
1695 })
1696 test('can be converted to a standard error object at global level', async () => {
1697 const api = makeApi({ globalCatch: true })
1698 const storeRef = setupApiStore(api, undefined, {
1699 withoutTestLifecycles: true,
1700 })
1701 const result = await storeRef.store.dispatch(
1702 api.endpoints.query.initiate(),
1703 )
1704 expect(result?.error).toEqual({
1705 status: 'CUSTOM_ERROR',
1706 error: 'errorResponseSchema failed validation',
1707 data: expect.any(Array),
1708 })
1709 expectFailureHandlersToHaveBeenCalled({
1710 schemaName: 'errorResponseSchema',
1711 value: {
1712 status: 'CUSTOM_ERROR',
1713 error: 'whoops',
1714 data: { status: 500, data: { value: 'error' } },
1715 },
1716 arg: undefined,
1717 })
1718 })
1719 test('can be converted to a standard error object at endpoint level', async () => {
1720 const api = makeApi({ endpointCatch: true })
1721 const storeRef = setupApiStore(api, undefined, {
1722 withoutTestLifecycles: true,
1723 })
1724 const result = await storeRef.store.dispatch(
1725 api.endpoints.query.initiate(),
1726 )
1727 expect(result?.error).toEqual({
1728 status: 'CUSTOM_ERROR',
1729 error: 'errorResponseSchema failed validation',
1730 data: expect.any(Array),
1731 })
1732 expectFailureHandlersToHaveBeenCalled({
1733 schemaName: 'errorResponseSchema',
1734 value: {
1735 status: 'CUSTOM_ERROR',
1736 error: 'whoops',
1737 data: { status: 500, data: { value: 'error' } },
1738 },
1739 arg: undefined,
1740 })
1741 })
1742 })
1743 describe('metaSchema', () => {
1744 const makeApi = (opts?: SkipApiOptions) =>
1745 createApi({
1746 baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
1747 ...apiOptions('meta', opts),
1748 endpoints: (build) => ({
1749 query: build.query<{ success: boolean }, void>({
1750 query: () => '/success',
1751 metaSchema: v.object({
1752 request: v.instance(Request),
1753 response: v.instance(Response),
1754 timestamp: v.number(),
1755 }),
1756 ...endpointOptions('meta', opts),
1757 }),
1758 }),
1759 })
1760 test("can be used to validate the endpoint's meta result", async () => {
1761 const api = makeApi()
1762 const storeRef = setupApiStore(api, undefined, {
1763 withoutTestLifecycles: true,
1764 })
1765 const result = await storeRef.store.dispatch(
1766 api.endpoints.query.initiate(),
1767 )
1768 expect(result?.error).toEqual(serializedSchemaError)
1769 expectFailureHandlersToHaveBeenCalled({
1770 schemaName: 'metaSchema',
1771 value: {
1772 request: expect.any(Request),
1773 response: expect.any(Response),
1774 },
1775 arg: undefined,
1776 })
1777 })
1778 test.each(skipCases)('can be skipped %s', async (_, arg) => {
1779 const api = makeApi(arg)
1780 const storeRef = setupApiStore(api, undefined, {
1781 withoutTestLifecycles: true,
1782 })
1783 const result = await storeRef.store.dispatch(
1784 api.endpoints.query.initiate(),
1785 )
1786 expect(result?.error).toBeUndefined()
1787 })
1788 test('can be converted to a standard error object at global level', async () => {
1789 const api = makeApi({ globalCatch: true })
1790 const storeRef = setupApiStore(api, undefined, {
1791 withoutTestLifecycles: true,
1792 })
1793 const result = await storeRef.store.dispatch(
1794 api.endpoints.query.initiate(),
1795 )
1796 expect(result?.error).toEqual({
1797 status: 'CUSTOM_ERROR',
1798 error: 'metaSchema failed validation',
1799 data: expect.any(Array),
1800 })
1801 expectFailureHandlersToHaveBeenCalled({
1802 schemaName: 'metaSchema',
1803 value: {
1804 request: expect.any(Request),
1805 response: expect.any(Response),
1806 },
1807 arg: undefined,
1808 })
1809 })
1810 test('can be converted to a standard error object at endpoint level', async () => {
1811 const api = makeApi({ endpointCatch: true })
1812 const storeRef = setupApiStore(api, undefined, {
1813 withoutTestLifecycles: true,
1814 })
1815 const result = await storeRef.store.dispatch(
1816 api.endpoints.query.initiate(),
1817 )
1818 expect(result?.error).toEqual({
1819 status: 'CUSTOM_ERROR',
1820 error: 'metaSchema failed validation',
1821 data: expect.any(Array),
1822 })
1823 expectFailureHandlersToHaveBeenCalled({
1824 schemaName: 'metaSchema',
1825 value: {
1826 request: expect.any(Request),
1827 response: expect.any(Response),
1828 },
1829 arg: undefined,
1830 })
1831 })
1832 })
1833})
Note: See TracBrowser for help on using the repository browser.