988 lines
28 KiB
Plaintext
988 lines
28 KiB
Plaintext
import { noop } from '@internal/listenerMiddleware/utils'
|
|
import type { PayloadAction, WithSlice } from '@reduxjs/toolkit'
|
|
import {
|
|
asyncThunkCreator,
|
|
buildCreateSlice,
|
|
combineSlices,
|
|
configureStore,
|
|
createAction,
|
|
createAsyncThunk,
|
|
createSlice,
|
|
} from '@reduxjs/toolkit'
|
|
|
|
type CreateSlice = typeof createSlice
|
|
|
|
describe('createSlice', () => {
|
|
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(noop)
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
afterAll(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
describe('when slice is undefined', () => {
|
|
it('should throw an error', () => {
|
|
expect(() =>
|
|
// @ts-ignore
|
|
createSlice({
|
|
reducers: {
|
|
increment: (state) => state + 1,
|
|
multiply: (state, action: PayloadAction<number>) =>
|
|
state * action.payload,
|
|
},
|
|
initialState: 0,
|
|
}),
|
|
).toThrowError()
|
|
})
|
|
})
|
|
|
|
describe('when slice is an empty string', () => {
|
|
it('should throw an error', () => {
|
|
expect(() =>
|
|
createSlice({
|
|
name: '',
|
|
reducers: {
|
|
increment: (state) => state + 1,
|
|
multiply: (state, action: PayloadAction<number>) =>
|
|
state * action.payload,
|
|
},
|
|
initialState: 0,
|
|
}),
|
|
).toThrowError()
|
|
})
|
|
})
|
|
|
|
describe('when initial state is undefined', () => {
|
|
beforeEach(() => {
|
|
vi.stubEnv('NODE_ENV', 'development')
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs()
|
|
})
|
|
|
|
it('should throw an error', () => {
|
|
createSlice({
|
|
name: 'test',
|
|
reducers: {},
|
|
initialState: undefined,
|
|
})
|
|
|
|
expect(consoleErrorSpy).toHaveBeenCalledOnce()
|
|
|
|
expect(consoleErrorSpy).toHaveBeenLastCalledWith(
|
|
'You must provide an `initialState` value that is not `undefined`. You may have misspelled `initialState`',
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('when passing slice', () => {
|
|
const { actions, reducer, caseReducers } = createSlice({
|
|
reducers: {
|
|
increment: (state) => state + 1,
|
|
},
|
|
initialState: 0,
|
|
name: 'cool',
|
|
})
|
|
|
|
it('should create increment action', () => {
|
|
expect(actions.hasOwnProperty('increment')).toBe(true)
|
|
})
|
|
|
|
it('should have the correct action for increment', () => {
|
|
expect(actions.increment()).toEqual({
|
|
type: 'cool/increment',
|
|
payload: undefined,
|
|
})
|
|
})
|
|
|
|
it('should return the correct value from reducer', () => {
|
|
expect(reducer(undefined, actions.increment())).toEqual(1)
|
|
})
|
|
|
|
it('should include the generated case reducers', () => {
|
|
expect(caseReducers).toBeTruthy()
|
|
expect(caseReducers.increment).toBeTruthy()
|
|
expect(typeof caseReducers.increment).toBe('function')
|
|
})
|
|
|
|
it('getInitialState should return the state', () => {
|
|
const initialState = 42
|
|
const slice = createSlice({
|
|
name: 'counter',
|
|
initialState,
|
|
reducers: {},
|
|
})
|
|
|
|
expect(slice.getInitialState()).toBe(initialState)
|
|
})
|
|
|
|
it('should allow non-draftable initial state', () => {
|
|
expect(() =>
|
|
createSlice({
|
|
name: 'params',
|
|
initialState: new URLSearchParams(),
|
|
reducers: {},
|
|
}),
|
|
).not.toThrowError()
|
|
})
|
|
})
|
|
|
|
describe('when initialState is a function', () => {
|
|
const initialState = () => ({ user: '' })
|
|
|
|
const { actions, reducer } = createSlice({
|
|
reducers: {
|
|
setUserName: (state, action) => {
|
|
state.user = action.payload
|
|
},
|
|
},
|
|
initialState,
|
|
name: 'user',
|
|
})
|
|
|
|
it('should set the username', () => {
|
|
expect(reducer(undefined, actions.setUserName('eric'))).toEqual({
|
|
user: 'eric',
|
|
})
|
|
})
|
|
|
|
it('getInitialState should return the state', () => {
|
|
const initialState = () => 42
|
|
const slice = createSlice({
|
|
name: 'counter',
|
|
initialState,
|
|
reducers: {},
|
|
})
|
|
|
|
expect(slice.getInitialState()).toBe(42)
|
|
})
|
|
|
|
it('should allow non-draftable initial state', () => {
|
|
expect(() =>
|
|
createSlice({
|
|
name: 'params',
|
|
initialState: () => new URLSearchParams(),
|
|
reducers: {},
|
|
}),
|
|
).not.toThrowError()
|
|
})
|
|
})
|
|
|
|
describe('when mutating state object', () => {
|
|
const initialState = { user: '' }
|
|
|
|
const { actions, reducer } = createSlice({
|
|
reducers: {
|
|
setUserName: (state, action) => {
|
|
state.user = action.payload
|
|
},
|
|
},
|
|
initialState,
|
|
name: 'user',
|
|
})
|
|
|
|
it('should set the username', () => {
|
|
expect(reducer(initialState, actions.setUserName('eric'))).toEqual({
|
|
user: 'eric',
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('when passing extra reducers', () => {
|
|
const addMore = createAction<{ amount: number }>('ADD_MORE')
|
|
|
|
const { reducer } = createSlice({
|
|
name: 'test',
|
|
reducers: {
|
|
increment: (state) => state + 1,
|
|
multiply: (state, action) => state * action.payload,
|
|
},
|
|
extraReducers: (builder) => {
|
|
builder.addCase(
|
|
addMore,
|
|
(state, action) => state + action.payload.amount,
|
|
)
|
|
},
|
|
|
|
initialState: 0,
|
|
})
|
|
|
|
it('should call extra reducers when their actions are dispatched', () => {
|
|
const result = reducer(10, addMore({ amount: 5 }))
|
|
|
|
expect(result).toBe(15)
|
|
})
|
|
|
|
describe('builder callback for extraReducers', () => {
|
|
const increment = createAction<number, 'increment'>('increment')
|
|
|
|
test('can be used with actionCreators', () => {
|
|
const slice = createSlice({
|
|
name: 'counter',
|
|
initialState: 0,
|
|
reducers: {},
|
|
extraReducers: (builder) =>
|
|
builder.addCase(
|
|
increment,
|
|
(state, action) => state + action.payload,
|
|
),
|
|
})
|
|
expect(slice.reducer(0, increment(5))).toBe(5)
|
|
})
|
|
|
|
test('can be used with string action types', () => {
|
|
const slice = createSlice({
|
|
name: 'counter',
|
|
initialState: 0,
|
|
reducers: {},
|
|
extraReducers: (builder) =>
|
|
builder.addCase(
|
|
'increment',
|
|
(state, action: { type: 'increment'; payload: number }) =>
|
|
state + action.payload,
|
|
),
|
|
})
|
|
expect(slice.reducer(0, increment(5))).toBe(5)
|
|
})
|
|
|
|
test('prevents the same action type from being specified twice', () => {
|
|
expect(() => {
|
|
const slice = createSlice({
|
|
name: 'counter',
|
|
initialState: 0,
|
|
reducers: {},
|
|
extraReducers: (builder) =>
|
|
builder
|
|
.addCase('increment', (state) => state + 1)
|
|
.addCase('increment', (state) => state + 1),
|
|
})
|
|
slice.reducer(undefined, { type: 'unrelated' })
|
|
}).toThrowErrorMatchingInlineSnapshot(
|
|
`[Error: \`builder.addCase\` cannot be called with two reducers for the same action type 'increment']`,
|
|
)
|
|
})
|
|
|
|
test('can be used with addAsyncThunk and async thunks', () => {
|
|
const asyncThunk = createAsyncThunk('test', (n: number) => n)
|
|
const slice = createSlice({
|
|
name: 'counter',
|
|
initialState: {
|
|
loading: false,
|
|
errored: false,
|
|
value: 0,
|
|
},
|
|
reducers: {},
|
|
extraReducers: (builder) =>
|
|
builder.addAsyncThunk(asyncThunk, {
|
|
pending(state) {
|
|
state.loading = true
|
|
},
|
|
fulfilled(state, action) {
|
|
state.value = action.payload
|
|
},
|
|
rejected(state) {
|
|
state.errored = true
|
|
},
|
|
settled(state) {
|
|
state.loading = false
|
|
},
|
|
}),
|
|
})
|
|
expect(
|
|
slice.reducer(undefined, asyncThunk.pending('requestId', 5)),
|
|
).toEqual({
|
|
loading: true,
|
|
errored: false,
|
|
value: 0,
|
|
})
|
|
expect(
|
|
slice.reducer(undefined, asyncThunk.fulfilled(5, 'requestId', 5)),
|
|
).toEqual({
|
|
loading: false,
|
|
errored: false,
|
|
value: 5,
|
|
})
|
|
expect(
|
|
slice.reducer(
|
|
undefined,
|
|
asyncThunk.rejected(new Error(), 'requestId', 5),
|
|
),
|
|
).toEqual({
|
|
loading: false,
|
|
errored: true,
|
|
value: 0,
|
|
})
|
|
})
|
|
|
|
test('can be used with addMatcher and type guard functions', () => {
|
|
const slice = createSlice({
|
|
name: 'counter',
|
|
initialState: 0,
|
|
reducers: {},
|
|
extraReducers: (builder) =>
|
|
builder.addMatcher(
|
|
increment.match,
|
|
(state, action: { type: 'increment'; payload: number }) =>
|
|
state + action.payload,
|
|
),
|
|
})
|
|
expect(slice.reducer(0, increment(5))).toBe(5)
|
|
})
|
|
|
|
test('can be used with addDefaultCase', () => {
|
|
const slice = createSlice({
|
|
name: 'counter',
|
|
initialState: 0,
|
|
reducers: {},
|
|
extraReducers: (builder) =>
|
|
builder.addDefaultCase(
|
|
(state, action) =>
|
|
state + (action as PayloadAction<number>).payload,
|
|
),
|
|
})
|
|
expect(slice.reducer(0, increment(5))).toBe(5)
|
|
})
|
|
|
|
// for further tests, see the test of createReducer that goes way more into depth on this
|
|
})
|
|
})
|
|
|
|
describe('behavior with enhanced case reducers', () => {
|
|
it('should pass all arguments to the prepare function', () => {
|
|
const prepare = vi.fn((payload, somethingElse) => ({ payload }))
|
|
|
|
const testSlice = createSlice({
|
|
name: 'test',
|
|
initialState: 0,
|
|
reducers: {
|
|
testReducer: {
|
|
reducer: (s) => s,
|
|
prepare,
|
|
},
|
|
},
|
|
})
|
|
|
|
expect(testSlice.actions.testReducer('a', 1)).toEqual({
|
|
type: 'test/testReducer',
|
|
payload: 'a',
|
|
})
|
|
expect(prepare).toHaveBeenCalledWith('a', 1)
|
|
})
|
|
|
|
it('should call the reducer function', () => {
|
|
const reducer = vi.fn(() => 5)
|
|
|
|
const testSlice = createSlice({
|
|
name: 'test',
|
|
initialState: 0,
|
|
reducers: {
|
|
testReducer: {
|
|
reducer,
|
|
prepare: (payload: any) => ({ payload }),
|
|
},
|
|
},
|
|
})
|
|
|
|
testSlice.reducer(0, testSlice.actions.testReducer('testPayload'))
|
|
expect(reducer).toHaveBeenCalledWith(
|
|
0,
|
|
expect.objectContaining({ payload: 'testPayload' }),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('circularity', () => {
|
|
test('extraReducers can reference each other circularly', () => {
|
|
const first = createSlice({
|
|
name: 'first',
|
|
initialState: 'firstInitial',
|
|
reducers: {
|
|
something() {
|
|
return 'firstSomething'
|
|
},
|
|
},
|
|
extraReducers(builder) {
|
|
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
|
builder.addCase(second.actions.other, () => {
|
|
return 'firstOther'
|
|
})
|
|
},
|
|
})
|
|
const second = createSlice({
|
|
name: 'second',
|
|
initialState: 'secondInitial',
|
|
reducers: {
|
|
other() {
|
|
return 'secondOther'
|
|
},
|
|
},
|
|
extraReducers(builder) {
|
|
builder.addCase(first.actions.something, () => {
|
|
return 'secondSomething'
|
|
})
|
|
},
|
|
})
|
|
|
|
expect(first.reducer(undefined, { type: 'unrelated' })).toBe(
|
|
'firstInitial',
|
|
)
|
|
expect(first.reducer(undefined, first.actions.something())).toBe(
|
|
'firstSomething',
|
|
)
|
|
expect(first.reducer(undefined, second.actions.other())).toBe(
|
|
'firstOther',
|
|
)
|
|
|
|
expect(second.reducer(undefined, { type: 'unrelated' })).toBe(
|
|
'secondInitial',
|
|
)
|
|
expect(second.reducer(undefined, first.actions.something())).toBe(
|
|
'secondSomething',
|
|
)
|
|
expect(second.reducer(undefined, second.actions.other())).toBe(
|
|
'secondOther',
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('Deprecation warnings', () => {
|
|
beforeEach(() => {
|
|
vi.resetModules()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs()
|
|
})
|
|
|
|
// NOTE: This needs to be in front of the later `createReducer` call to check the one-time warning
|
|
it('Throws an error if the legacy object notation is used', async () => {
|
|
const { createSlice } = await import('../createSlice')
|
|
|
|
let dummySlice = (createSlice as CreateSlice)({
|
|
name: 'dummy',
|
|
initialState: [],
|
|
reducers: {},
|
|
extraReducers: {
|
|
// @ts-ignore
|
|
a: () => [],
|
|
},
|
|
})
|
|
let reducer: any
|
|
// Have to trigger the lazy creation
|
|
const wrapper = () => {
|
|
reducer = dummySlice.reducer
|
|
reducer(undefined, { type: 'dummy' })
|
|
}
|
|
|
|
expect(wrapper).toThrowError(
|
|
/The object notation for `createSlice.extraReducers` has been removed/,
|
|
)
|
|
|
|
dummySlice = (createSlice as CreateSlice)({
|
|
name: 'dummy',
|
|
initialState: [],
|
|
reducers: {},
|
|
extraReducers: {
|
|
// @ts-ignore
|
|
a: () => [],
|
|
},
|
|
})
|
|
expect(wrapper).toThrowError(
|
|
/The object notation for `createSlice.extraReducers` has been removed/,
|
|
)
|
|
})
|
|
|
|
// TODO Determine final production behavior here
|
|
it.todo('Crashes in production', () => {
|
|
vi.stubEnv('NODE_ENV', 'production')
|
|
|
|
const { createSlice } = require('../createSlice')
|
|
|
|
const dummySlice = (createSlice as CreateSlice)({
|
|
name: 'dummy',
|
|
initialState: [],
|
|
reducers: {},
|
|
// @ts-ignore
|
|
extraReducers: {},
|
|
})
|
|
const wrapper = () => {
|
|
const { reducer } = dummySlice
|
|
reducer(undefined, { type: 'dummy' })
|
|
}
|
|
|
|
expect(wrapper).toThrowError(
|
|
/The object notation for `createSlice.extraReducers` has been removed/,
|
|
)
|
|
|
|
vi.unstubAllEnvs()
|
|
})
|
|
})
|
|
describe('slice selectors', () => {
|
|
const slice = createSlice({
|
|
name: 'counter',
|
|
initialState: 42,
|
|
reducers: {},
|
|
selectors: {
|
|
selectSlice: (state) => state,
|
|
selectMultiple: Object.assign(
|
|
(state: number, multiplier: number) => state * multiplier,
|
|
{ test: 0 },
|
|
),
|
|
},
|
|
})
|
|
it('expects reducer under slice.reducerPath if no selectState callback passed', () => {
|
|
const testState = {
|
|
[slice.reducerPath]: slice.getInitialState(),
|
|
}
|
|
const { selectSlice, selectMultiple } = slice.selectors
|
|
expect(selectSlice(testState)).toBe(slice.getInitialState())
|
|
expect(selectMultiple(testState, 2)).toBe(slice.getInitialState() * 2)
|
|
})
|
|
it('allows passing a selector for a custom location', () => {
|
|
const customState = {
|
|
number: slice.getInitialState(),
|
|
}
|
|
const { selectSlice, selectMultiple } = slice.getSelectors(
|
|
(state: typeof customState) => state.number,
|
|
)
|
|
expect(selectSlice(customState)).toBe(slice.getInitialState())
|
|
expect(selectMultiple(customState, 2)).toBe(slice.getInitialState() * 2)
|
|
})
|
|
it('allows accessing properties on the selector', () => {
|
|
expect(slice.selectors.selectMultiple.unwrapped.test).toBe(0)
|
|
})
|
|
it('has selectSlice attached to slice, which can go without this', () => {
|
|
const slice = createSlice({
|
|
name: 'counter',
|
|
initialState: 42,
|
|
reducers: {},
|
|
})
|
|
const { selectSlice } = slice
|
|
expect(() => selectSlice({ counter: 42 })).not.toThrow()
|
|
expect(selectSlice({ counter: 42 })).toBe(42)
|
|
})
|
|
})
|
|
describe('slice injections', () => {
|
|
it('uses injectInto to inject slice into combined reducer', () => {
|
|
const slice = createSlice({
|
|
name: 'counter',
|
|
initialState: 42,
|
|
reducers: {
|
|
increment: (state) => ++state,
|
|
},
|
|
selectors: {
|
|
selectMultiple: (state, multiplier: number) => state * multiplier,
|
|
},
|
|
})
|
|
|
|
const { increment } = slice.actions
|
|
|
|
const combinedReducer = combineSlices({
|
|
static: slice.reducer,
|
|
}).withLazyLoadedSlices<WithSlice<typeof slice>>()
|
|
|
|
const uninjectedState = combinedReducer(undefined, increment())
|
|
|
|
expect(uninjectedState.counter).toBe(undefined)
|
|
|
|
const injectedSlice = slice.injectInto(combinedReducer)
|
|
|
|
// selector returns initial state if undefined in real state
|
|
expect(injectedSlice.selectSlice(uninjectedState)).toBe(
|
|
slice.getInitialState(),
|
|
)
|
|
expect(injectedSlice.selectors.selectMultiple({}, 1)).toBe(
|
|
slice.getInitialState(),
|
|
)
|
|
expect(injectedSlice.getSelectors().selectMultiple(undefined, 1)).toBe(
|
|
slice.getInitialState(),
|
|
)
|
|
|
|
const injectedState = combinedReducer(undefined, increment())
|
|
|
|
expect(injectedSlice.selectSlice(injectedState)).toBe(
|
|
slice.getInitialState() + 1,
|
|
)
|
|
expect(injectedSlice.selectors.selectMultiple(injectedState, 1)).toBe(
|
|
slice.getInitialState() + 1,
|
|
)
|
|
})
|
|
it('allows providing a custom name to inject under', () => {
|
|
const slice = createSlice({
|
|
name: 'counter',
|
|
reducerPath: 'injected',
|
|
initialState: 42,
|
|
reducers: {
|
|
increment: (state) => ++state,
|
|
},
|
|
selectors: {
|
|
selectMultiple: (state, multiplier: number) => state * multiplier,
|
|
},
|
|
})
|
|
|
|
const { increment } = slice.actions
|
|
|
|
const combinedReducer = combineSlices({
|
|
static: slice.reducer,
|
|
}).withLazyLoadedSlices<WithSlice<typeof slice> & { injected2: number }>()
|
|
|
|
const uninjectedState = combinedReducer(undefined, increment())
|
|
|
|
expect(uninjectedState.injected).toBe(undefined)
|
|
|
|
const injected = slice.injectInto(combinedReducer)
|
|
|
|
const injectedState = combinedReducer(undefined, increment())
|
|
|
|
expect(injected.selectSlice(injectedState)).toBe(
|
|
slice.getInitialState() + 1,
|
|
)
|
|
expect(injected.selectors.selectMultiple(injectedState, 2)).toBe(
|
|
(slice.getInitialState() + 1) * 2,
|
|
)
|
|
|
|
const injected2 = slice.injectInto(combinedReducer, {
|
|
reducerPath: 'injected2',
|
|
})
|
|
|
|
const injected2State = combinedReducer(undefined, increment())
|
|
|
|
expect(injected2.selectSlice(injected2State)).toBe(
|
|
slice.getInitialState() + 1,
|
|
)
|
|
expect(injected2.selectors.selectMultiple(injected2State, 2)).toBe(
|
|
(slice.getInitialState() + 1) * 2,
|
|
)
|
|
})
|
|
it('avoids incorrectly caching selectors', () => {
|
|
const slice = createSlice({
|
|
name: 'counter',
|
|
reducerPath: 'injected',
|
|
initialState: 42,
|
|
reducers: {
|
|
increment: (state) => ++state,
|
|
},
|
|
selectors: {
|
|
selectMultiple: (state, multiplier: number) => state * multiplier,
|
|
},
|
|
})
|
|
expect(slice.getSelectors()).toBe(slice.getSelectors())
|
|
const combinedReducer = combineSlices({
|
|
static: slice.reducer,
|
|
}).withLazyLoadedSlices<WithSlice<typeof slice>>()
|
|
|
|
const injected = slice.injectInto(combinedReducer)
|
|
|
|
expect(injected.getSelectors()).not.toBe(slice.getSelectors())
|
|
|
|
expect(injected.getSelectors().selectMultiple(undefined, 1)).toBe(42)
|
|
|
|
expect(() =>
|
|
// @ts-expect-error
|
|
slice.getSelectors().selectMultiple(undefined, 1),
|
|
).toThrowErrorMatchingInlineSnapshot(
|
|
`[Error: selectState returned undefined for an uninjected slice reducer]`,
|
|
)
|
|
|
|
const injected2 = slice.injectInto(combinedReducer, {
|
|
reducerPath: 'other',
|
|
})
|
|
|
|
// can use same cache for localised selectors
|
|
expect(injected.getSelectors()).toBe(injected2.getSelectors())
|
|
// these should be different
|
|
expect(injected.selectors).not.toBe(injected2.selectors)
|
|
})
|
|
it('caches initial states for selectors', () => {
|
|
const slice = createSlice({
|
|
name: 'counter',
|
|
initialState: () => ({ value: 0 }),
|
|
reducers: {},
|
|
selectors: {
|
|
selectObj: (state) => state,
|
|
},
|
|
})
|
|
// not cached
|
|
expect(slice.getInitialState()).not.toBe(slice.getInitialState())
|
|
expect(slice.reducer(undefined, { type: 'dummy' })).not.toBe(
|
|
slice.reducer(undefined, { type: 'dummy' }),
|
|
)
|
|
|
|
const combinedReducer = combineSlices({
|
|
static: slice.reducer,
|
|
}).withLazyLoadedSlices<WithSlice<typeof slice>>()
|
|
|
|
const injected = slice.injectInto(combinedReducer)
|
|
|
|
// still not cached
|
|
expect(injected.getInitialState()).not.toBe(injected.getInitialState())
|
|
expect(injected.reducer(undefined, { type: 'dummy' })).not.toBe(
|
|
injected.reducer(undefined, { type: 'dummy' }),
|
|
)
|
|
// cached
|
|
expect(injected.selectSlice({})).toBe(injected.selectSlice({}))
|
|
expect(injected.selectors.selectObj({})).toBe(
|
|
injected.selectors.selectObj({}),
|
|
)
|
|
})
|
|
})
|
|
describe('reducers definition with asyncThunks', () => {
|
|
it('is disabled by default', () => {
|
|
expect(() =>
|
|
createSlice({
|
|
name: 'test',
|
|
initialState: [] as any[],
|
|
reducers: (create) => ({ thunk: create.asyncThunk(() => {}) }),
|
|
}),
|
|
).toThrowErrorMatchingInlineSnapshot(
|
|
`[Error: Cannot use \`create.asyncThunk\` in the built-in \`createSlice\`. Use \`buildCreateSlice({ creators: { asyncThunk: asyncThunkCreator } })\` to create a customised version of \`createSlice\`.]`,
|
|
)
|
|
})
|
|
const createAppSlice = buildCreateSlice({
|
|
creators: { asyncThunk: asyncThunkCreator },
|
|
})
|
|
function pending(state: any[], action: any) {
|
|
state.push(['pendingReducer', action])
|
|
}
|
|
function fulfilled(state: any[], action: any) {
|
|
state.push(['fulfilledReducer', action])
|
|
}
|
|
function rejected(state: any[], action: any) {
|
|
state.push(['rejectedReducer', action])
|
|
}
|
|
function settled(state: any[], action: any) {
|
|
state.push(['settledReducer', action])
|
|
}
|
|
|
|
test('successful thunk', async () => {
|
|
const slice = createAppSlice({
|
|
name: 'test',
|
|
initialState: [] as any[],
|
|
reducers: (create) => ({
|
|
thunkReducers: create.asyncThunk(
|
|
function payloadCreator(arg: string, api) {
|
|
return Promise.resolve('resolved payload')
|
|
},
|
|
{ pending, fulfilled, rejected, settled },
|
|
),
|
|
}),
|
|
})
|
|
|
|
const store = configureStore({
|
|
reducer: slice.reducer,
|
|
})
|
|
await store.dispatch(slice.actions.thunkReducers('test'))
|
|
expect(store.getState()).toMatchObject([
|
|
[
|
|
'pendingReducer',
|
|
{
|
|
type: 'test/thunkReducers/pending',
|
|
payload: undefined,
|
|
},
|
|
],
|
|
[
|
|
'fulfilledReducer',
|
|
{
|
|
type: 'test/thunkReducers/fulfilled',
|
|
payload: 'resolved payload',
|
|
},
|
|
],
|
|
[
|
|
'settledReducer',
|
|
{
|
|
type: 'test/thunkReducers/fulfilled',
|
|
payload: 'resolved payload',
|
|
},
|
|
],
|
|
])
|
|
})
|
|
|
|
test('rejected thunk', async () => {
|
|
const slice = createAppSlice({
|
|
name: 'test',
|
|
initialState: [] as any[],
|
|
reducers: (create) => ({
|
|
thunkReducers: create.asyncThunk(
|
|
// payloadCreator isn't allowed to return never
|
|
function payloadCreator(arg: string, api): any {
|
|
throw new Error('')
|
|
},
|
|
{ pending, fulfilled, rejected, settled },
|
|
),
|
|
}),
|
|
})
|
|
|
|
const store = configureStore({
|
|
reducer: slice.reducer,
|
|
})
|
|
await store.dispatch(slice.actions.thunkReducers('test'))
|
|
expect(store.getState()).toMatchObject([
|
|
[
|
|
'pendingReducer',
|
|
{
|
|
type: 'test/thunkReducers/pending',
|
|
payload: undefined,
|
|
},
|
|
],
|
|
[
|
|
'rejectedReducer',
|
|
{
|
|
type: 'test/thunkReducers/rejected',
|
|
payload: undefined,
|
|
},
|
|
],
|
|
[
|
|
'settledReducer',
|
|
{
|
|
type: 'test/thunkReducers/rejected',
|
|
payload: undefined,
|
|
},
|
|
],
|
|
])
|
|
})
|
|
|
|
test('with options', async () => {
|
|
const slice = createAppSlice({
|
|
name: 'test',
|
|
initialState: [] as any[],
|
|
reducers: (create) => ({
|
|
thunkReducers: create.asyncThunk(
|
|
function payloadCreator(arg: string, api) {
|
|
return 'should not call this'
|
|
},
|
|
{
|
|
options: {
|
|
condition() {
|
|
return false
|
|
},
|
|
dispatchConditionRejection: true,
|
|
},
|
|
pending,
|
|
fulfilled,
|
|
rejected,
|
|
settled,
|
|
},
|
|
),
|
|
}),
|
|
})
|
|
|
|
const store = configureStore({
|
|
reducer: slice.reducer,
|
|
})
|
|
await store.dispatch(slice.actions.thunkReducers('test'))
|
|
expect(store.getState()).toMatchObject([
|
|
[
|
|
'rejectedReducer',
|
|
{
|
|
type: 'test/thunkReducers/rejected',
|
|
payload: undefined,
|
|
meta: { condition: true },
|
|
},
|
|
],
|
|
[
|
|
'settledReducer',
|
|
{
|
|
type: 'test/thunkReducers/rejected',
|
|
payload: undefined,
|
|
meta: { condition: true },
|
|
},
|
|
],
|
|
])
|
|
})
|
|
|
|
test('has caseReducers for the asyncThunk', async () => {
|
|
const slice = createAppSlice({
|
|
name: 'test',
|
|
initialState: [],
|
|
reducers: (create) => ({
|
|
thunkReducers: create.asyncThunk(
|
|
function payloadCreator(arg, api) {
|
|
return Promise.resolve('resolved payload')
|
|
},
|
|
{ pending, fulfilled, settled },
|
|
),
|
|
}),
|
|
})
|
|
|
|
expect(slice.caseReducers.thunkReducers.pending).toBe(pending)
|
|
expect(slice.caseReducers.thunkReducers.fulfilled).toBe(fulfilled)
|
|
expect(slice.caseReducers.thunkReducers.settled).toBe(settled)
|
|
// even though it is not defined above, this should at least be a no-op function to match the TypeScript typings
|
|
// and should be callable as a reducer even if it does nothing
|
|
expect(() =>
|
|
slice.caseReducers.thunkReducers.rejected(
|
|
[],
|
|
slice.actions.thunkReducers.rejected(
|
|
new Error('test'),
|
|
'fakeRequestId',
|
|
),
|
|
),
|
|
).not.toThrow()
|
|
})
|
|
|
|
test('can define reducer with prepare statement using create.preparedReducer', async () => {
|
|
const slice = createSlice({
|
|
name: 'test',
|
|
initialState: [] as any[],
|
|
reducers: (create) => ({
|
|
prepared: create.preparedReducer(
|
|
(p: string, m: number, e: { message: string }) => ({
|
|
payload: p,
|
|
meta: m,
|
|
error: e,
|
|
}),
|
|
(state, action) => {
|
|
state.push(action)
|
|
},
|
|
),
|
|
}),
|
|
})
|
|
|
|
expect(
|
|
slice.reducer(
|
|
[],
|
|
slice.actions.prepared('test', 1, { message: 'err' }),
|
|
),
|
|
).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
"error": {
|
|
"message": "err",
|
|
},
|
|
"meta": 1,
|
|
"payload": "test",
|
|
"type": "test/prepared",
|
|
},
|
|
]
|
|
`)
|
|
})
|
|
|
|
test('throws an error when invoked with a normal `prepare` object that has not gone through a `create.preparedReducer` call', async () => {
|
|
expect(() =>
|
|
createSlice({
|
|
name: 'test',
|
|
initialState: [] as any[],
|
|
reducers: (create) => ({
|
|
prepared: {
|
|
prepare: (p: string, m: number, e: { message: string }) => ({
|
|
payload: p,
|
|
meta: m,
|
|
error: e,
|
|
}),
|
|
reducer: (state, action) => {
|
|
state.push(action)
|
|
},
|
|
},
|
|
}),
|
|
}),
|
|
).toThrowErrorMatchingInlineSnapshot(
|
|
`[Error: Please use the \`create.preparedReducer\` notation for prepared action creators with the \`create\` notation.]`,
|
|
)
|
|
})
|
|
})
|
|
})
|