@reduxjs/toolkit
Version:
The official, opinionated, batteries-included toolset for efficient Redux development
910 lines (755 loc) • 28.5 kB
text/typescript
import type { AnyAction } from '@reduxjs/toolkit'
import {
createAsyncThunk,
unwrapResult,
configureStore,
createReducer,
} from '@reduxjs/toolkit'
import { miniSerializeError } from '@internal/createAsyncThunk'
import {
mockConsole,
createConsole,
getLog,
} from 'console-testing-library/pure'
import { expectType } from './helpers'
declare global {
interface Window {
AbortController: AbortController
}
}
describe('createAsyncThunk', () => {
it('creates the action types', () => {
const thunkActionCreator = createAsyncThunk('testType', async () => 42)
expect(thunkActionCreator.fulfilled.type).toBe('testType/fulfilled')
expect(thunkActionCreator.pending.type).toBe('testType/pending')
expect(thunkActionCreator.rejected.type).toBe('testType/rejected')
})
it('exposes the typePrefix it was created with', () => {
const thunkActionCreator = createAsyncThunk('testType', async () => 42)
expect(thunkActionCreator.typePrefix).toBe('testType')
})
it('works without passing arguments to the payload creator', async () => {
const thunkActionCreator = createAsyncThunk('testType', async () => 42)
let timesReducerCalled = 0
const reducer = () => {
timesReducerCalled++
}
const store = configureStore({
reducer,
})
// reset from however many times the store called it
timesReducerCalled = 0
await store.dispatch(thunkActionCreator())
expect(timesReducerCalled).toBe(2)
})
it('accepts arguments and dispatches the actions on resolve', async () => {
const dispatch = jest.fn()
let passedArg: any
const result = 42
const args = 123
let generatedRequestId = ''
const thunkActionCreator = createAsyncThunk(
'testType',
async (arg: number, { requestId }) => {
passedArg = arg
generatedRequestId = requestId
return result
}
)
const thunkFunction = thunkActionCreator(args)
const thunkPromise = thunkFunction(dispatch, () => {}, undefined)
expect(thunkPromise.requestId).toBe(generatedRequestId)
expect(thunkPromise.arg).toBe(args)
await thunkPromise
expect(passedArg).toBe(args)
expect(dispatch).toHaveBeenNthCalledWith(
1,
thunkActionCreator.pending(generatedRequestId, args)
)
expect(dispatch).toHaveBeenNthCalledWith(
2,
thunkActionCreator.fulfilled(result, generatedRequestId, args)
)
})
it('accepts arguments and dispatches the actions on reject', async () => {
const dispatch = jest.fn()
const args = 123
let generatedRequestId = ''
const error = new Error('Panic!')
const thunkActionCreator = createAsyncThunk(
'testType',
async (args: number, { requestId }) => {
generatedRequestId = requestId
throw error
}
)
const thunkFunction = thunkActionCreator(args)
try {
await thunkFunction(dispatch, () => {}, undefined)
} catch (e) {}
expect(dispatch).toHaveBeenNthCalledWith(
1,
thunkActionCreator.pending(generatedRequestId, args)
)
expect(dispatch).toHaveBeenCalledTimes(2)
// Have to check the bits of the action separately since the error was processed
const errorAction = dispatch.mock.calls[1][0]
expect(errorAction.error).toEqual(miniSerializeError(error))
expect(errorAction.meta.requestId).toBe(generatedRequestId)
expect(errorAction.meta.arg).toBe(args)
})
it('dispatches an empty error when throwing a random object without serializedError properties', async () => {
const dispatch = jest.fn()
const args = 123
let generatedRequestId = ''
const errorObject = { wny: 'dothis' }
const thunkActionCreator = createAsyncThunk(
'testType',
async (args: number, { requestId }) => {
generatedRequestId = requestId
throw errorObject
}
)
const thunkFunction = thunkActionCreator(args)
try {
await thunkFunction(dispatch, () => {}, undefined)
} catch (e) {}
expect(dispatch).toHaveBeenNthCalledWith(
1,
thunkActionCreator.pending(generatedRequestId, args)
)
expect(dispatch).toHaveBeenCalledTimes(2)
const errorAction = dispatch.mock.calls[1][0]
expect(errorAction.error).toEqual({})
expect(errorAction.meta.requestId).toBe(generatedRequestId)
expect(errorAction.meta.arg).toBe(args)
})
it('dispatches an action with a formatted error when throwing an object with known error keys', async () => {
const dispatch = jest.fn()
const args = 123
let generatedRequestId = ''
const errorObject = {
name: 'Custom thrown error',
message: 'This is not necessary',
code: '400',
}
const thunkActionCreator = createAsyncThunk(
'testType',
async (args: number, { requestId }) => {
generatedRequestId = requestId
throw errorObject
}
)
const thunkFunction = thunkActionCreator(args)
try {
await thunkFunction(dispatch, () => {}, undefined)
} catch (e) {}
expect(dispatch).toHaveBeenNthCalledWith(
1,
thunkActionCreator.pending(generatedRequestId, args)
)
expect(dispatch).toHaveBeenCalledTimes(2)
// Have to check the bits of the action separately since the error was processed
const errorAction = dispatch.mock.calls[1][0]
expect(errorAction.error).toEqual(miniSerializeError(errorObject))
expect(Object.keys(errorAction.error)).not.toContain('stack')
expect(errorAction.meta.requestId).toBe(generatedRequestId)
expect(errorAction.meta.arg).toBe(args)
})
it('dispatches a rejected action with a customized payload when a user returns rejectWithValue()', async () => {
const dispatch = jest.fn()
const args = 123
let generatedRequestId = ''
const errorPayload = {
errorMessage:
'I am a fake server-provided 400 payload with validation details',
errors: [
{ field_one: 'Must be a string' },
{ field_two: 'Must be a number' },
],
}
const thunkActionCreator = createAsyncThunk(
'testType',
async (args: number, { requestId, rejectWithValue }) => {
generatedRequestId = requestId
return rejectWithValue(errorPayload)
}
)
const thunkFunction = thunkActionCreator(args)
try {
await thunkFunction(dispatch, () => {}, undefined)
} catch (e) {}
expect(dispatch).toHaveBeenNthCalledWith(
1,
thunkActionCreator.pending(generatedRequestId, args)
)
expect(dispatch).toHaveBeenCalledTimes(2)
// Have to check the bits of the action separately since the error was processed
const errorAction = dispatch.mock.calls[1][0]
expect(errorAction.error.message).toEqual('Rejected')
expect(errorAction.payload).toBe(errorPayload)
expect(errorAction.meta.arg).toBe(args)
})
it('dispatches a rejected action with a customized payload when a user throws rejectWithValue()', async () => {
const dispatch = jest.fn()
const args = 123
let generatedRequestId = ''
const errorPayload = {
errorMessage:
'I am a fake server-provided 400 payload with validation details',
errors: [
{ field_one: 'Must be a string' },
{ field_two: 'Must be a number' },
],
}
const thunkActionCreator = createAsyncThunk(
'testType',
async (args: number, { requestId, rejectWithValue }) => {
generatedRequestId = requestId
throw rejectWithValue(errorPayload)
}
)
const thunkFunction = thunkActionCreator(args)
try {
await thunkFunction(dispatch, () => {}, undefined)
} catch (e) {}
expect(dispatch).toHaveBeenNthCalledWith(
1,
thunkActionCreator.pending(generatedRequestId, args)
)
expect(dispatch).toHaveBeenCalledTimes(2)
// Have to check the bits of the action separately since the error was processed
const errorAction = dispatch.mock.calls[1][0]
expect(errorAction.error.message).toEqual('Rejected')
expect(errorAction.payload).toBe(errorPayload)
expect(errorAction.meta.arg).toBe(args)
})
it('dispatches a rejected action with a miniSerializeError when rejectWithValue conditions are not satisfied', async () => {
const dispatch = jest.fn()
const args = 123
let generatedRequestId = ''
const error = new Error('Panic!')
const errorPayload = {
errorMessage:
'I am a fake server-provided 400 payload with validation details',
errors: [
{ field_one: 'Must be a string' },
{ field_two: 'Must be a number' },
],
}
const thunkActionCreator = createAsyncThunk(
'testType',
async (args: number, { requestId, rejectWithValue }) => {
generatedRequestId = requestId
try {
throw error
} catch (err) {
if (!(err as any).response) {
throw err
}
return rejectWithValue(errorPayload)
}
}
)
const thunkFunction = thunkActionCreator(args)
try {
await thunkFunction(dispatch, () => {}, undefined)
} catch (e) {}
expect(dispatch).toHaveBeenNthCalledWith(
1,
thunkActionCreator.pending(generatedRequestId, args)
)
expect(dispatch).toHaveBeenCalledTimes(2)
// Have to check the bits of the action separately since the error was processed
const errorAction = dispatch.mock.calls[1][0]
expect(errorAction.error).toEqual(miniSerializeError(error))
expect(errorAction.payload).toEqual(undefined)
expect(errorAction.meta.requestId).toBe(generatedRequestId)
expect(errorAction.meta.arg).toBe(args)
})
})
describe('createAsyncThunk with abortController', () => {
const asyncThunk = createAsyncThunk(
'test',
function abortablePayloadCreator(_: any, { signal }) {
return new Promise((resolve, reject) => {
if (signal.aborted) {
reject(
new DOMException(
'This should never be reached as it should already be handled.',
'AbortError'
)
)
}
signal.addEventListener('abort', () => {
reject(new DOMException('Was aborted while running', 'AbortError'))
})
setTimeout(resolve, 100)
})
}
)
let store = configureStore({
reducer(store: AnyAction[] = []) {
return store
},
})
beforeEach(() => {
store = configureStore({
reducer(store: AnyAction[] = [], action) {
return [...store, action]
},
})
})
test('normal usage', async () => {
await store.dispatch(asyncThunk({}))
expect(store.getState()).toEqual([
expect.any(Object),
expect.objectContaining({ type: 'test/pending' }),
expect.objectContaining({ type: 'test/fulfilled' }),
])
})
test('abort after dispatch', async () => {
const promise = store.dispatch(asyncThunk({}))
promise.abort('AbortReason')
const result = await promise
const expectedAbortedAction = {
type: 'test/rejected',
error: {
message: 'AbortReason',
name: 'AbortError',
},
meta: { aborted: true, requestId: promise.requestId },
}
// abortedAction with reason is dispatched after test/pending is dispatched
expect(store.getState()).toMatchObject([
{},
{ type: 'test/pending' },
expectedAbortedAction,
])
// same abortedAction is returned, but with the AbortError from the abortablePayloadCreator
expect(result).toMatchObject(expectedAbortedAction)
// calling unwrapResult on the returned object re-throws the error from the abortablePayloadCreator
expect(() => unwrapResult(result)).toThrowError(
expect.objectContaining(expectedAbortedAction.error)
)
})
test('even when the payloadCreator does not directly support the signal, no further actions are dispatched', async () => {
const unawareAsyncThunk = createAsyncThunk('unaware', async () => {
await new Promise((resolve) => setTimeout(resolve, 100))
return 'finished'
})
const promise = store.dispatch(unawareAsyncThunk())
promise.abort('AbortReason')
const result = await promise
const expectedAbortedAction = {
type: 'unaware/rejected',
error: {
message: 'AbortReason',
name: 'AbortError',
},
}
// abortedAction with reason is dispatched after test/pending is dispatched
expect(store.getState()).toEqual([
expect.any(Object),
expect.objectContaining({ type: 'unaware/pending' }),
expect.objectContaining(expectedAbortedAction),
])
// same abortedAction is returned, but with the AbortError from the abortablePayloadCreator
expect(result).toMatchObject(expectedAbortedAction)
// calling unwrapResult on the returned object re-throws the error from the abortablePayloadCreator
expect(() => unwrapResult(result)).toThrowError(
expect.objectContaining(expectedAbortedAction.error)
)
})
test('dispatch(asyncThunk) returns on abort and does not wait for the promiseProvider to finish', async () => {
let running = false
const longRunningAsyncThunk = createAsyncThunk('longRunning', async () => {
running = true
await new Promise((resolve) => setTimeout(resolve, 30000))
running = false
})
const promise = store.dispatch(longRunningAsyncThunk())
expect(running).toBeTruthy()
promise.abort()
const result = await promise
expect(running).toBeTruthy()
expect(result).toMatchObject({
type: 'longRunning/rejected',
error: { message: 'Aborted', name: 'AbortError' },
meta: { aborted: true },
})
})
describe('behaviour with missing AbortController', () => {
let keepAbortController: typeof window['AbortController']
let freshlyLoadedModule: typeof import('../createAsyncThunk')
let restore: () => void
let nodeEnv: string
beforeEach(() => {
keepAbortController = window.AbortController
delete (window as any).AbortController
jest.resetModules()
freshlyLoadedModule = require('../createAsyncThunk')
restore = mockConsole(createConsole())
nodeEnv = process.env.NODE_ENV!
;(process.env as any).NODE_ENV = 'development'
})
afterEach(() => {
;(process.env as any).NODE_ENV = nodeEnv
restore()
window.AbortController = keepAbortController
jest.resetModules()
})
test('calling `abort` on an asyncThunk works with a FallbackAbortController if no global abortController is not available', async () => {
const longRunningAsyncThunk = freshlyLoadedModule.createAsyncThunk(
'longRunning',
async () => {
await new Promise((resolve) => setTimeout(resolve, 30000))
}
)
store.dispatch(longRunningAsyncThunk()).abort()
// should only log once, even if called twice
store.dispatch(longRunningAsyncThunk()).abort()
expect(getLog().log).toMatchInlineSnapshot(`
"This platform does not implement AbortController.
If you want to use the AbortController to react to \`abort\` events, please consider importing a polyfill like 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'."
`)
})
})
})
test('non-serializable arguments are ignored by serializableStateInvariantMiddleware', async () => {
const restore = mockConsole(createConsole())
const nonSerializableValue = new Map()
const asyncThunk = createAsyncThunk('test', (arg: Map<any, any>) => {})
configureStore({
reducer: () => 0,
}).dispatch(asyncThunk(nonSerializableValue))
expect(getLog().log).toMatchInlineSnapshot(`""`)
restore()
})
describe('conditional skipping of asyncThunks', () => {
const arg = {}
const getState = jest.fn(() => ({}))
const dispatch = jest.fn((x: any) => x)
const payloadCreator = jest.fn((x: typeof arg) => 10)
const condition = jest.fn(() => false)
const extra = {}
beforeEach(() => {
getState.mockClear()
dispatch.mockClear()
payloadCreator.mockClear()
condition.mockClear()
})
test('returning false from condition skips payloadCreator and returns a rejected action', async () => {
const asyncThunk = createAsyncThunk('test', payloadCreator, { condition })
const result = await asyncThunk(arg)(dispatch, getState, extra)
expect(condition).toHaveBeenCalled()
expect(payloadCreator).not.toHaveBeenCalled()
expect(asyncThunk.rejected.match(result)).toBe(true)
expect((result as any).meta.condition).toBe(true)
})
test('return falsy from condition does not skip payload creator', async () => {
// Override TS's expectation that this is a boolean
condition.mockReturnValueOnce(undefined as unknown as boolean)
const asyncThunk = createAsyncThunk('test', payloadCreator, { condition })
const result = await asyncThunk(arg)(dispatch, getState, extra)
expect(condition).toHaveBeenCalled()
expect(payloadCreator).toHaveBeenCalled()
expect(asyncThunk.fulfilled.match(result)).toBe(true)
expect(result.payload).toBe(10)
})
test('returning true from condition executes payloadCreator', async () => {
condition.mockReturnValueOnce(true)
const asyncThunk = createAsyncThunk('test', payloadCreator, { condition })
const result = await asyncThunk(arg)(dispatch, getState, extra)
expect(condition).toHaveBeenCalled()
expect(payloadCreator).toHaveBeenCalled()
expect(asyncThunk.fulfilled.match(result)).toBe(true)
expect(result.payload).toBe(10)
})
test('condition is called with arg, getState and extra', async () => {
const asyncThunk = createAsyncThunk('test', payloadCreator, { condition })
await asyncThunk(arg)(dispatch, getState, extra)
expect(condition).toHaveBeenCalledTimes(1)
expect(condition).toHaveBeenLastCalledWith(
arg,
expect.objectContaining({ getState, extra })
)
})
test('rejected action is not dispatched by default', async () => {
const asyncThunk = createAsyncThunk('test', payloadCreator, { condition })
await asyncThunk(arg)(dispatch, getState, extra)
expect(dispatch).toHaveBeenCalledTimes(0)
})
test('does not fail when attempting to abort a canceled promise', async () => {
const asyncPayloadCreator = jest.fn(async (x: typeof arg) => {
await new Promise((resolve) => setTimeout(resolve, 2000))
return 10
})
const asyncThunk = createAsyncThunk('test', asyncPayloadCreator, {
condition,
})
const promise = asyncThunk(arg)(dispatch, getState, extra)
promise.abort(
`If the promise was 1. somehow canceled, 2. in a 'started' state and 3. we attempted to abort, this would crash the tests`
)
})
test('rejected action can be dispatched via option', async () => {
const asyncThunk = createAsyncThunk('test', payloadCreator, {
condition,
dispatchConditionRejection: true,
})
await asyncThunk(arg)(dispatch, getState, extra)
expect(dispatch).toHaveBeenCalledTimes(1)
expect(dispatch).toHaveBeenLastCalledWith(
expect.objectContaining({
error: {
message: 'Aborted due to condition callback returning false.',
name: 'ConditionError',
},
meta: {
aborted: false,
arg: arg,
rejectedWithValue: false,
condition: true,
requestId: expect.stringContaining(''),
requestStatus: 'rejected',
},
payload: undefined,
type: 'test/rejected',
})
)
})
test('serializeError implementation', async () => {
function serializeError() {
return 'serialized!'
}
const errorObject = 'something else!'
const store = configureStore({
reducer: (state = [], action) => [...state, action],
})
const asyncThunk = createAsyncThunk<
unknown,
void,
{ serializedErrorType: string }
>('test', () => Promise.reject(errorObject), { serializeError })
const rejected = await store.dispatch(asyncThunk())
if (!asyncThunk.rejected.match(rejected)) {
throw new Error()
}
const expectation = {
type: 'test/rejected',
payload: undefined,
error: 'serialized!',
meta: expect.any(Object),
}
expect(rejected).toEqual(expectation)
expect(store.getState()[2]).toEqual(expectation)
expect(rejected.error).not.toEqual(miniSerializeError(errorObject))
})
})
describe('unwrapResult', () => {
const getState = jest.fn(() => ({}))
const dispatch = jest.fn((x: any) => x)
const extra = {}
test('fulfilled case', async () => {
const asyncThunk = createAsyncThunk('test', () => {
return 'fulfilled!' as const
})
const unwrapPromise = asyncThunk()(dispatch, getState, extra).then(
unwrapResult
)
await expect(unwrapPromise).resolves.toBe('fulfilled!')
const unwrapPromise2 = asyncThunk()(dispatch, getState, extra)
const res = await unwrapPromise2.unwrap()
expect(res).toBe('fulfilled!')
})
test('error case', async () => {
const error = new Error('Panic!')
const asyncThunk = createAsyncThunk('test', () => {
throw error
})
const unwrapPromise = asyncThunk()(dispatch, getState, extra).then(
unwrapResult
)
await expect(unwrapPromise).rejects.toEqual(miniSerializeError(error))
const unwrapPromise2 = asyncThunk()(dispatch, getState, extra)
await expect(unwrapPromise2.unwrap()).rejects.toEqual(
miniSerializeError(error)
)
})
test('rejectWithValue case', async () => {
const asyncThunk = createAsyncThunk('test', (_, { rejectWithValue }) => {
return rejectWithValue('rejectWithValue!')
})
const unwrapPromise = asyncThunk()(dispatch, getState, extra).then(
unwrapResult
)
await expect(unwrapPromise).rejects.toBe('rejectWithValue!')
const unwrapPromise2 = asyncThunk()(dispatch, getState, extra)
await expect(unwrapPromise2.unwrap()).rejects.toBe('rejectWithValue!')
})
})
describe('idGenerator option', () => {
const getState = () => ({})
const dispatch = (x: any) => x
const extra = {}
test('idGenerator implementation - can customizes how request IDs are generated', async () => {
function makeFakeIdGenerator() {
let id = 0
return jest.fn(() => {
id++
return `fake-random-id-${id}`
})
}
let generatedRequestId = ''
const idGenerator = makeFakeIdGenerator()
const asyncThunk = createAsyncThunk(
'test',
async (args: void, { requestId }) => {
generatedRequestId = requestId
},
{ idGenerator }
)
// dispatching the thunks should be using the custom id generator
const promise0 = asyncThunk()(dispatch, getState, extra)
expect(generatedRequestId).toEqual('fake-random-id-1')
expect(promise0.requestId).toEqual('fake-random-id-1')
expect((await promise0).meta.requestId).toEqual('fake-random-id-1')
const promise1 = asyncThunk()(dispatch, getState, extra)
expect(generatedRequestId).toEqual('fake-random-id-2')
expect(promise1.requestId).toEqual('fake-random-id-2')
expect((await promise1).meta.requestId).toEqual('fake-random-id-2')
const promise2 = asyncThunk()(dispatch, getState, extra)
expect(generatedRequestId).toEqual('fake-random-id-3')
expect(promise2.requestId).toEqual('fake-random-id-3')
expect((await promise2).meta.requestId).toEqual('fake-random-id-3')
generatedRequestId = ''
const defaultAsyncThunk = createAsyncThunk(
'test',
async (args: void, { requestId }) => {
generatedRequestId = requestId
}
)
// dispatching the default options thunk should still generate an id,
// but not using the custom id generator
const promise3 = defaultAsyncThunk()(dispatch, getState, extra)
expect(generatedRequestId).toEqual(promise3.requestId)
expect(promise3.requestId).not.toEqual('')
expect(promise3.requestId).not.toEqual(
expect.stringContaining('fake-random-id')
)
expect((await promise3).meta.requestId).not.toEqual(
expect.stringContaining('fake-fandom-id')
)
})
})
test('`condition` will see state changes from a synchonously invoked asyncThunk', () => {
type State = ReturnType<typeof store.getState>
const onStart = jest.fn()
const asyncThunk = createAsyncThunk<
void,
{ force?: boolean },
{ state: State }
>('test', onStart, {
condition({ force }, { getState }) {
return force || !getState().started
},
})
const store = configureStore({
reducer: createReducer({ started: false }, (builder) => {
builder.addCase(asyncThunk.pending, (state) => {
state.started = true
})
}),
})
store.dispatch(asyncThunk({ force: false }))
expect(onStart).toHaveBeenCalledTimes(1)
store.dispatch(asyncThunk({ force: false }))
expect(onStart).toHaveBeenCalledTimes(1)
store.dispatch(asyncThunk({ force: true }))
expect(onStart).toHaveBeenCalledTimes(2)
})
describe('meta', () => {
const getNewStore = () =>
configureStore({
reducer(actions = [], action) {
return [...actions, action]
},
})
let store = getNewStore()
beforeEach(() => {
const store = getNewStore()
})
test('pendingMeta', () => {
const pendingThunk = createAsyncThunk('test', (arg: string) => {}, {
getPendingMeta({ arg, requestId }) {
expect(arg).toBe('testArg')
expect(requestId).toEqual(expect.any(String))
return { extraProp: 'foo' }
},
})
const ret = store.dispatch(pendingThunk('testArg'))
expect(store.getState()[1]).toEqual({
meta: {
arg: 'testArg',
extraProp: 'foo',
requestId: ret.requestId,
requestStatus: 'pending',
},
payload: undefined,
type: 'test/pending',
})
})
test('fulfilledMeta', async () => {
const fulfilledThunk = createAsyncThunk<
string,
string,
{ fulfilledMeta: { extraProp: string } }
>('test', (arg: string, { fulfillWithValue }) => {
return fulfillWithValue('hooray!', { extraProp: 'bar' })
})
const ret = store.dispatch(fulfilledThunk('testArg'))
expect(await ret).toEqual({
meta: {
arg: 'testArg',
extraProp: 'bar',
requestId: ret.requestId,
requestStatus: 'fulfilled',
},
payload: 'hooray!',
type: 'test/fulfilled',
})
})
test('rejectedMeta', async () => {
const fulfilledThunk = createAsyncThunk<
string,
string,
{ rejectedMeta: { extraProp: string } }
>('test', (arg: string, { rejectWithValue }) => {
return rejectWithValue('damn!', { extraProp: 'baz' })
})
const promise = store.dispatch(fulfilledThunk('testArg'))
const ret = await promise
expect(ret).toEqual({
meta: {
arg: 'testArg',
extraProp: 'baz',
requestId: promise.requestId,
requestStatus: 'rejected',
rejectedWithValue: true,
aborted: false,
condition: false,
},
error: { message: 'Rejected' },
payload: 'damn!',
type: 'test/rejected',
})
if (ret.meta.requestStatus === 'rejected' && ret.meta.rejectedWithValue) {
expectType<string>(ret.meta.extraProp)
} else {
// could be caused by a `throw`, `abort()` or `condition` - no `rejectedMeta` in that case
// @ts-expect-error
ret.meta.extraProp
}
})
})