Spaces:
Sleeping
Sleeping
| import { noop } from '@internal/listenerMiddleware/utils' | |
| import type { QuerySubState } from '@internal/query/core/apiState' | |
| import type { Post } from '@internal/query/tests/mocks/handlers' | |
| import { posts } from '@internal/query/tests/mocks/handlers' | |
| import { actionsReducer, setupApiStore } from '@internal/tests/utils/helpers' | |
| import type { SerializedError } from '@reduxjs/toolkit' | |
| import { configureStore } from '@reduxjs/toolkit' | |
| import type { BaseQueryFn, FetchBaseQueryError } from '@reduxjs/toolkit/query' | |
| import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query' | |
| describe('queryFn base implementation tests', () => { | |
| const baseQuery: BaseQueryFn<string, { wrappedByBaseQuery: string }, string> = | |
| vi.fn((arg: string) => | |
| arg.includes('withErrorQuery') | |
| ? { error: `cut${arg}` } | |
| : { data: { wrappedByBaseQuery: arg } }, | |
| ) | |
| const api = createApi({ | |
| baseQuery, | |
| endpoints: (build) => ({ | |
| withQuery: build.query<string, string>({ | |
| query(arg: string) { | |
| return `resultFrom(${arg})` | |
| }, | |
| transformResponse(response) { | |
| return response.wrappedByBaseQuery | |
| }, | |
| }), | |
| withErrorQuery: build.query<string, string>({ | |
| query(arg: string) { | |
| return `resultFrom(${arg})` | |
| }, | |
| transformErrorResponse(response) { | |
| return response.slice(3) | |
| }, | |
| }), | |
| withQueryFn: build.query<string, string>({ | |
| queryFn(arg: string) { | |
| return { data: `resultFrom(${arg})` } | |
| }, | |
| }), | |
| withInvalidDataQueryFn: build.query<string, string>({ | |
| // @ts-expect-error | |
| queryFn(arg: string) { | |
| return { data: 5 } | |
| }, | |
| }), | |
| withErrorQueryFn: build.query<string, string>({ | |
| queryFn(arg: string) { | |
| return { error: `resultFrom(${arg})` } | |
| }, | |
| }), | |
| withInvalidErrorQueryFn: build.query<string, string>({ | |
| // @ts-expect-error | |
| queryFn(arg: string) { | |
| return { error: 5 } | |
| }, | |
| }), | |
| withThrowingQueryFn: build.query<string, string>({ | |
| queryFn(arg: string) { | |
| throw new Error(`resultFrom(${arg})`) | |
| }, | |
| }), | |
| withAsyncQueryFn: build.query<string, string>({ | |
| async queryFn(arg: string) { | |
| return { data: `resultFrom(${arg})` } | |
| }, | |
| }), | |
| withInvalidDataAsyncQueryFn: build.query<string, string>({ | |
| // @ts-expect-error | |
| async queryFn(arg: string) { | |
| return { data: 5 } | |
| }, | |
| }), | |
| withAsyncErrorQueryFn: build.query<string, string>({ | |
| async queryFn(arg: string) { | |
| return { error: `resultFrom(${arg})` } | |
| }, | |
| }), | |
| withInvalidAsyncErrorQueryFn: build.query<string, string>({ | |
| // @ts-expect-error | |
| async queryFn(arg: string) { | |
| return { error: 5 } | |
| }, | |
| }), | |
| withAsyncThrowingQueryFn: build.query<string, string>({ | |
| async queryFn(arg: string) { | |
| throw new Error(`resultFrom(${arg})`) | |
| }, | |
| }), | |
| mutationWithQueryFn: build.mutation<string, string>({ | |
| queryFn(arg: string) { | |
| return { data: `resultFrom(${arg})` } | |
| }, | |
| }), | |
| mutationWithInvalidDataQueryFn: build.mutation<string, string>({ | |
| // @ts-expect-error | |
| queryFn(arg: string) { | |
| return { data: 5 } | |
| }, | |
| }), | |
| mutationWithErrorQueryFn: build.mutation<string, string>({ | |
| queryFn(arg: string) { | |
| return { error: `resultFrom(${arg})` } | |
| }, | |
| }), | |
| mutationWithInvalidErrorQueryFn: build.mutation<string, string>({ | |
| // @ts-expect-error | |
| queryFn(arg: string) { | |
| return { error: 5 } | |
| }, | |
| }), | |
| mutationWithThrowingQueryFn: build.mutation<string, string>({ | |
| queryFn(arg: string) { | |
| throw new Error(`resultFrom(${arg})`) | |
| }, | |
| }), | |
| mutationWithAsyncQueryFn: build.mutation<string, string>({ | |
| async queryFn(arg: string) { | |
| return { data: `resultFrom(${arg})` } | |
| }, | |
| }), | |
| mutationWithInvalidAsyncQueryFn: build.mutation<string, string>({ | |
| // @ts-expect-error | |
| async queryFn(arg: string) { | |
| return { data: 5 } | |
| }, | |
| }), | |
| mutationWithAsyncErrorQueryFn: build.mutation<string, string>({ | |
| async queryFn(arg: string) { | |
| return { error: `resultFrom(${arg})` } | |
| }, | |
| }), | |
| mutationWithInvalidAsyncErrorQueryFn: build.mutation<string, string>({ | |
| // @ts-expect-error | |
| async queryFn(arg: string) { | |
| return { error: 5 } | |
| }, | |
| }), | |
| mutationWithAsyncThrowingQueryFn: build.mutation<string, string>({ | |
| async queryFn(arg: string) { | |
| throw new Error(`resultFrom(${arg})`) | |
| }, | |
| }), | |
| // @ts-expect-error | |
| withNeither: build.query<string, string>({}), | |
| // @ts-expect-error | |
| mutationWithNeither: build.mutation<string, string>({}), | |
| }), | |
| }) | |
| const { | |
| withQuery, | |
| withErrorQuery, | |
| withQueryFn, | |
| withErrorQueryFn, | |
| withThrowingQueryFn, | |
| withAsyncQueryFn, | |
| withAsyncErrorQueryFn, | |
| withAsyncThrowingQueryFn, | |
| mutationWithQueryFn, | |
| mutationWithErrorQueryFn, | |
| mutationWithThrowingQueryFn, | |
| mutationWithAsyncQueryFn, | |
| mutationWithAsyncErrorQueryFn, | |
| mutationWithAsyncThrowingQueryFn, | |
| withNeither, | |
| mutationWithNeither, | |
| } = api.endpoints | |
| const store = configureStore({ | |
| reducer: { | |
| [api.reducerPath]: api.reducer, | |
| }, | |
| middleware: (gDM) => gDM({}).concat(api.middleware), | |
| }) | |
| test.each([ | |
| ['withQuery', withQuery, 'data'], | |
| ['withErrorQuery', withErrorQuery, 'error'], | |
| ['withQueryFn', withQueryFn, 'data'], | |
| ['withErrorQueryFn', withErrorQueryFn, 'error'], | |
| ['withThrowingQueryFn', withThrowingQueryFn, 'throw'], | |
| ['withAsyncQueryFn', withAsyncQueryFn, 'data'], | |
| ['withAsyncErrorQueryFn', withAsyncErrorQueryFn, 'error'], | |
| ['withAsyncThrowingQueryFn', withAsyncThrowingQueryFn, 'throw'], | |
| ])('%s', async (endpointName, endpoint, expectedResult) => { | |
| const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(noop) | |
| const thunk = endpoint.initiate(endpointName) | |
| const result: undefined | QuerySubState<any> = await store.dispatch(thunk) | |
| if (endpointName.includes('Throw')) { | |
| expect(consoleErrorSpy).toHaveBeenCalledOnce() | |
| expect(consoleErrorSpy).toHaveBeenLastCalledWith( | |
| `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".`, | |
| Error(`resultFrom(${endpointName})`), | |
| ) | |
| } else { | |
| expect(consoleErrorSpy).not.toHaveBeenCalled() | |
| } | |
| if (expectedResult === 'data') { | |
| expect(result).toEqual( | |
| expect.objectContaining({ | |
| data: `resultFrom(${endpointName})`, | |
| }), | |
| ) | |
| } else if (expectedResult === 'error') { | |
| expect(result).toEqual( | |
| expect.objectContaining({ | |
| error: `resultFrom(${endpointName})`, | |
| }), | |
| ) | |
| } else { | |
| expect(result).toEqual( | |
| expect.objectContaining({ | |
| error: expect.objectContaining({ | |
| message: `resultFrom(${endpointName})`, | |
| }), | |
| }), | |
| ) | |
| } | |
| consoleErrorSpy.mockRestore() | |
| }) | |
| test.each([ | |
| ['mutationWithQueryFn', mutationWithQueryFn, 'data'], | |
| ['mutationWithErrorQueryFn', mutationWithErrorQueryFn, 'error'], | |
| ['mutationWithThrowingQueryFn', mutationWithThrowingQueryFn, 'throw'], | |
| ['mutationWithAsyncQueryFn', mutationWithAsyncQueryFn, 'data'], | |
| ['mutationWithAsyncErrorQueryFn', mutationWithAsyncErrorQueryFn, 'error'], | |
| [ | |
| 'mutationWithAsyncThrowingQueryFn', | |
| mutationWithAsyncThrowingQueryFn, | |
| 'throw', | |
| ], | |
| ])('%s', async (endpointName, endpoint, expectedResult) => { | |
| const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(noop) | |
| const thunk = endpoint.initiate(endpointName) | |
| const result: | |
| | undefined | |
| | { data: string } | |
| | { error: string | SerializedError } = await store.dispatch(thunk) | |
| if (endpointName.includes('Throw')) { | |
| expect(consoleErrorSpy).toHaveBeenCalledOnce() | |
| expect(consoleErrorSpy).toHaveBeenLastCalledWith( | |
| `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".`, | |
| Error(`resultFrom(${endpointName})`), | |
| ) | |
| } else { | |
| expect(consoleErrorSpy).not.toHaveBeenCalled() | |
| } | |
| if (expectedResult === 'data') { | |
| expect(result).toEqual( | |
| expect.objectContaining({ | |
| data: `resultFrom(${endpointName})`, | |
| }), | |
| ) | |
| } else if (expectedResult === 'error') { | |
| expect(result).toEqual( | |
| expect.objectContaining({ | |
| error: `resultFrom(${endpointName})`, | |
| }), | |
| ) | |
| } else { | |
| expect(result).toEqual( | |
| expect.objectContaining({ | |
| error: expect.objectContaining({ | |
| message: `resultFrom(${endpointName})`, | |
| }), | |
| }), | |
| ) | |
| } | |
| consoleErrorSpy.mockRestore() | |
| }) | |
| test('neither provided', async () => { | |
| const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(noop) | |
| { | |
| const thunk = withNeither.initiate('withNeither') | |
| const result: QuerySubState<any> = await store.dispatch(thunk) | |
| expect(consoleErrorSpy).toHaveBeenCalledOnce() | |
| expect(consoleErrorSpy).toHaveBeenLastCalledWith( | |
| `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".`, | |
| TypeError('endpointDefinition.queryFn is not a function'), | |
| ) | |
| expect(result.error).toEqual( | |
| expect.objectContaining({ | |
| message: 'endpointDefinition.queryFn is not a function', | |
| }), | |
| ) | |
| consoleErrorSpy.mockClear() | |
| } | |
| { | |
| const thunk = mutationWithNeither.initiate('mutationWithNeither') | |
| const result: | |
| | undefined | |
| | { data: string } | |
| | { error: string | SerializedError } = await store.dispatch(thunk) | |
| expect(consoleErrorSpy).toHaveBeenCalledOnce() | |
| expect(consoleErrorSpy).toHaveBeenLastCalledWith( | |
| `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".`, | |
| TypeError('endpointDefinition.queryFn is not a function'), | |
| ) | |
| if (!('error' in result)) { | |
| expect.fail() | |
| } | |
| expect(result.error).toEqual( | |
| expect.objectContaining({ | |
| message: 'endpointDefinition.queryFn is not a function', | |
| }), | |
| ) | |
| } | |
| consoleErrorSpy.mockRestore() | |
| }) | |
| }) | |
| describe('usage scenario tests', () => { | |
| const mockData = { id: 1, name: 'Banana' } | |
| const mockDocResult = { | |
| exists: () => true, | |
| data: () => mockData, | |
| } | |
| const get = vi.fn(() => Promise.resolve(mockDocResult)) | |
| const doc = vi.fn((name) => ({ | |
| get, | |
| })) | |
| const collection = vi.fn((name) => ({ get, doc })) | |
| const firestore = () => { | |
| return { collection, doc } | |
| } | |
| const baseQuery = fetchBaseQuery({ baseUrl: 'https://example.com/' }) | |
| const api = createApi({ | |
| baseQuery, | |
| endpoints: (build) => ({ | |
| getRandomUser: build.query<Post, void>({ | |
| async queryFn(_arg: void, _queryApi, _extraOptions, fetchWithBQ) { | |
| // get a random post | |
| const randomResult = await fetchWithBQ('posts/random') | |
| if (randomResult.error) { | |
| throw randomResult.error | |
| } | |
| const post = randomResult.data as Post | |
| const result = await fetchWithBQ(`/post/${post.id}`) | |
| return result.data | |
| ? { data: result.data as Post } | |
| : { error: result.error as FetchBaseQueryError } | |
| }, | |
| }), | |
| getFirebaseUser: build.query<typeof mockData, number>({ | |
| async queryFn(arg: number) { | |
| const getResult = await firestore().collection('users').doc(arg).get() | |
| if (!getResult.exists()) { | |
| throw new Error('Missing user') | |
| } | |
| return { data: getResult.data() } | |
| }, | |
| }), | |
| getMissingFirebaseUser: build.query<typeof mockData, number>({ | |
| async queryFn(arg: number) { | |
| const getResult = await firestore().collection('users').doc(arg).get() | |
| // intentionally throw if it exists to keep the mocking overhead low | |
| if (getResult.exists()) { | |
| throw new Error('Missing user') | |
| } | |
| return { data: getResult.data() } | |
| }, | |
| }), | |
| }), | |
| }) | |
| const storeRef = setupApiStore(api, { | |
| ...actionsReducer, | |
| }) | |
| /** | |
| * Allow for a scenario where you can chain X requests | |
| * https://discord.com/channels/102860784329052160/103538784460615680/825430959247720449 | |
| * const resp1 = await api.get(url); | |
| * const resp2 = await api.get(`${url2}/id=${resp1.data.id}`); | |
| */ | |
| it('can chain multiple queries together', async () => { | |
| const result = await storeRef.store.dispatch( | |
| api.endpoints.getRandomUser.initiate(), | |
| ) | |
| expect(result.data).toEqual(posts[1]) | |
| }) | |
| it('can wrap a service like Firebase', async () => { | |
| const result = await storeRef.store.dispatch( | |
| api.endpoints.getFirebaseUser.initiate(1), | |
| ) | |
| expect(result.data).toEqual(mockData) | |
| }) | |
| it('can wrap a service like Firebase and handle errors', async () => { | |
| const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(noop) | |
| const result: QuerySubState<any> = await storeRef.store.dispatch( | |
| api.endpoints.getMissingFirebaseUser.initiate(1), | |
| ) | |
| expect(consoleErrorSpy).toHaveBeenCalledOnce() | |
| expect(consoleErrorSpy).toHaveBeenLastCalledWith( | |
| `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".`, | |
| Error('Missing user'), | |
| ) | |
| expect(result.data).toBeUndefined() | |
| expect(result.error).toEqual( | |
| expect.objectContaining({ | |
| message: 'Missing user', | |
| name: 'Error', | |
| }), | |
| ) | |
| consoleErrorSpy.mockRestore() | |
| }) | |
| }) | |