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 = vi.fn((arg: string) => arg.includes('withErrorQuery') ? { error: `cut${arg}` } : { data: { wrappedByBaseQuery: arg } }, ) const api = createApi({ baseQuery, endpoints: (build) => ({ withQuery: build.query({ query(arg: string) { return `resultFrom(${arg})` }, transformResponse(response) { return response.wrappedByBaseQuery }, }), withErrorQuery: build.query({ query(arg: string) { return `resultFrom(${arg})` }, transformErrorResponse(response) { return response.slice(3) }, }), withQueryFn: build.query({ queryFn(arg: string) { return { data: `resultFrom(${arg})` } }, }), withInvalidDataQueryFn: build.query({ // @ts-expect-error queryFn(arg: string) { return { data: 5 } }, }), withErrorQueryFn: build.query({ queryFn(arg: string) { return { error: `resultFrom(${arg})` } }, }), withInvalidErrorQueryFn: build.query({ // @ts-expect-error queryFn(arg: string) { return { error: 5 } }, }), withThrowingQueryFn: build.query({ queryFn(arg: string) { throw new Error(`resultFrom(${arg})`) }, }), withAsyncQueryFn: build.query({ async queryFn(arg: string) { return { data: `resultFrom(${arg})` } }, }), withInvalidDataAsyncQueryFn: build.query({ // @ts-expect-error async queryFn(arg: string) { return { data: 5 } }, }), withAsyncErrorQueryFn: build.query({ async queryFn(arg: string) { return { error: `resultFrom(${arg})` } }, }), withInvalidAsyncErrorQueryFn: build.query({ // @ts-expect-error async queryFn(arg: string) { return { error: 5 } }, }), withAsyncThrowingQueryFn: build.query({ async queryFn(arg: string) { throw new Error(`resultFrom(${arg})`) }, }), mutationWithQueryFn: build.mutation({ queryFn(arg: string) { return { data: `resultFrom(${arg})` } }, }), mutationWithInvalidDataQueryFn: build.mutation({ // @ts-expect-error queryFn(arg: string) { return { data: 5 } }, }), mutationWithErrorQueryFn: build.mutation({ queryFn(arg: string) { return { error: `resultFrom(${arg})` } }, }), mutationWithInvalidErrorQueryFn: build.mutation({ // @ts-expect-error queryFn(arg: string) { return { error: 5 } }, }), mutationWithThrowingQueryFn: build.mutation({ queryFn(arg: string) { throw new Error(`resultFrom(${arg})`) }, }), mutationWithAsyncQueryFn: build.mutation({ async queryFn(arg: string) { return { data: `resultFrom(${arg})` } }, }), mutationWithInvalidAsyncQueryFn: build.mutation({ // @ts-expect-error async queryFn(arg: string) { return { data: 5 } }, }), mutationWithAsyncErrorQueryFn: build.mutation({ async queryFn(arg: string) { return { error: `resultFrom(${arg})` } }, }), mutationWithInvalidAsyncErrorQueryFn: build.mutation({ // @ts-expect-error async queryFn(arg: string) { return { error: 5 } }, }), mutationWithAsyncThrowingQueryFn: build.mutation({ async queryFn(arg: string) { throw new Error(`resultFrom(${arg})`) }, }), // @ts-expect-error withNeither: build.query({}), // @ts-expect-error mutationWithNeither: build.mutation({}), }), }) 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 = 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 = 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({ 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({ 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({ 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 = 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() }) })