import type { UnknownAction } from '@reduxjs/toolkit' import { configureStore, createAsyncThunk, createReducer, unwrapResult, miniSerializeError, } from '@reduxjs/toolkit' import { vi } from 'vitest' import { createConsole, getLog, mockConsole, } from 'console-testing-library/pure' import { delay } from '@internal/utils' 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('includes a settled matcher', () => { const thunkActionCreator = createAsyncThunk('testType', async () => 42) expect(thunkActionCreator.settled).toEqual(expect.any(Function)) expect(thunkActionCreator.settled(thunkActionCreator.pending(''))).toBe( false, ) expect( thunkActionCreator.settled(thunkActionCreator.rejected(null, '')), ).toBe(true) expect( thunkActionCreator.settled(thunkActionCreator.fulfilled(42, '')), ).toBe(true) }) 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 = vi.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 = vi.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 = vi.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 = vi.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 = vi.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 = vi.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 = vi.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: UnknownAction[] = []) { return store }, }) beforeEach(() => { store = configureStore({ reducer(store: UnknownAction[] = [], 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(async () => { keepAbortController = window.AbortController delete (window as any).AbortController vi.resetModules() freshlyLoadedModule = await import('../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 vi.resetModules() }) test('calling a thunk made with createAsyncThunk should fail if no global abortController is not available', async () => { const longRunningAsyncThunk = freshlyLoadedModule.createAsyncThunk( 'longRunning', async () => { await new Promise((resolve) => setTimeout(resolve, 30000)) }, ) expect(longRunningAsyncThunk()).toThrow('AbortController is not defined') }) }) }) test('non-serializable arguments are ignored by serializableStateInvariantMiddleware', async () => { const restore = mockConsole(createConsole()) const nonSerializableValue = new Map() const asyncThunk = createAsyncThunk('test', (arg: Map) => {}) configureStore({ reducer: () => 0, }).dispatch(asyncThunk(nonSerializableValue)) expect(getLog().log).toMatchInlineSnapshot(`""`) restore() }) describe('conditional skipping of asyncThunks', () => { const arg = {} const getState = vi.fn(() => ({})) const dispatch = vi.fn((x: any) => x) const payloadCreator = vi.fn((x: typeof arg) => 10) const condition = vi.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('pending is dispatched synchronously if condition is synchronous', async () => { const condition = () => true const asyncThunk = createAsyncThunk('test', payloadCreator, { condition }) const thunkCallPromise = asyncThunk(arg)(dispatch, getState, extra) expect(dispatch).toHaveBeenCalledTimes(1) await thunkCallPromise expect(dispatch).toHaveBeenCalledTimes(2) }) test('async condition', async () => { const condition = () => Promise.resolve(false) const asyncThunk = createAsyncThunk('test', payloadCreator, { condition }) await asyncThunk(arg)(dispatch, getState, extra) expect(dispatch).toHaveBeenCalledTimes(0) }) test('async condition with rejected promise', async () => { const condition = () => Promise.reject() const asyncThunk = createAsyncThunk('test', payloadCreator, { condition }) await asyncThunk(arg)(dispatch, getState, extra) expect(dispatch).toHaveBeenCalledTimes(1) expect(dispatch).toHaveBeenLastCalledWith( expect.objectContaining({ type: 'test/rejected' }), ) }) test('async condition with AbortController signal first', async () => { const condition = async () => { await delay(25) return true } const asyncThunk = createAsyncThunk('test', payloadCreator, { condition }) try { const thunkPromise = asyncThunk(arg)(dispatch, getState, extra) thunkPromise.abort() await thunkPromise } catch (err) {} expect(dispatch).toHaveBeenCalledTimes(0) }) 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 = vi.fn(async (x: typeof arg) => { await delay(200) 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, 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 = vi.fn(() => ({})) const dispatch = vi.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 vi.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('idGenerator should be called with thunkArg', async () => { const customIdGenerator = vi.fn((seed) => `fake-unique-random-id-${seed}`) let generatedRequestId = '' const asyncThunk = createAsyncThunk( 'test', async (args: any, { requestId }) => { generatedRequestId = requestId }, { idGenerator: customIdGenerator }, ) const thunkArg = 1 const expected = 'fake-unique-random-id-1' const asyncThunkPromise = asyncThunk(thunkArg)(dispatch, getState, extra) expect(customIdGenerator).toHaveBeenCalledWith(thunkArg) expect(asyncThunkPromise.requestId).toEqual(expected) expect((await asyncThunkPromise).meta.requestId).toEqual(expected) }) }) test('`condition` will see state changes from a synchronously invoked asyncThunk', () => { type State = ReturnType const onStart = vi.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] }, }) const 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) { } else { // could be caused by a `throw`, `abort()` or `condition` - no `rejectedMeta` in that case // @ts-expect-error ret.meta.extraProp } }) test('typed createAsyncThunk.withTypes', () => { const typedCAT = createAsyncThunk.withTypes<{ state: { s: string } rejectValue: string extra: { s: string; n: number } }>() const thunk = typedCAT('a', () => 'b') const expectFunction = expect.any(Function) expect(thunk.fulfilled).toEqual(expectFunction) expect(thunk.pending).toEqual(expectFunction) expect(thunk.rejected).toEqual(expectFunction) expect(thunk.settled).toEqual(expectFunction) expect(thunk.fulfilled.type).toBe('a/fulfilled') }) })