source: node_modules/@reduxjs/toolkit/src/query/tests/queryFn.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: 14.0 KB
Line 
1import { noop } from '@internal/listenerMiddleware/utils'
2import type { QuerySubState } from '@internal/query/core/apiState'
3import type { Post } from '@internal/query/tests/mocks/handlers'
4import { posts } from '@internal/query/tests/mocks/handlers'
5import { actionsReducer, setupApiStore } from '@internal/tests/utils/helpers'
6import type { SerializedError } from '@reduxjs/toolkit'
7import { configureStore } from '@reduxjs/toolkit'
8import type { BaseQueryFn, FetchBaseQueryError } from '@reduxjs/toolkit/query'
9import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
10
11describe('queryFn base implementation tests', () => {
12 const baseQuery: BaseQueryFn<string, { wrappedByBaseQuery: string }, string> =
13 vi.fn((arg: string) =>
14 arg.includes('withErrorQuery')
15 ? { error: `cut${arg}` }
16 : { data: { wrappedByBaseQuery: arg } },
17 )
18
19 const api = createApi({
20 baseQuery,
21 endpoints: (build) => ({
22 withQuery: build.query<string, string>({
23 query(arg: string) {
24 return `resultFrom(${arg})`
25 },
26 transformResponse(response) {
27 return response.wrappedByBaseQuery
28 },
29 }),
30 withErrorQuery: build.query<string, string>({
31 query(arg: string) {
32 return `resultFrom(${arg})`
33 },
34 transformErrorResponse(response) {
35 return response.slice(3)
36 },
37 }),
38 withQueryFn: build.query<string, string>({
39 queryFn(arg: string) {
40 return { data: `resultFrom(${arg})` }
41 },
42 }),
43 withInvalidDataQueryFn: build.query<string, string>({
44 // @ts-expect-error
45 queryFn(arg: string) {
46 return { data: 5 }
47 },
48 }),
49 withErrorQueryFn: build.query<string, string>({
50 queryFn(arg: string) {
51 return { error: `resultFrom(${arg})` }
52 },
53 }),
54 withInvalidErrorQueryFn: build.query<string, string>({
55 // @ts-expect-error
56 queryFn(arg: string) {
57 return { error: 5 }
58 },
59 }),
60 withThrowingQueryFn: build.query<string, string>({
61 queryFn(arg: string) {
62 throw new Error(`resultFrom(${arg})`)
63 },
64 }),
65 withAsyncQueryFn: build.query<string, string>({
66 async queryFn(arg: string) {
67 return { data: `resultFrom(${arg})` }
68 },
69 }),
70 withInvalidDataAsyncQueryFn: build.query<string, string>({
71 // @ts-expect-error
72 async queryFn(arg: string) {
73 return { data: 5 }
74 },
75 }),
76 withAsyncErrorQueryFn: build.query<string, string>({
77 async queryFn(arg: string) {
78 return { error: `resultFrom(${arg})` }
79 },
80 }),
81 withInvalidAsyncErrorQueryFn: build.query<string, string>({
82 // @ts-expect-error
83 async queryFn(arg: string) {
84 return { error: 5 }
85 },
86 }),
87 withAsyncThrowingQueryFn: build.query<string, string>({
88 async queryFn(arg: string) {
89 throw new Error(`resultFrom(${arg})`)
90 },
91 }),
92 mutationWithQueryFn: build.mutation<string, string>({
93 queryFn(arg: string) {
94 return { data: `resultFrom(${arg})` }
95 },
96 }),
97 mutationWithInvalidDataQueryFn: build.mutation<string, string>({
98 // @ts-expect-error
99 queryFn(arg: string) {
100 return { data: 5 }
101 },
102 }),
103 mutationWithErrorQueryFn: build.mutation<string, string>({
104 queryFn(arg: string) {
105 return { error: `resultFrom(${arg})` }
106 },
107 }),
108 mutationWithInvalidErrorQueryFn: build.mutation<string, string>({
109 // @ts-expect-error
110 queryFn(arg: string) {
111 return { error: 5 }
112 },
113 }),
114 mutationWithThrowingQueryFn: build.mutation<string, string>({
115 queryFn(arg: string) {
116 throw new Error(`resultFrom(${arg})`)
117 },
118 }),
119 mutationWithAsyncQueryFn: build.mutation<string, string>({
120 async queryFn(arg: string) {
121 return { data: `resultFrom(${arg})` }
122 },
123 }),
124 mutationWithInvalidAsyncQueryFn: build.mutation<string, string>({
125 // @ts-expect-error
126 async queryFn(arg: string) {
127 return { data: 5 }
128 },
129 }),
130 mutationWithAsyncErrorQueryFn: build.mutation<string, string>({
131 async queryFn(arg: string) {
132 return { error: `resultFrom(${arg})` }
133 },
134 }),
135 mutationWithInvalidAsyncErrorQueryFn: build.mutation<string, string>({
136 // @ts-expect-error
137 async queryFn(arg: string) {
138 return { error: 5 }
139 },
140 }),
141 mutationWithAsyncThrowingQueryFn: build.mutation<string, string>({
142 async queryFn(arg: string) {
143 throw new Error(`resultFrom(${arg})`)
144 },
145 }),
146 // @ts-expect-error
147 withNeither: build.query<string, string>({}),
148 // @ts-expect-error
149 mutationWithNeither: build.mutation<string, string>({}),
150 }),
151 })
152
153 const {
154 withQuery,
155 withErrorQuery,
156 withQueryFn,
157 withErrorQueryFn,
158 withThrowingQueryFn,
159 withAsyncQueryFn,
160 withAsyncErrorQueryFn,
161 withAsyncThrowingQueryFn,
162 mutationWithQueryFn,
163 mutationWithErrorQueryFn,
164 mutationWithThrowingQueryFn,
165 mutationWithAsyncQueryFn,
166 mutationWithAsyncErrorQueryFn,
167 mutationWithAsyncThrowingQueryFn,
168 withNeither,
169 mutationWithNeither,
170 } = api.endpoints
171
172 const store = configureStore({
173 reducer: {
174 [api.reducerPath]: api.reducer,
175 },
176 middleware: (gDM) => gDM({}).concat(api.middleware),
177 })
178
179 test.each([
180 ['withQuery', withQuery, 'data'],
181 ['withErrorQuery', withErrorQuery, 'error'],
182 ['withQueryFn', withQueryFn, 'data'],
183 ['withErrorQueryFn', withErrorQueryFn, 'error'],
184 ['withThrowingQueryFn', withThrowingQueryFn, 'throw'],
185 ['withAsyncQueryFn', withAsyncQueryFn, 'data'],
186 ['withAsyncErrorQueryFn', withAsyncErrorQueryFn, 'error'],
187 ['withAsyncThrowingQueryFn', withAsyncThrowingQueryFn, 'throw'],
188 ])('%s', async (endpointName, endpoint, expectedResult) => {
189 const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(noop)
190
191 const thunk = endpoint.initiate(endpointName)
192
193 const result: undefined | QuerySubState<any> = await store.dispatch(thunk)
194
195 if (endpointName.includes('Throw')) {
196 expect(consoleErrorSpy).toHaveBeenCalledOnce()
197
198 expect(consoleErrorSpy).toHaveBeenLastCalledWith(
199 `An unhandled error occurred processing a request for the endpoint "${endpointName}".\nIn the case of an unhandled error, no tags will be "provided" or "invalidated".`,
200 Error(`resultFrom(${endpointName})`),
201 )
202 } else {
203 expect(consoleErrorSpy).not.toHaveBeenCalled()
204 }
205
206 if (expectedResult === 'data') {
207 expect(result).toEqual(
208 expect.objectContaining({
209 data: `resultFrom(${endpointName})`,
210 }),
211 )
212 } else if (expectedResult === 'error') {
213 expect(result).toEqual(
214 expect.objectContaining({
215 error: `resultFrom(${endpointName})`,
216 }),
217 )
218 } else {
219 expect(result).toEqual(
220 expect.objectContaining({
221 error: expect.objectContaining({
222 message: `resultFrom(${endpointName})`,
223 }),
224 }),
225 )
226 }
227
228 consoleErrorSpy.mockRestore()
229 })
230
231 test.each([
232 ['mutationWithQueryFn', mutationWithQueryFn, 'data'],
233 ['mutationWithErrorQueryFn', mutationWithErrorQueryFn, 'error'],
234 ['mutationWithThrowingQueryFn', mutationWithThrowingQueryFn, 'throw'],
235 ['mutationWithAsyncQueryFn', mutationWithAsyncQueryFn, 'data'],
236 ['mutationWithAsyncErrorQueryFn', mutationWithAsyncErrorQueryFn, 'error'],
237 [
238 'mutationWithAsyncThrowingQueryFn',
239 mutationWithAsyncThrowingQueryFn,
240 'throw',
241 ],
242 ])('%s', async (endpointName, endpoint, expectedResult) => {
243 const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(noop)
244
245 const thunk = endpoint.initiate(endpointName)
246
247 const result:
248 | undefined
249 | { data: string }
250 | { error: string | SerializedError } = await store.dispatch(thunk)
251
252 if (endpointName.includes('Throw')) {
253 expect(consoleErrorSpy).toHaveBeenCalledOnce()
254
255 expect(consoleErrorSpy).toHaveBeenLastCalledWith(
256 `An unhandled error occurred processing a request for the endpoint "${endpointName}".\nIn the case of an unhandled error, no tags will be "provided" or "invalidated".`,
257 Error(`resultFrom(${endpointName})`),
258 )
259 } else {
260 expect(consoleErrorSpy).not.toHaveBeenCalled()
261 }
262
263 if (expectedResult === 'data') {
264 expect(result).toEqual(
265 expect.objectContaining({
266 data: `resultFrom(${endpointName})`,
267 }),
268 )
269 } else if (expectedResult === 'error') {
270 expect(result).toEqual(
271 expect.objectContaining({
272 error: `resultFrom(${endpointName})`,
273 }),
274 )
275 } else {
276 expect(result).toEqual(
277 expect.objectContaining({
278 error: expect.objectContaining({
279 message: `resultFrom(${endpointName})`,
280 }),
281 }),
282 )
283 }
284
285 consoleErrorSpy.mockRestore()
286 })
287
288 test('neither provided', async () => {
289 const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(noop)
290
291 {
292 const thunk = withNeither.initiate('withNeither')
293
294 const result: QuerySubState<any> = await store.dispatch(thunk)
295
296 expect(consoleErrorSpy).toHaveBeenCalledOnce()
297
298 expect(consoleErrorSpy).toHaveBeenLastCalledWith(
299 `An unhandled error occurred processing a request for the endpoint "withNeither".\nIn the case of an unhandled error, no tags will be "provided" or "invalidated".`,
300 TypeError('endpointDefinition.queryFn is not a function'),
301 )
302
303 expect(result.error).toEqual(
304 expect.objectContaining({
305 message: 'endpointDefinition.queryFn is not a function',
306 }),
307 )
308
309 consoleErrorSpy.mockClear()
310 }
311 {
312 const thunk = mutationWithNeither.initiate('mutationWithNeither')
313
314 const result:
315 | undefined
316 | { data: string }
317 | { error: string | SerializedError } = await store.dispatch(thunk)
318
319 expect(consoleErrorSpy).toHaveBeenCalledOnce()
320
321 expect(consoleErrorSpy).toHaveBeenLastCalledWith(
322 `An unhandled error occurred processing a request for the endpoint "mutationWithNeither".\nIn the case of an unhandled error, no tags will be "provided" or "invalidated".`,
323 TypeError('endpointDefinition.queryFn is not a function'),
324 )
325
326 if (!('error' in result)) {
327 expect.fail()
328 }
329
330 expect(result.error).toEqual(
331 expect.objectContaining({
332 message: 'endpointDefinition.queryFn is not a function',
333 }),
334 )
335 }
336
337 consoleErrorSpy.mockRestore()
338 })
339})
340
341describe('usage scenario tests', () => {
342 const mockData = { id: 1, name: 'Banana' }
343 const mockDocResult = {
344 exists: () => true,
345 data: () => mockData,
346 }
347 const get = vi.fn(() => Promise.resolve(mockDocResult))
348 const doc = vi.fn((name) => ({
349 get,
350 }))
351 const collection = vi.fn((name) => ({ get, doc }))
352 const firestore = () => {
353 return { collection, doc }
354 }
355
356 const baseQuery = fetchBaseQuery({ baseUrl: 'https://example.com/' })
357 const api = createApi({
358 baseQuery,
359 endpoints: (build) => ({
360 getRandomUser: build.query<Post, void>({
361 async queryFn(_arg: void, _queryApi, _extraOptions, fetchWithBQ) {
362 // get a random post
363 const randomResult = await fetchWithBQ('posts/random')
364 if (randomResult.error) {
365 throw randomResult.error
366 }
367 const post = randomResult.data as Post
368 const result = await fetchWithBQ(`/post/${post.id}`)
369 return result.data
370 ? { data: result.data as Post }
371 : { error: result.error as FetchBaseQueryError }
372 },
373 }),
374 getFirebaseUser: build.query<typeof mockData, number>({
375 async queryFn(arg: number) {
376 const getResult = await firestore().collection('users').doc(arg).get()
377 if (!getResult.exists()) {
378 throw new Error('Missing user')
379 }
380 return { data: getResult.data() }
381 },
382 }),
383 getMissingFirebaseUser: build.query<typeof mockData, number>({
384 async queryFn(arg: number) {
385 const getResult = await firestore().collection('users').doc(arg).get()
386 // intentionally throw if it exists to keep the mocking overhead low
387 if (getResult.exists()) {
388 throw new Error('Missing user')
389 }
390 return { data: getResult.data() }
391 },
392 }),
393 }),
394 })
395
396 const storeRef = setupApiStore(api, {
397 ...actionsReducer,
398 })
399
400 /**
401 * Allow for a scenario where you can chain X requests
402 * https://discord.com/channels/102860784329052160/103538784460615680/825430959247720449
403 * const resp1 = await api.get(url);
404 * const resp2 = await api.get(`${url2}/id=${resp1.data.id}`);
405 */
406
407 it('can chain multiple queries together', async () => {
408 const result = await storeRef.store.dispatch(
409 api.endpoints.getRandomUser.initiate(),
410 )
411 expect(result.data).toEqual(posts[1])
412 })
413
414 it('can wrap a service like Firebase', async () => {
415 const result = await storeRef.store.dispatch(
416 api.endpoints.getFirebaseUser.initiate(1),
417 )
418 expect(result.data).toEqual(mockData)
419 })
420
421 it('can wrap a service like Firebase and handle errors', async () => {
422 const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(noop)
423
424 const result: QuerySubState<any> = await storeRef.store.dispatch(
425 api.endpoints.getMissingFirebaseUser.initiate(1),
426 )
427
428 expect(consoleErrorSpy).toHaveBeenCalledOnce()
429
430 expect(consoleErrorSpy).toHaveBeenLastCalledWith(
431 `An unhandled error occurred processing a request for the endpoint "getMissingFirebaseUser".\nIn the case of an unhandled error, no tags will be "provided" or "invalidated".`,
432 Error('Missing user'),
433 )
434
435 expect(result.data).toBeUndefined()
436 expect(result.error).toEqual(
437 expect.objectContaining({
438 message: 'Missing user',
439 name: 'Error',
440 }),
441 )
442
443 consoleErrorSpy.mockRestore()
444 })
445})
Note: See TracBrowser for help on using the repository browser.