1834 lines
56 KiB
Plaintext
1834 lines
56 KiB
Plaintext
import { noop } from '@internal/listenerMiddleware/utils'
|
|
import { server } from '@internal/query/tests/mocks/server'
|
|
import {
|
|
getSerializedHeaders,
|
|
setupApiStore,
|
|
} from '@internal/tests/utils/helpers'
|
|
import type { SerializedError } from '@reduxjs/toolkit'
|
|
import { configureStore, createAction, createReducer } from '@reduxjs/toolkit'
|
|
import type {
|
|
DefinitionsFromApi,
|
|
FetchBaseQueryError,
|
|
FetchBaseQueryMeta,
|
|
OverrideResultType,
|
|
SchemaFailureConverter,
|
|
SchemaType,
|
|
SerializeQueryArgs,
|
|
TagTypesFromApi,
|
|
} from '@reduxjs/toolkit/query'
|
|
import {
|
|
createApi,
|
|
fetchBaseQuery,
|
|
NamedSchemaError,
|
|
} from '@reduxjs/toolkit/query'
|
|
import { HttpResponse, delay, http } from 'msw'
|
|
import nodeFetch from 'node-fetch'
|
|
import * as v from 'valibot'
|
|
import type { SchemaFailureHandler } from '../endpointDefinitions'
|
|
|
|
beforeAll(() => {
|
|
vi.stubEnv('NODE_ENV', 'development')
|
|
|
|
return vi.unstubAllEnvs
|
|
})
|
|
|
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(noop)
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
server.resetHandlers()
|
|
})
|
|
|
|
afterAll(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
function paginate<T>(array: T[], page_size: number, page_number: number) {
|
|
// human-readable page numbers usually start with 1, so we reduce 1 in the first argument
|
|
return array.slice((page_number - 1) * page_size, page_number * page_size)
|
|
}
|
|
|
|
test('sensible defaults', () => {
|
|
const api = createApi({
|
|
baseQuery: fetchBaseQuery(),
|
|
endpoints: (build) => ({
|
|
getUser: build.query<unknown, void>({
|
|
query(id) {
|
|
return { url: `user/${id}` }
|
|
},
|
|
}),
|
|
updateUser: build.mutation<unknown, void>({
|
|
query: () => '',
|
|
}),
|
|
}),
|
|
})
|
|
configureStore({
|
|
reducer: {
|
|
[api.reducerPath]: api.reducer,
|
|
},
|
|
middleware: (gDM) => gDM().concat(api.middleware),
|
|
})
|
|
expect(api.reducerPath).toBe('api')
|
|
|
|
expect(api.endpoints.getUser.name).toBe('getUser')
|
|
expect(api.endpoints.updateUser.name).toBe('updateUser')
|
|
})
|
|
|
|
describe('wrong tagTypes log errors', () => {
|
|
const baseQuery = vi.fn()
|
|
const api = createApi({
|
|
baseQuery,
|
|
tagTypes: ['User'],
|
|
endpoints: (build) => ({
|
|
provideNothing: build.query<unknown, void>({
|
|
query: () => '',
|
|
}),
|
|
provideTypeString: build.query<unknown, void>({
|
|
query: () => '',
|
|
providesTags: ['User'],
|
|
}),
|
|
provideTypeWithId: build.query<unknown, void>({
|
|
query: () => '',
|
|
providesTags: [{ type: 'User', id: 5 }],
|
|
}),
|
|
provideTypeWithIdAndCallback: build.query<unknown, void>({
|
|
query: () => '',
|
|
providesTags: () => [{ type: 'User', id: 5 }],
|
|
}),
|
|
provideWrongTypeString: build.query<unknown, void>({
|
|
query: () => '',
|
|
// @ts-expect-error
|
|
providesTags: ['Users'],
|
|
}),
|
|
provideWrongTypeWithId: build.query<unknown, void>({
|
|
query: () => '',
|
|
// @ts-expect-error
|
|
providesTags: [{ type: 'Users', id: 5 }],
|
|
}),
|
|
provideWrongTypeWithIdAndCallback: build.query<unknown, void>({
|
|
query: () => '',
|
|
// @ts-expect-error
|
|
providesTags: () => [{ type: 'Users', id: 5 }],
|
|
}),
|
|
invalidateNothing: build.query<unknown, void>({
|
|
query: () => '',
|
|
}),
|
|
invalidateTypeString: build.mutation<unknown, void>({
|
|
query: () => '',
|
|
invalidatesTags: ['User'],
|
|
}),
|
|
invalidateTypeWithId: build.mutation<unknown, void>({
|
|
query: () => '',
|
|
invalidatesTags: [{ type: 'User', id: 5 }],
|
|
}),
|
|
invalidateTypeWithIdAndCallback: build.mutation<unknown, void>({
|
|
query: () => '',
|
|
invalidatesTags: () => [{ type: 'User', id: 5 }],
|
|
}),
|
|
|
|
invalidateWrongTypeString: build.mutation<unknown, void>({
|
|
query: () => '',
|
|
// @ts-expect-error
|
|
invalidatesTags: ['Users'],
|
|
}),
|
|
invalidateWrongTypeWithId: build.mutation<unknown, void>({
|
|
query: () => '',
|
|
// @ts-expect-error
|
|
invalidatesTags: [{ type: 'Users', id: 5 }],
|
|
}),
|
|
invalidateWrongTypeWithIdAndCallback: build.mutation<unknown, void>({
|
|
query: () => '',
|
|
// @ts-expect-error
|
|
invalidatesTags: () => [{ type: 'Users', id: 5 }],
|
|
}),
|
|
}),
|
|
})
|
|
const store = configureStore({
|
|
reducer: {
|
|
[api.reducerPath]: api.reducer,
|
|
},
|
|
middleware: (gDM) => gDM().concat(api.middleware),
|
|
})
|
|
|
|
beforeEach(() => {
|
|
baseQuery.mockResolvedValue({ data: 'foo' })
|
|
})
|
|
|
|
test.each<[keyof typeof api.endpoints, boolean?]>([
|
|
['provideNothing', false],
|
|
['provideTypeString', false],
|
|
['provideTypeWithId', false],
|
|
['provideTypeWithIdAndCallback', false],
|
|
['provideWrongTypeString', true],
|
|
['provideWrongTypeWithId', true],
|
|
['provideWrongTypeWithIdAndCallback', true],
|
|
['invalidateNothing', false],
|
|
['invalidateTypeString', false],
|
|
['invalidateTypeWithId', false],
|
|
['invalidateTypeWithIdAndCallback', false],
|
|
['invalidateWrongTypeString', true],
|
|
['invalidateWrongTypeWithId', true],
|
|
['invalidateWrongTypeWithIdAndCallback', true],
|
|
])(`endpoint %s should log an error? %s`, async (endpoint, shouldError) => {
|
|
vi.stubEnv('NODE_ENV', 'development')
|
|
|
|
// @ts-ignore
|
|
store.dispatch(api.endpoints[endpoint].initiate())
|
|
let result: { status: string }
|
|
do {
|
|
await delay(5)
|
|
// @ts-ignore
|
|
result = api.endpoints[endpoint].select()(store.getState())
|
|
} while (result.status === 'pending')
|
|
|
|
if (shouldError) {
|
|
expect(consoleErrorSpy).toHaveBeenLastCalledWith(
|
|
"Tag type 'Users' was used, but not specified in `tagTypes`!",
|
|
)
|
|
} else {
|
|
expect(consoleErrorSpy).not.toHaveBeenCalled()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('endpoint definition typings', () => {
|
|
const api = createApi({
|
|
baseQuery: (from: 'From'): { data: 'To' } | Promise<{ data: 'To' }> => ({
|
|
data: 'To',
|
|
}),
|
|
endpoints: () => ({}),
|
|
tagTypes: ['typeA', 'typeB'],
|
|
})
|
|
test('query: query & transformResponse types', () => {
|
|
api.injectEndpoints({
|
|
endpoints: (build) => ({
|
|
query: build.query<'RetVal', 'Arg'>({
|
|
query: (x: 'Arg') => 'From' as const,
|
|
transformResponse(r: 'To') {
|
|
return 'RetVal' as const
|
|
},
|
|
}),
|
|
query1: build.query<'RetVal', 'Arg'>({
|
|
// @ts-expect-error
|
|
query: (x: 'Error') => 'From' as const,
|
|
transformResponse(r: 'To') {
|
|
return 'RetVal' as const
|
|
},
|
|
}),
|
|
query2: build.query<'RetVal', 'Arg'>({
|
|
// @ts-expect-error
|
|
query: (x: 'Arg') => 'Error' as const,
|
|
transformResponse(r: 'To') {
|
|
return 'RetVal' as const
|
|
},
|
|
}),
|
|
query3: build.query<'RetVal', 'Arg'>({
|
|
query: (x: 'Arg') => 'From' as const,
|
|
// @ts-expect-error
|
|
transformResponse(r: 'Error') {
|
|
return 'RetVal' as const
|
|
},
|
|
}),
|
|
query4: build.query<'RetVal', 'Arg'>({
|
|
query: (x: 'Arg') => 'From' as const,
|
|
// @ts-expect-error
|
|
transformResponse(r: 'To') {
|
|
return 'Error' as const
|
|
},
|
|
}),
|
|
queryInference1: build.query<'RetVal', 'Arg'>({
|
|
query: (x) => {
|
|
return 'From'
|
|
},
|
|
transformResponse(r) {
|
|
return 'RetVal'
|
|
},
|
|
}),
|
|
queryInference2: (() => {
|
|
const query = build.query({
|
|
query: (x: 'Arg') => 'From' as const,
|
|
transformResponse(r: 'To') {
|
|
return 'RetVal' as const
|
|
},
|
|
})
|
|
return query
|
|
})(),
|
|
}),
|
|
})
|
|
})
|
|
test('mutation: query & transformResponse types', () => {
|
|
api.injectEndpoints({
|
|
endpoints: (build) => ({
|
|
query: build.mutation<'RetVal', 'Arg'>({
|
|
query: (x: 'Arg') => 'From' as const,
|
|
transformResponse(r: 'To') {
|
|
return 'RetVal' as const
|
|
},
|
|
}),
|
|
query1: build.mutation<'RetVal', 'Arg'>({
|
|
// @ts-expect-error
|
|
query: (x: 'Error') => 'From' as const,
|
|
transformResponse(r: 'To') {
|
|
return 'RetVal' as const
|
|
},
|
|
}),
|
|
query2: build.mutation<'RetVal', 'Arg'>({
|
|
// @ts-expect-error
|
|
query: (x: 'Arg') => 'Error' as const,
|
|
transformResponse(r: 'To') {
|
|
return 'RetVal' as const
|
|
},
|
|
}),
|
|
query3: build.mutation<'RetVal', 'Arg'>({
|
|
query: (x: 'Arg') => 'From' as const,
|
|
// @ts-expect-error
|
|
transformResponse(r: 'Error') {
|
|
return 'RetVal' as const
|
|
},
|
|
}),
|
|
query4: build.mutation<'RetVal', 'Arg'>({
|
|
query: (x: 'Arg') => 'From' as const,
|
|
// @ts-expect-error
|
|
transformResponse(r: 'To') {
|
|
return 'Error' as const
|
|
},
|
|
}),
|
|
mutationInference1: build.mutation<'RetVal', 'Arg'>({
|
|
query: (x) => {
|
|
return 'From'
|
|
},
|
|
transformResponse(r) {
|
|
return 'RetVal'
|
|
},
|
|
}),
|
|
mutationInference2: (() => {
|
|
const query = build.mutation({
|
|
query: (x: 'Arg') => 'From' as const,
|
|
transformResponse(r: 'To') {
|
|
return 'RetVal' as const
|
|
},
|
|
})
|
|
return query
|
|
})(),
|
|
}),
|
|
})
|
|
})
|
|
|
|
describe('enhancing endpoint definitions', () => {
|
|
const baseQuery = vi.fn((x: string) => ({ data: 'success' }))
|
|
const commonBaseQueryApi = {
|
|
dispatch: expect.any(Function),
|
|
endpoint: expect.any(String),
|
|
abort: expect.any(Function),
|
|
extra: undefined,
|
|
forced: expect.any(Boolean),
|
|
getState: expect.any(Function),
|
|
signal: expect.any(Object),
|
|
type: expect.any(String),
|
|
queryCacheKey: expect.any(String),
|
|
}
|
|
beforeEach(() => {
|
|
baseQuery.mockClear()
|
|
})
|
|
function getNewApi() {
|
|
return createApi({
|
|
baseQuery,
|
|
tagTypes: ['old'],
|
|
endpoints: (build) => ({
|
|
query1: build.query<'out1', 'in1'>({ query: (id) => `${id}` }),
|
|
query2: build.query<'out2', 'in2'>({ query: (id) => `${id}` }),
|
|
mutation1: build.mutation<'out1', 'in1'>({ query: (id) => `${id}` }),
|
|
mutation2: build.mutation<'out2', 'in2'>({ query: (id) => `${id}` }),
|
|
}),
|
|
})
|
|
}
|
|
let api = getNewApi()
|
|
beforeEach(() => {
|
|
api = getNewApi()
|
|
})
|
|
|
|
test('pre-modification behavior', async () => {
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
storeRef.store.dispatch(api.endpoints.query1.initiate('in1'))
|
|
storeRef.store.dispatch(api.endpoints.query2.initiate('in2'))
|
|
storeRef.store.dispatch(api.endpoints.mutation1.initiate('in1'))
|
|
storeRef.store.dispatch(api.endpoints.mutation2.initiate('in2'))
|
|
|
|
expect(baseQuery.mock.calls).toEqual([
|
|
[
|
|
'in1',
|
|
{
|
|
dispatch: expect.any(Function),
|
|
endpoint: expect.any(String),
|
|
getState: expect.any(Function),
|
|
signal: expect.any(Object),
|
|
abort: expect.any(Function),
|
|
forced: expect.any(Boolean),
|
|
type: expect.any(String),
|
|
queryCacheKey: expect.any(String),
|
|
},
|
|
undefined,
|
|
],
|
|
[
|
|
'in2',
|
|
{
|
|
dispatch: expect.any(Function),
|
|
endpoint: expect.any(String),
|
|
getState: expect.any(Function),
|
|
signal: expect.any(Object),
|
|
abort: expect.any(Function),
|
|
forced: expect.any(Boolean),
|
|
type: expect.any(String),
|
|
queryCacheKey: expect.any(String),
|
|
},
|
|
undefined,
|
|
],
|
|
[
|
|
'in1',
|
|
{
|
|
dispatch: expect.any(Function),
|
|
endpoint: expect.any(String),
|
|
getState: expect.any(Function),
|
|
signal: expect.any(Object),
|
|
abort: expect.any(Function),
|
|
// forced: undefined,
|
|
type: expect.any(String),
|
|
},
|
|
undefined,
|
|
],
|
|
[
|
|
'in2',
|
|
{
|
|
dispatch: expect.any(Function),
|
|
endpoint: expect.any(String),
|
|
getState: expect.any(Function),
|
|
signal: expect.any(Object),
|
|
abort: expect.any(Function),
|
|
// forced: undefined,
|
|
type: expect.any(String),
|
|
},
|
|
undefined,
|
|
],
|
|
])
|
|
})
|
|
|
|
test('warn on wrong tagType', async () => {
|
|
vi.stubEnv('NODE_ENV', 'development')
|
|
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
// only type-test this part
|
|
if (2 > 1) {
|
|
api.enhanceEndpoints({
|
|
endpoints: {
|
|
query1: {
|
|
// @ts-expect-error
|
|
providesTags: ['new'],
|
|
},
|
|
query2: {
|
|
// @ts-expect-error
|
|
providesTags: ['missing'],
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
const enhanced = api.enhanceEndpoints({
|
|
addTagTypes: ['new'],
|
|
endpoints: {
|
|
query1: {
|
|
providesTags: ['new'],
|
|
},
|
|
query2: {
|
|
// @ts-expect-error
|
|
providesTags: ['missing'],
|
|
},
|
|
},
|
|
})
|
|
|
|
storeRef.store.dispatch(api.endpoints.query1.initiate('in1'))
|
|
await delay(1)
|
|
expect(consoleErrorSpy).not.toHaveBeenCalled()
|
|
|
|
storeRef.store.dispatch(api.endpoints.query2.initiate('in2'))
|
|
await delay(1)
|
|
|
|
expect(consoleErrorSpy).toHaveBeenCalledOnce()
|
|
|
|
expect(consoleErrorSpy).toHaveBeenLastCalledWith(
|
|
"Tag type 'missing' was used, but not specified in `tagTypes`!",
|
|
)
|
|
|
|
// only type-test this part
|
|
if (2 > 1) {
|
|
enhanced.enhanceEndpoints({
|
|
endpoints: {
|
|
query1: {
|
|
// returned `enhanced` api contains "new" enitityType
|
|
providesTags: ['new'],
|
|
},
|
|
query2: {
|
|
// @ts-expect-error
|
|
providesTags: ['missing'],
|
|
},
|
|
},
|
|
})
|
|
}
|
|
})
|
|
|
|
test('modify', () => {
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
api.enhanceEndpoints({
|
|
endpoints: {
|
|
query1: {
|
|
query: (x) => {
|
|
return 'modified1'
|
|
},
|
|
},
|
|
query2(definition) {
|
|
definition.query = (x) => {
|
|
return 'modified2'
|
|
}
|
|
},
|
|
mutation1: {
|
|
query: (x) => {
|
|
return 'modified1'
|
|
},
|
|
},
|
|
mutation2(definition) {
|
|
definition.query = (x) => {
|
|
return 'modified2'
|
|
}
|
|
},
|
|
// @ts-expect-error
|
|
nonExisting: {},
|
|
},
|
|
})
|
|
|
|
storeRef.store.dispatch(api.endpoints.query1.initiate('in1'))
|
|
storeRef.store.dispatch(api.endpoints.query2.initiate('in2'))
|
|
storeRef.store.dispatch(api.endpoints.mutation1.initiate('in1'))
|
|
storeRef.store.dispatch(api.endpoints.mutation2.initiate('in2'))
|
|
|
|
expect(baseQuery.mock.calls).toEqual([
|
|
['modified1', commonBaseQueryApi, undefined],
|
|
['modified2', commonBaseQueryApi, undefined],
|
|
[
|
|
'modified1',
|
|
{
|
|
...commonBaseQueryApi,
|
|
forced: undefined,
|
|
queryCacheKey: undefined,
|
|
},
|
|
undefined,
|
|
],
|
|
[
|
|
'modified2',
|
|
{
|
|
...commonBaseQueryApi,
|
|
forced: undefined,
|
|
queryCacheKey: undefined,
|
|
},
|
|
undefined,
|
|
],
|
|
])
|
|
})
|
|
|
|
test('updated transform response types', async () => {
|
|
const baseApi = createApi({
|
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|
tagTypes: ['old'],
|
|
endpoints: (build) => ({
|
|
query1: build.query<'out1', void>({ query: () => 'success' }),
|
|
mutation1: build.mutation<'out1', void>({ query: () => 'success' }),
|
|
}),
|
|
})
|
|
|
|
type Transformed = { value: string }
|
|
|
|
type Definitions = DefinitionsFromApi<typeof api>
|
|
type TagTypes = TagTypesFromApi<typeof api>
|
|
|
|
type Q1Definition = OverrideResultType<Definitions['query1'], Transformed>
|
|
type M1Definition = OverrideResultType<
|
|
Definitions['mutation1'],
|
|
Transformed
|
|
>
|
|
|
|
type UpdatedDefitions = Omit<Definitions, 'query1' | 'mutation1'> & {
|
|
query1: Q1Definition
|
|
mutation1: M1Definition
|
|
}
|
|
|
|
const enhancedApi = baseApi.enhanceEndpoints<TagTypes, UpdatedDefitions>({
|
|
endpoints: {
|
|
query1: {
|
|
transformResponse: (a, b, c) => ({
|
|
value: 'transformed',
|
|
}),
|
|
},
|
|
mutation1: {
|
|
transformResponse: (a, b, c) => ({
|
|
value: 'transformed',
|
|
}),
|
|
},
|
|
},
|
|
})
|
|
|
|
const storeRef = setupApiStore(enhancedApi, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
|
|
const queryResponse = await storeRef.store.dispatch(
|
|
enhancedApi.endpoints.query1.initiate(),
|
|
)
|
|
expect(queryResponse.data).toEqual({ value: 'transformed' })
|
|
|
|
const mutationResponse = await storeRef.store.dispatch(
|
|
enhancedApi.endpoints.mutation1.initiate(),
|
|
)
|
|
expect('data' in mutationResponse && mutationResponse.data).toEqual({
|
|
value: 'transformed',
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('additional transformResponse behaviors', () => {
|
|
type SuccessResponse = { value: 'success' }
|
|
type EchoResponseData = { banana: 'bread' }
|
|
type ErrorResponse = { value: 'error' }
|
|
const api = createApi({
|
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|
endpoints: (build) => ({
|
|
echo: build.mutation({
|
|
query: () => ({ method: 'PUT', url: '/echo' }),
|
|
}),
|
|
mutation: build.mutation({
|
|
query: () => ({
|
|
url: '/echo',
|
|
method: 'POST',
|
|
body: { nested: { banana: 'bread' } },
|
|
}),
|
|
transformResponse: (response: { body: { nested: EchoResponseData } }) =>
|
|
response.body.nested,
|
|
}),
|
|
mutationWithError: build.mutation({
|
|
query: () => ({
|
|
url: '/error',
|
|
method: 'POST',
|
|
}),
|
|
transformErrorResponse: (response) => {
|
|
const data = response.data as ErrorResponse
|
|
return data.value
|
|
},
|
|
}),
|
|
mutationWithMeta: build.mutation({
|
|
query: () => ({
|
|
url: '/echo',
|
|
method: 'POST',
|
|
body: { nested: { banana: 'bread' } },
|
|
}),
|
|
transformResponse: (
|
|
response: { body: { nested: EchoResponseData } },
|
|
meta,
|
|
) => {
|
|
return {
|
|
...response.body.nested,
|
|
meta: {
|
|
request: { headers: getSerializedHeaders(meta?.request.headers) },
|
|
response: {
|
|
headers: getSerializedHeaders(meta?.response?.headers),
|
|
},
|
|
},
|
|
}
|
|
},
|
|
}),
|
|
query: build.query<SuccessResponse & EchoResponseData, void>({
|
|
query: () => '/success',
|
|
transformResponse: async (response: SuccessResponse) => {
|
|
const res: any = await nodeFetch('https://example.com/echo', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ banana: 'bread' }),
|
|
}).then((res) => res.json())
|
|
|
|
const additionalData = res.body as EchoResponseData
|
|
return { ...response, ...additionalData }
|
|
},
|
|
}),
|
|
queryWithMeta: build.query<SuccessResponse, void>({
|
|
query: () => '/success',
|
|
transformResponse: async (response: SuccessResponse, meta) => {
|
|
return {
|
|
...response,
|
|
meta: {
|
|
request: { headers: getSerializedHeaders(meta?.request.headers) },
|
|
response: {
|
|
headers: getSerializedHeaders(meta?.response?.headers),
|
|
},
|
|
},
|
|
}
|
|
},
|
|
}),
|
|
}),
|
|
})
|
|
|
|
const storeRef = setupApiStore(api)
|
|
|
|
test('transformResponse handles an async transformation and returns the merged data (query)', async () => {
|
|
const result = await storeRef.store.dispatch(api.endpoints.query.initiate())
|
|
|
|
expect(result.data).toEqual({ value: 'success', banana: 'bread' })
|
|
})
|
|
|
|
test('transformResponse transforms a response from a mutation', async () => {
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.mutation.initiate({}),
|
|
)
|
|
|
|
expect('data' in result && result.data).toEqual({ banana: 'bread' })
|
|
})
|
|
|
|
test('transformResponse transforms a response from a mutation with an error', async () => {
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.mutationWithError.initiate({}),
|
|
)
|
|
|
|
expect('error' in result && result.error).toEqual('error')
|
|
})
|
|
|
|
test('transformResponse can inject baseQuery meta into the end result from a mutation', async () => {
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.mutationWithMeta.initiate({}),
|
|
)
|
|
|
|
expect('data' in result && result.data).toEqual({
|
|
banana: 'bread',
|
|
meta: {
|
|
request: {
|
|
headers: {
|
|
accept: 'application/json',
|
|
'content-type': 'application/json',
|
|
},
|
|
},
|
|
response: {
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
},
|
|
},
|
|
},
|
|
})
|
|
})
|
|
|
|
test('transformResponse can inject baseQuery meta into the end result from a query', async () => {
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.queryWithMeta.initiate(),
|
|
)
|
|
|
|
expect(result.data).toEqual({
|
|
value: 'success',
|
|
meta: {
|
|
request: {
|
|
headers: {
|
|
accept: 'application/json',
|
|
},
|
|
},
|
|
response: {
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
},
|
|
},
|
|
},
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('query endpoint lifecycles - onStart, onSuccess, onError', () => {
|
|
const initialState = {
|
|
count: null as null | number,
|
|
}
|
|
const setCount = createAction<number>('setCount')
|
|
const testReducer = createReducer(initialState, (builder) => {
|
|
builder.addCase(setCount, (state, action) => {
|
|
state.count = action.payload
|
|
})
|
|
})
|
|
|
|
type SuccessResponse = { value: 'success' }
|
|
const api = createApi({
|
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|
endpoints: (build) => ({
|
|
echo: build.mutation({
|
|
query: () => ({ method: 'PUT', url: '/echo' }),
|
|
}),
|
|
query: build.query<SuccessResponse, void>({
|
|
query: () => '/success',
|
|
async onQueryStarted(_, api) {
|
|
api.dispatch(setCount(0))
|
|
try {
|
|
await api.queryFulfilled
|
|
api.dispatch(setCount(1))
|
|
} catch {
|
|
api.dispatch(setCount(-1))
|
|
}
|
|
},
|
|
}),
|
|
mutation: build.mutation<SuccessResponse, void>({
|
|
query: () => ({ url: '/success', method: 'POST' }),
|
|
async onQueryStarted(_, api) {
|
|
api.dispatch(setCount(0))
|
|
try {
|
|
await api.queryFulfilled
|
|
api.dispatch(setCount(1))
|
|
} catch {
|
|
api.dispatch(setCount(-1))
|
|
}
|
|
},
|
|
}),
|
|
}),
|
|
})
|
|
|
|
const storeRef = setupApiStore(api, { testReducer })
|
|
|
|
test('query lifecycle events fire properly', async () => {
|
|
// We intentionally fail the first request so we can test all lifecycles
|
|
server.use(
|
|
http.get(
|
|
'https://example.com/success',
|
|
() => HttpResponse.json({ value: 'failed' }, { status: 500 }),
|
|
{ once: true },
|
|
),
|
|
)
|
|
|
|
expect(storeRef.store.getState().testReducer.count).toBe(null)
|
|
const failAttempt = storeRef.store.dispatch(api.endpoints.query.initiate())
|
|
expect(storeRef.store.getState().testReducer.count).toBe(0)
|
|
await failAttempt
|
|
await delay(10)
|
|
expect(storeRef.store.getState().testReducer.count).toBe(-1)
|
|
|
|
const successAttempt = storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(storeRef.store.getState().testReducer.count).toBe(0)
|
|
await successAttempt
|
|
await delay(10)
|
|
expect(storeRef.store.getState().testReducer.count).toBe(1)
|
|
})
|
|
|
|
test('mutation lifecycle events fire properly', async () => {
|
|
// We intentionally fail the first request so we can test all lifecycles
|
|
server.use(
|
|
http.post(
|
|
'https://example.com/success',
|
|
() => HttpResponse.json({ value: 'failed' }, { status: 500 }),
|
|
{ once: true },
|
|
),
|
|
)
|
|
|
|
expect(storeRef.store.getState().testReducer.count).toBe(null)
|
|
const failAttempt = storeRef.store.dispatch(
|
|
api.endpoints.mutation.initiate(),
|
|
)
|
|
expect(storeRef.store.getState().testReducer.count).toBe(0)
|
|
await failAttempt
|
|
expect(storeRef.store.getState().testReducer.count).toBe(-1)
|
|
|
|
const successAttempt = storeRef.store.dispatch(
|
|
api.endpoints.mutation.initiate(),
|
|
)
|
|
expect(storeRef.store.getState().testReducer.count).toBe(0)
|
|
await successAttempt
|
|
expect(storeRef.store.getState().testReducer.count).toBe(1)
|
|
})
|
|
})
|
|
|
|
test('providesTags and invalidatesTags can use baseQueryMeta', async () => {
|
|
let _meta: FetchBaseQueryMeta | undefined
|
|
|
|
type SuccessResponse = { value: 'success' }
|
|
|
|
const api = createApi({
|
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|
tagTypes: ['success'],
|
|
endpoints: (build) => ({
|
|
query: build.query<SuccessResponse, void>({
|
|
query: () => '/success',
|
|
providesTags: (_result, _error, _arg, meta) => {
|
|
_meta = meta
|
|
return ['success']
|
|
},
|
|
}),
|
|
mutation: build.mutation<SuccessResponse, void>({
|
|
query: () => ({ url: '/success', method: 'POST' }),
|
|
invalidatesTags: (_result, _error, _arg, meta) => {
|
|
_meta = meta
|
|
return ['success']
|
|
},
|
|
}),
|
|
}),
|
|
})
|
|
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
|
|
await storeRef.store.dispatch(api.endpoints.query.initiate())
|
|
expect('request' in _meta! && 'response' in _meta!).toBe(true)
|
|
|
|
_meta = undefined
|
|
|
|
await storeRef.store.dispatch(api.endpoints.mutation.initiate())
|
|
|
|
expect('request' in _meta! && 'response' in _meta!).toBe(true)
|
|
})
|
|
|
|
describe('structuralSharing flag behaviors', () => {
|
|
type SuccessResponse = { value: 'success' }
|
|
|
|
const api = createApi({
|
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|
tagTypes: ['success'],
|
|
endpoints: (build) => ({
|
|
enabled: build.query<SuccessResponse, void>({
|
|
query: () => '/success',
|
|
}),
|
|
disabled: build.query<SuccessResponse, void>({
|
|
query: () => ({ url: '/success' }),
|
|
structuralSharing: false,
|
|
}),
|
|
}),
|
|
})
|
|
|
|
const storeRef = setupApiStore(api)
|
|
|
|
it('enables structural sharing for query endpoints by default', async () => {
|
|
await storeRef.store.dispatch(api.endpoints.enabled.initiate())
|
|
const firstRef = api.endpoints.enabled.select()(storeRef.store.getState())
|
|
|
|
await storeRef.store.dispatch(
|
|
api.endpoints.enabled.initiate(undefined, { forceRefetch: true }),
|
|
)
|
|
|
|
const secondRef = api.endpoints.enabled.select()(storeRef.store.getState())
|
|
|
|
expect(firstRef.requestId).not.toEqual(secondRef.requestId)
|
|
expect(firstRef.data === secondRef.data).toBeTruthy()
|
|
})
|
|
|
|
it('allows a query endpoint to opt-out of structural sharing', async () => {
|
|
await storeRef.store.dispatch(api.endpoints.disabled.initiate())
|
|
const firstRef = api.endpoints.disabled.select()(storeRef.store.getState())
|
|
|
|
await storeRef.store.dispatch(
|
|
api.endpoints.disabled.initiate(undefined, { forceRefetch: true }),
|
|
)
|
|
|
|
const secondRef = api.endpoints.disabled.select()(storeRef.store.getState())
|
|
|
|
expect(firstRef.requestId).not.toEqual(secondRef.requestId)
|
|
expect(firstRef.data === secondRef.data).toBeFalsy()
|
|
})
|
|
})
|
|
|
|
describe('custom serializeQueryArgs per endpoint', () => {
|
|
const customArgsSerializer: SerializeQueryArgs<number> = ({
|
|
endpointName,
|
|
queryArgs,
|
|
}) => `${endpointName}-${queryArgs}`
|
|
|
|
type SuccessResponse = { value: 'success' }
|
|
|
|
const serializer1 = vi.fn(customArgsSerializer)
|
|
|
|
interface MyApiClient {
|
|
fetchPost: (id: string) => Promise<SuccessResponse>
|
|
}
|
|
|
|
const dummyClient: MyApiClient = {
|
|
async fetchPost() {
|
|
return { value: 'success' }
|
|
},
|
|
}
|
|
|
|
const api = createApi({
|
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|
serializeQueryArgs: ({ endpointName, queryArgs }) =>
|
|
`base-${endpointName}-${queryArgs}`,
|
|
endpoints: (build) => ({
|
|
queryWithNoSerializer: build.query<SuccessResponse, number>({
|
|
query: (arg) => `${arg}`,
|
|
}),
|
|
queryWithCustomSerializer: build.query<SuccessResponse, number>({
|
|
query: (arg) => `${arg}`,
|
|
serializeQueryArgs: serializer1,
|
|
}),
|
|
queryWithCustomObjectSerializer: build.query<
|
|
SuccessResponse,
|
|
{ id: number; client: MyApiClient }
|
|
>({
|
|
query: (arg) => `${arg.id}`,
|
|
serializeQueryArgs: ({
|
|
endpointDefinition,
|
|
endpointName,
|
|
queryArgs,
|
|
}) => {
|
|
const { id } = queryArgs
|
|
return { id }
|
|
},
|
|
}),
|
|
queryWithCustomNumberSerializer: build.query<
|
|
SuccessResponse,
|
|
{ id: number; client: MyApiClient }
|
|
>({
|
|
query: (arg) => `${arg.id}`,
|
|
serializeQueryArgs: ({
|
|
endpointDefinition,
|
|
endpointName,
|
|
queryArgs,
|
|
}) => {
|
|
const { id } = queryArgs
|
|
return id
|
|
},
|
|
}),
|
|
listItems: build.query<string[], number>({
|
|
query: (pageNumber) => `/listItems?page=${pageNumber}`,
|
|
serializeQueryArgs: ({ endpointName }) => {
|
|
return endpointName
|
|
},
|
|
merge: (currentCache, newItems) => {
|
|
currentCache.push(...newItems)
|
|
},
|
|
forceRefetch({ currentArg, previousArg }) {
|
|
return currentArg !== previousArg
|
|
},
|
|
}),
|
|
listItems2: build.query<{ items: string[]; meta?: any }, number>({
|
|
query: (pageNumber) => `/listItems2?page=${pageNumber}`,
|
|
serializeQueryArgs: ({ endpointName }) => {
|
|
return endpointName
|
|
},
|
|
transformResponse(items: string[]) {
|
|
return { items }
|
|
},
|
|
merge: (currentCache, newData, meta) => {
|
|
currentCache.items.push(...newData.items)
|
|
currentCache.meta = meta
|
|
},
|
|
forceRefetch({ currentArg, previousArg }) {
|
|
return currentArg !== previousArg
|
|
},
|
|
}),
|
|
}),
|
|
})
|
|
|
|
const storeRef = setupApiStore(api)
|
|
|
|
it('Works via createApi', async () => {
|
|
await storeRef.store.dispatch(
|
|
api.endpoints.queryWithNoSerializer.initiate(99),
|
|
)
|
|
|
|
expect(serializer1).not.toHaveBeenCalled()
|
|
|
|
await storeRef.store.dispatch(
|
|
api.endpoints.queryWithCustomSerializer.initiate(42),
|
|
)
|
|
|
|
expect(serializer1).toHaveBeenCalled()
|
|
|
|
expect(
|
|
storeRef.store.getState().api.queries['base-queryWithNoSerializer-99'],
|
|
).toBeTruthy()
|
|
|
|
expect(
|
|
storeRef.store.getState().api.queries['queryWithCustomSerializer-42'],
|
|
).toBeTruthy()
|
|
})
|
|
|
|
const serializer2 = vi.fn(customArgsSerializer)
|
|
|
|
const injectedApi = api.injectEndpoints({
|
|
endpoints: (build) => ({
|
|
injectedQueryWithCustomSerializer: build.query<SuccessResponse, number>({
|
|
query: (arg) => `${arg}`,
|
|
serializeQueryArgs: serializer2,
|
|
}),
|
|
}),
|
|
})
|
|
|
|
it('Works via injectEndpoints', async () => {
|
|
expect(serializer2).not.toHaveBeenCalled()
|
|
|
|
await storeRef.store.dispatch(
|
|
injectedApi.endpoints.injectedQueryWithCustomSerializer.initiate(5),
|
|
)
|
|
|
|
expect(serializer2).toHaveBeenCalled()
|
|
expect(
|
|
storeRef.store.getState().api.queries[
|
|
'injectedQueryWithCustomSerializer-5'
|
|
],
|
|
).toBeTruthy()
|
|
})
|
|
|
|
test('Serializes a returned object for query args', async () => {
|
|
await storeRef.store.dispatch(
|
|
api.endpoints.queryWithCustomObjectSerializer.initiate({
|
|
id: 42,
|
|
client: dummyClient,
|
|
}),
|
|
)
|
|
|
|
expect(
|
|
storeRef.store.getState().api.queries[
|
|
'queryWithCustomObjectSerializer({"id":42})'
|
|
],
|
|
).toBeTruthy()
|
|
})
|
|
|
|
test('Serializes a returned primitive for query args', async () => {
|
|
await storeRef.store.dispatch(
|
|
api.endpoints.queryWithCustomNumberSerializer.initiate({
|
|
id: 42,
|
|
client: dummyClient,
|
|
}),
|
|
)
|
|
|
|
expect(
|
|
storeRef.store.getState().api.queries[
|
|
'queryWithCustomNumberSerializer(42)'
|
|
],
|
|
).toBeTruthy()
|
|
})
|
|
|
|
test('serializeQueryArgs + merge allows refetching as args change with same cache key', async () => {
|
|
const allItems = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'i']
|
|
const PAGE_SIZE = 3
|
|
|
|
server.use(
|
|
http.get('https://example.com/listItems', ({ request }) => {
|
|
const url = new URL(request.url)
|
|
const pageString = url.searchParams.get('page')
|
|
const pageNum = parseInt(pageString || '0')
|
|
|
|
const results = paginate(allItems, PAGE_SIZE, pageNum)
|
|
return HttpResponse.json(results)
|
|
}),
|
|
)
|
|
|
|
// Page number shouldn't matter here, because the cache key ignores that.
|
|
// We just need to select the only cache entry.
|
|
const selectListItems = api.endpoints.listItems.select(0)
|
|
|
|
await storeRef.store.dispatch(api.endpoints.listItems.initiate(1))
|
|
|
|
const initialEntry = selectListItems(storeRef.store.getState())
|
|
expect(initialEntry.data).toEqual(['a', 'b', 'c'])
|
|
|
|
await storeRef.store.dispatch(api.endpoints.listItems.initiate(2))
|
|
const updatedEntry = selectListItems(storeRef.store.getState())
|
|
expect(updatedEntry.data).toEqual(['a', 'b', 'c', 'd', 'e', 'f'])
|
|
})
|
|
|
|
test('merge receives a meta object as an argument', async () => {
|
|
const allItems = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'i']
|
|
const PAGE_SIZE = 3
|
|
|
|
server.use(
|
|
http.get('https://example.com/listItems2', ({ request }) => {
|
|
const url = new URL(request.url)
|
|
const pageString = url.searchParams.get('page')
|
|
const pageNum = parseInt(pageString || '0')
|
|
|
|
const results = paginate(allItems, PAGE_SIZE, pageNum)
|
|
return HttpResponse.json(results)
|
|
}),
|
|
)
|
|
|
|
const selectListItems = api.endpoints.listItems2.select(0)
|
|
|
|
await storeRef.store.dispatch(api.endpoints.listItems2.initiate(1))
|
|
await storeRef.store.dispatch(api.endpoints.listItems2.initiate(2))
|
|
const cacheEntry = selectListItems(storeRef.store.getState())
|
|
|
|
// Should have passed along the third arg from `merge` containing these fields
|
|
expect(cacheEntry.data?.meta).toEqual({
|
|
requestId: expect.any(String),
|
|
fulfilledTimeStamp: expect.any(Number),
|
|
arg: 2,
|
|
baseQueryMeta: expect.any(Object),
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('timeout behavior', () => {
|
|
test('triggers TIMEOUT_ERROR', async () => {
|
|
const api = createApi({
|
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com', timeout: 5 }),
|
|
endpoints: (build) => ({
|
|
query: build.query<unknown, void>({
|
|
query: () => '/success',
|
|
}),
|
|
}),
|
|
})
|
|
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
|
|
server.use(
|
|
http.get(
|
|
'https://example.com/success',
|
|
async () => {
|
|
await delay(50)
|
|
return HttpResponse.json({ value: 'failed' }, { status: 500 })
|
|
},
|
|
{ once: true },
|
|
),
|
|
)
|
|
|
|
const result = await storeRef.store.dispatch(api.endpoints.query.initiate())
|
|
|
|
expect(result?.error).toEqual({
|
|
status: 'TIMEOUT_ERROR',
|
|
error: expect.stringMatching(/^AbortError:/),
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('endpoint schemas', () => {
|
|
const schemaConverter: SchemaFailureConverter<
|
|
ReturnType<typeof fetchBaseQuery>
|
|
> = (error) => {
|
|
return {
|
|
status: 'CUSTOM_ERROR',
|
|
error: error.schemaName + ' failed validation',
|
|
data: error.issues,
|
|
}
|
|
}
|
|
|
|
const serializedSchemaError = {
|
|
name: 'SchemaError',
|
|
message: expect.any(String),
|
|
stack: expect.any(String),
|
|
} satisfies SerializedError
|
|
|
|
const onSchemaFailureGlobal = vi.fn<Parameters<SchemaFailureHandler>>()
|
|
const onSchemaFailureEndpoint = vi.fn<Parameters<SchemaFailureHandler>>()
|
|
afterEach(() => {
|
|
onSchemaFailureGlobal.mockClear()
|
|
onSchemaFailureEndpoint.mockClear()
|
|
})
|
|
|
|
function expectFailureHandlersToHaveBeenCalled({
|
|
schemaName,
|
|
value,
|
|
arg,
|
|
}: {
|
|
schemaName: `${SchemaType}Schema`
|
|
value: unknown
|
|
arg: unknown
|
|
}) {
|
|
for (const handler of [onSchemaFailureGlobal, onSchemaFailureEndpoint]) {
|
|
expect(handler).toHaveBeenCalledOnce()
|
|
const [namedError, info] = handler.mock.calls[0]
|
|
expect(namedError).toBeInstanceOf(NamedSchemaError)
|
|
expect(namedError.issues.length).toBeGreaterThan(0)
|
|
expect(namedError.value).toEqual(value)
|
|
expect(namedError.schemaName).toBe(schemaName)
|
|
expect(info.endpoint).toBe('query')
|
|
expect(info.type).toBe('query')
|
|
expect(info.arg).toEqual(arg)
|
|
}
|
|
}
|
|
|
|
interface SkipApiOptions {
|
|
globalSkip?: boolean
|
|
endpointSkip?: boolean
|
|
useArray?: boolean
|
|
globalCatch?: boolean
|
|
endpointCatch?: boolean
|
|
}
|
|
|
|
const apiOptions = (
|
|
type: SchemaType,
|
|
{ useArray, globalSkip, globalCatch }: SkipApiOptions = {},
|
|
) => ({
|
|
onSchemaFailure: onSchemaFailureGlobal,
|
|
skipSchemaValidation: useArray ? globalSkip && [type] : globalSkip,
|
|
catchSchemaFailure: globalCatch ? schemaConverter : undefined,
|
|
})
|
|
|
|
const endpointOptions = (
|
|
type: SchemaType,
|
|
{ useArray, endpointSkip, endpointCatch }: SkipApiOptions = {},
|
|
) => ({
|
|
onSchemaFailure: onSchemaFailureEndpoint,
|
|
skipSchemaValidation: useArray ? endpointSkip && [type] : endpointSkip,
|
|
catchSchemaFailure: endpointCatch ? schemaConverter : undefined,
|
|
})
|
|
|
|
const skipCases: [string, SkipApiOptions][] = [
|
|
['globally', { globalSkip: true }],
|
|
['on the endpoint', { endpointSkip: true }],
|
|
['globally (array)', { globalSkip: true, useArray: true }],
|
|
['on the endpoint (array)', { endpointSkip: true, useArray: true }],
|
|
]
|
|
|
|
describe('argSchema', () => {
|
|
const makeApi = (opts?: SkipApiOptions) =>
|
|
createApi({
|
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|
...apiOptions('arg', opts),
|
|
endpoints: (build) => ({
|
|
query: build.query<unknown, { id: number }>({
|
|
query: ({ id }) => `/post/${id}`,
|
|
argSchema: v.object({ id: v.number() }),
|
|
...endpointOptions('arg', opts),
|
|
}),
|
|
}),
|
|
})
|
|
test("can be used to validate the endpoint's arguments", async () => {
|
|
const api = makeApi()
|
|
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate({ id: 1 }),
|
|
)
|
|
|
|
expect(result?.error).toBeUndefined()
|
|
|
|
const invalidResult = await storeRef.store.dispatch(
|
|
// @ts-expect-error
|
|
api.endpoints.query.initiate({ id: '1' }),
|
|
)
|
|
|
|
expect(invalidResult?.error).toEqual(serializedSchemaError)
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'argSchema',
|
|
value: { id: '1' },
|
|
arg: { id: '1' },
|
|
})
|
|
})
|
|
|
|
test.each(skipCases)('can be skipped %s', async (_, arg) => {
|
|
const api = makeApi(arg)
|
|
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
|
|
const result = await storeRef.store.dispatch(
|
|
// @ts-expect-error
|
|
api.endpoints.query.initiate({ id: '1' }),
|
|
)
|
|
|
|
expect(result?.error).toBeUndefined()
|
|
})
|
|
// we only need to test this once
|
|
test('endpoint overrides global skip', async () => {
|
|
const api = makeApi({ globalSkip: true, endpointSkip: false })
|
|
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
|
|
const result = await storeRef.store.dispatch(
|
|
// @ts-expect-error
|
|
api.endpoints.query.initiate({ id: '1' }),
|
|
)
|
|
|
|
expect(result?.error).toEqual(serializedSchemaError)
|
|
})
|
|
|
|
test('can be converted to a standard error object at global level', async () => {
|
|
const api = makeApi({ globalCatch: true })
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
|
|
const result = await storeRef.store.dispatch(
|
|
// @ts-expect-error
|
|
api.endpoints.query.initiate({ id: '1' }),
|
|
)
|
|
|
|
expect(result?.error).toEqual({
|
|
status: 'CUSTOM_ERROR',
|
|
error: 'argSchema failed validation',
|
|
data: expect.any(Array),
|
|
})
|
|
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'argSchema',
|
|
value: { id: '1' },
|
|
arg: { id: '1' },
|
|
})
|
|
})
|
|
test('can be converted to a standard error object at endpoint level', async () => {
|
|
const api = makeApi({ endpointCatch: true })
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
|
|
const result = await storeRef.store.dispatch(
|
|
// @ts-expect-error
|
|
api.endpoints.query.initiate({ id: '1' }),
|
|
)
|
|
|
|
expect(result?.error).toEqual({
|
|
status: 'CUSTOM_ERROR',
|
|
error: 'argSchema failed validation',
|
|
data: expect.any(Array),
|
|
})
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'argSchema',
|
|
value: { id: '1' },
|
|
arg: { id: '1' },
|
|
})
|
|
})
|
|
})
|
|
describe('rawResponseSchema', () => {
|
|
const makeApi = (opts?: SkipApiOptions) =>
|
|
createApi({
|
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|
...apiOptions('rawResponse', opts),
|
|
endpoints: (build) => ({
|
|
query: build.query<{ success: boolean }, void>({
|
|
query: () => '/success',
|
|
rawResponseSchema: v.object({ value: v.literal('success!') }),
|
|
...endpointOptions('rawResponse', opts),
|
|
}),
|
|
}),
|
|
})
|
|
test("can be used to validate the endpoint's raw result", async () => {
|
|
const api = makeApi()
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toEqual(serializedSchemaError)
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'rawResponseSchema',
|
|
value: { value: 'success' },
|
|
arg: undefined,
|
|
})
|
|
})
|
|
test.each(skipCases)('can be skipped %s', async (_, arg) => {
|
|
const api = makeApi(arg)
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toBeUndefined()
|
|
})
|
|
test('can be skipped on the endpoint', async () => {
|
|
const api = makeApi({ endpointSkip: true })
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toBeUndefined()
|
|
})
|
|
test('can be converted to a standard error object at global level', async () => {
|
|
const api = makeApi({ globalCatch: true })
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toEqual({
|
|
status: 'CUSTOM_ERROR',
|
|
error: 'rawResponseSchema failed validation',
|
|
data: expect.any(Array),
|
|
})
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'rawResponseSchema',
|
|
value: { value: 'success' },
|
|
arg: undefined,
|
|
})
|
|
})
|
|
test('can be converted to a standard error object at endpoint level', async () => {
|
|
const api = makeApi({ endpointCatch: true })
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toEqual({
|
|
status: 'CUSTOM_ERROR',
|
|
error: 'rawResponseSchema failed validation',
|
|
data: expect.any(Array),
|
|
})
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'rawResponseSchema',
|
|
value: { value: 'success' },
|
|
arg: undefined,
|
|
})
|
|
})
|
|
})
|
|
describe('responseSchema', () => {
|
|
const makeApi = (opts?: SkipApiOptions) =>
|
|
createApi({
|
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|
...apiOptions('response', opts),
|
|
endpoints: (build) => ({
|
|
query: build.query<{ success: boolean }, void>({
|
|
query: () => '/success',
|
|
transformResponse: () => ({ success: false }),
|
|
responseSchema: v.object({ success: v.literal(true) }),
|
|
...endpointOptions('response', opts),
|
|
}),
|
|
}),
|
|
})
|
|
test("can be used to validate the endpoint's final result", async () => {
|
|
const api = makeApi()
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toEqual(serializedSchemaError)
|
|
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'responseSchema',
|
|
value: { success: false },
|
|
arg: undefined,
|
|
})
|
|
})
|
|
test.each(skipCases)('can be skipped %s', async (_, arg) => {
|
|
const api = makeApi(arg)
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toBeUndefined()
|
|
})
|
|
test('can be converted to a standard error object at global level', async () => {
|
|
const api = makeApi({ globalCatch: true })
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toEqual({
|
|
status: 'CUSTOM_ERROR',
|
|
error: 'responseSchema failed validation',
|
|
data: expect.any(Array),
|
|
})
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'responseSchema',
|
|
value: { success: false },
|
|
arg: undefined,
|
|
})
|
|
})
|
|
test('can be converted to a standard error object at endpoint level', async () => {
|
|
const api = makeApi({ endpointCatch: true })
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toEqual({
|
|
status: 'CUSTOM_ERROR',
|
|
error: 'responseSchema failed validation',
|
|
data: expect.any(Array),
|
|
})
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'responseSchema',
|
|
value: { success: false },
|
|
arg: undefined,
|
|
})
|
|
})
|
|
})
|
|
describe('rawErrorResponseSchema', () => {
|
|
const makeApi = (opts?: SkipApiOptions) =>
|
|
createApi({
|
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|
...apiOptions('rawErrorResponse', opts),
|
|
endpoints: (build) => ({
|
|
query: build.query<{ success: boolean }, void>({
|
|
query: () => '/error',
|
|
rawErrorResponseSchema: v.object({
|
|
status: v.pipe(v.number(), v.minValue(400), v.maxValue(499)),
|
|
data: v.unknown(),
|
|
}),
|
|
...endpointOptions('rawErrorResponse', opts),
|
|
}),
|
|
}),
|
|
})
|
|
test("can be used to validate the endpoint's raw error result", async () => {
|
|
const api = makeApi()
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toEqual(serializedSchemaError)
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'rawErrorResponseSchema',
|
|
value: { status: 500, data: { value: 'error' } },
|
|
arg: undefined,
|
|
})
|
|
})
|
|
test.each(skipCases)('can be skipped %s', async (_, arg) => {
|
|
const api = makeApi(arg)
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).not.toEqual(serializedSchemaError)
|
|
})
|
|
test('can be converted to a standard error object at global level', async () => {
|
|
const api = makeApi({ globalCatch: true })
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toEqual({
|
|
status: 'CUSTOM_ERROR',
|
|
error: 'rawErrorResponseSchema failed validation',
|
|
data: expect.any(Array),
|
|
})
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'rawErrorResponseSchema',
|
|
value: { status: 500, data: { value: 'error' } },
|
|
arg: undefined,
|
|
})
|
|
})
|
|
test('can be converted to a standard error object at endpoint level', async () => {
|
|
const api = makeApi({ endpointCatch: true })
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toEqual({
|
|
status: 'CUSTOM_ERROR',
|
|
error: 'rawErrorResponseSchema failed validation',
|
|
data: expect.any(Array),
|
|
})
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'rawErrorResponseSchema',
|
|
value: { status: 500, data: { value: 'error' } },
|
|
arg: undefined,
|
|
})
|
|
})
|
|
})
|
|
describe('errorResponseSchema', () => {
|
|
const makeApi = (opts?: SkipApiOptions) =>
|
|
createApi({
|
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|
...apiOptions('errorResponse', opts),
|
|
endpoints: (build) => ({
|
|
query: build.query<{ success: boolean }, void>({
|
|
query: () => '/error',
|
|
transformErrorResponse: (error): FetchBaseQueryError => ({
|
|
status: 'CUSTOM_ERROR',
|
|
data: error,
|
|
error: 'whoops',
|
|
}),
|
|
errorResponseSchema: v.object({
|
|
status: v.literal('CUSTOM_ERROR'),
|
|
error: v.literal('oh no'),
|
|
data: v.unknown(),
|
|
}),
|
|
...endpointOptions('errorResponse', opts),
|
|
}),
|
|
}),
|
|
})
|
|
test("can be used to validate the endpoint's final error result", async () => {
|
|
const api = makeApi()
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toEqual(serializedSchemaError)
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'errorResponseSchema',
|
|
value: {
|
|
status: 'CUSTOM_ERROR',
|
|
error: 'whoops',
|
|
data: { status: 500, data: { value: 'error' } },
|
|
},
|
|
arg: undefined,
|
|
})
|
|
})
|
|
test.each(skipCases)('can be skipped %s', async (_, arg) => {
|
|
const api = makeApi(arg)
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).not.toEqual(serializedSchemaError)
|
|
})
|
|
test('can be converted to a standard error object at global level', async () => {
|
|
const api = makeApi({ globalCatch: true })
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toEqual({
|
|
status: 'CUSTOM_ERROR',
|
|
error: 'errorResponseSchema failed validation',
|
|
data: expect.any(Array),
|
|
})
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'errorResponseSchema',
|
|
value: {
|
|
status: 'CUSTOM_ERROR',
|
|
error: 'whoops',
|
|
data: { status: 500, data: { value: 'error' } },
|
|
},
|
|
arg: undefined,
|
|
})
|
|
})
|
|
test('can be converted to a standard error object at endpoint level', async () => {
|
|
const api = makeApi({ endpointCatch: true })
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toEqual({
|
|
status: 'CUSTOM_ERROR',
|
|
error: 'errorResponseSchema failed validation',
|
|
data: expect.any(Array),
|
|
})
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'errorResponseSchema',
|
|
value: {
|
|
status: 'CUSTOM_ERROR',
|
|
error: 'whoops',
|
|
data: { status: 500, data: { value: 'error' } },
|
|
},
|
|
arg: undefined,
|
|
})
|
|
})
|
|
})
|
|
describe('metaSchema', () => {
|
|
const makeApi = (opts?: SkipApiOptions) =>
|
|
createApi({
|
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://example.com' }),
|
|
...apiOptions('meta', opts),
|
|
endpoints: (build) => ({
|
|
query: build.query<{ success: boolean }, void>({
|
|
query: () => '/success',
|
|
metaSchema: v.object({
|
|
request: v.instance(Request),
|
|
response: v.instance(Response),
|
|
timestamp: v.number(),
|
|
}),
|
|
...endpointOptions('meta', opts),
|
|
}),
|
|
}),
|
|
})
|
|
test("can be used to validate the endpoint's meta result", async () => {
|
|
const api = makeApi()
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toEqual(serializedSchemaError)
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'metaSchema',
|
|
value: {
|
|
request: expect.any(Request),
|
|
response: expect.any(Response),
|
|
},
|
|
arg: undefined,
|
|
})
|
|
})
|
|
test.each(skipCases)('can be skipped %s', async (_, arg) => {
|
|
const api = makeApi(arg)
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toBeUndefined()
|
|
})
|
|
test('can be converted to a standard error object at global level', async () => {
|
|
const api = makeApi({ globalCatch: true })
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toEqual({
|
|
status: 'CUSTOM_ERROR',
|
|
error: 'metaSchema failed validation',
|
|
data: expect.any(Array),
|
|
})
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'metaSchema',
|
|
value: {
|
|
request: expect.any(Request),
|
|
response: expect.any(Response),
|
|
},
|
|
arg: undefined,
|
|
})
|
|
})
|
|
test('can be converted to a standard error object at endpoint level', async () => {
|
|
const api = makeApi({ endpointCatch: true })
|
|
const storeRef = setupApiStore(api, undefined, {
|
|
withoutTestLifecycles: true,
|
|
})
|
|
const result = await storeRef.store.dispatch(
|
|
api.endpoints.query.initiate(),
|
|
)
|
|
expect(result?.error).toEqual({
|
|
status: 'CUSTOM_ERROR',
|
|
error: 'metaSchema failed validation',
|
|
data: expect.any(Array),
|
|
})
|
|
expectFailureHandlersToHaveBeenCalled({
|
|
schemaName: 'metaSchema',
|
|
value: {
|
|
request: expect.any(Request),
|
|
response: expect.any(Response),
|
|
},
|
|
arg: undefined,
|
|
})
|
|
})
|
|
})
|
|
})
|