This commit is contained in:
135
node_modules/@reduxjs/toolkit/src/tests/EnhancerArray.typetest.ts
generated
vendored
Normal file
135
node_modules/@reduxjs/toolkit/src/tests/EnhancerArray.typetest.ts
generated
vendored
Normal file
@@ -0,0 +1,135 @@
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import type { StoreEnhancer } from 'redux'
|
||||
|
||||
declare const expectType: <T>(t: T) => T
|
||||
|
||||
declare const enhancer1: StoreEnhancer<
|
||||
{
|
||||
has1: true
|
||||
},
|
||||
{ stateHas1: true }
|
||||
>
|
||||
|
||||
declare const enhancer2: StoreEnhancer<
|
||||
{
|
||||
has2: true
|
||||
},
|
||||
{ stateHas2: true }
|
||||
>
|
||||
|
||||
{
|
||||
// prepend single element
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: () => 0,
|
||||
enhancers: (dE) => dE.prepend(enhancer1),
|
||||
})
|
||||
expectType<true>(store.has1)
|
||||
expectType<true>(store.getState().stateHas1)
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<true>(store.has2)
|
||||
// @ts-expect-error
|
||||
expectType<true>(store.getState().stateHas2)
|
||||
}
|
||||
|
||||
// prepend multiple (rest)
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: () => 0,
|
||||
enhancers: (dE) => dE.prepend(enhancer1, enhancer2),
|
||||
})
|
||||
expectType<true>(store.has1)
|
||||
expectType<true>(store.getState().stateHas1)
|
||||
expectType<true>(store.has2)
|
||||
expectType<true>(store.getState().stateHas2)
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<true>(store.has3)
|
||||
// @ts-expect-error
|
||||
expectType<true>(store.getState().stateHas3)
|
||||
}
|
||||
|
||||
// prepend multiple (array notation)
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: () => 0,
|
||||
enhancers: (dE) => dE.prepend([enhancer1, enhancer2] as const),
|
||||
})
|
||||
expectType<true>(store.has1)
|
||||
expectType<true>(store.getState().stateHas1)
|
||||
expectType<true>(store.has2)
|
||||
expectType<true>(store.getState().stateHas2)
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<true>(store.has3)
|
||||
// @ts-expect-error
|
||||
expectType<true>(store.getState().stateHas3)
|
||||
}
|
||||
|
||||
// concat single element
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: () => 0,
|
||||
enhancers: (dE) => dE.concat(enhancer1),
|
||||
})
|
||||
expectType<true>(store.has1)
|
||||
expectType<true>(store.getState().stateHas1)
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<true>(store.has2)
|
||||
// @ts-expect-error
|
||||
expectType<true>(store.getState().stateHas2)
|
||||
}
|
||||
|
||||
// prepend multiple (rest)
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: () => 0,
|
||||
enhancers: (dE) => dE.concat(enhancer1, enhancer2),
|
||||
})
|
||||
expectType<true>(store.has1)
|
||||
expectType<true>(store.getState().stateHas1)
|
||||
expectType<true>(store.has2)
|
||||
expectType<true>(store.getState().stateHas2)
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<true>(store.has3)
|
||||
// @ts-expect-error
|
||||
expectType<true>(store.getState().stateHas3)
|
||||
}
|
||||
|
||||
// concat multiple (array notation)
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: () => 0,
|
||||
enhancers: (dE) => dE.concat([enhancer1, enhancer2] as const),
|
||||
})
|
||||
expectType<true>(store.has1)
|
||||
expectType<true>(store.getState().stateHas1)
|
||||
expectType<true>(store.has2)
|
||||
expectType<true>(store.getState().stateHas2)
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<true>(store.has3)
|
||||
// @ts-expect-error
|
||||
expectType<true>(store.getState().stateHas3)
|
||||
}
|
||||
|
||||
// concat and prepend
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: () => 0,
|
||||
enhancers: (dE) => dE.concat(enhancer1).prepend(enhancer2),
|
||||
})
|
||||
expectType<true>(store.has1)
|
||||
expectType<true>(store.getState().stateHas1)
|
||||
expectType<true>(store.has2)
|
||||
expectType<true>(store.getState().stateHas2)
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<true>(store.has3)
|
||||
// @ts-expect-error
|
||||
expectType<true>(store.getState().stateHas3)
|
||||
}
|
||||
}
|
||||
118
node_modules/@reduxjs/toolkit/src/tests/MiddlewareArray.typetest.ts
generated
vendored
Normal file
118
node_modules/@reduxjs/toolkit/src/tests/MiddlewareArray.typetest.ts
generated
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
import { getDefaultMiddleware, configureStore } from '@reduxjs/toolkit'
|
||||
import type { Middleware } from 'redux'
|
||||
|
||||
declare const expectType: <T>(t: T) => T
|
||||
|
||||
declare const middleware1: Middleware<{
|
||||
(_: string): number
|
||||
}>
|
||||
|
||||
declare const middleware2: Middleware<{
|
||||
(_: number): string
|
||||
}>
|
||||
|
||||
type ThunkReturn = Promise<'thunk'>
|
||||
declare const thunkCreator: () => () => ThunkReturn
|
||||
|
||||
{
|
||||
// prepend single element
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: () => 0,
|
||||
middleware: (gDM) => gDM().prepend(middleware1),
|
||||
})
|
||||
expectType<number>(store.dispatch('foo'))
|
||||
expectType<ThunkReturn>(store.dispatch(thunkCreator()))
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<string>(store.dispatch('foo'))
|
||||
}
|
||||
|
||||
// prepend multiple (rest)
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: () => 0,
|
||||
middleware: (gDM) => gDM().prepend(middleware1, middleware2),
|
||||
})
|
||||
expectType<number>(store.dispatch('foo'))
|
||||
expectType<string>(store.dispatch(5))
|
||||
expectType<ThunkReturn>(store.dispatch(thunkCreator()))
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<string>(store.dispatch('foo'))
|
||||
}
|
||||
|
||||
// prepend multiple (array notation)
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: () => 0,
|
||||
middleware: (gDM) => gDM().prepend([middleware1, middleware2] as const),
|
||||
})
|
||||
|
||||
expectType<number>(store.dispatch('foo'))
|
||||
expectType<string>(store.dispatch(5))
|
||||
expectType<ThunkReturn>(store.dispatch(thunkCreator()))
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<string>(store.dispatch('foo'))
|
||||
}
|
||||
|
||||
// concat single element
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: () => 0,
|
||||
middleware: (gDM) => gDM().concat(middleware1),
|
||||
})
|
||||
|
||||
expectType<number>(store.dispatch('foo'))
|
||||
expectType<ThunkReturn>(store.dispatch(thunkCreator()))
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<string>(store.dispatch('foo'))
|
||||
}
|
||||
|
||||
// prepend multiple (rest)
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: () => 0,
|
||||
middleware: (gDM) => gDM().concat(middleware1, middleware2),
|
||||
})
|
||||
|
||||
expectType<number>(store.dispatch('foo'))
|
||||
expectType<string>(store.dispatch(5))
|
||||
expectType<ThunkReturn>(store.dispatch(thunkCreator()))
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<string>(store.dispatch('foo'))
|
||||
}
|
||||
|
||||
// concat multiple (array notation)
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: () => 0,
|
||||
middleware: (gDM) => gDM().concat([middleware1, middleware2] as const),
|
||||
})
|
||||
|
||||
expectType<number>(store.dispatch('foo'))
|
||||
expectType<string>(store.dispatch(5))
|
||||
expectType<ThunkReturn>(store.dispatch(thunkCreator()))
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<string>(store.dispatch('foo'))
|
||||
}
|
||||
|
||||
// concat and prepend
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: () => 0,
|
||||
middleware: (gDM) => gDM().concat(middleware1).prepend(middleware2),
|
||||
})
|
||||
|
||||
expectType<number>(store.dispatch('foo'))
|
||||
expectType<string>(store.dispatch(5))
|
||||
expectType<ThunkReturn>(store.dispatch(thunkCreator()))
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<string>(store.dispatch('foo'))
|
||||
}
|
||||
}
|
||||
8
node_modules/@reduxjs/toolkit/src/tests/__snapshots__/createAsyncThunk.test.ts.snap
generated
vendored
Normal file
8
node_modules/@reduxjs/toolkit/src/tests/__snapshots__/createAsyncThunk.test.ts.snap
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`createAsyncThunk with abortController behaviour with missing AbortController calling \`abort\` on an asyncThunk works with a FallbackAbortController if no global abortController is not available 1`] = `
|
||||
"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'."
|
||||
`;
|
||||
|
||||
exports[`non-serializable arguments are ignored by serializableStateInvariantMiddleware 1`] = `""`;
|
||||
54
node_modules/@reduxjs/toolkit/src/tests/__snapshots__/serializableStateInvariantMiddleware.test.ts.snap
generated
vendored
Normal file
54
node_modules/@reduxjs/toolkit/src/tests/__snapshots__/serializableStateInvariantMiddleware.test.ts.snap
generated
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`serializableStateInvariantMiddleware Should log an error when a non-serializable action is dispatched 1`] = `
|
||||
"A non-serializable value was detected in an action, in the path: \`type\`. Value: Symbol(SOME_CONSTANT)
|
||||
Take a look at the logic that dispatched this action: Object {
|
||||
\\"type\\": Symbol(SOME_CONSTANT),
|
||||
}
|
||||
(See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)
|
||||
(To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)"
|
||||
`;
|
||||
|
||||
exports[`serializableStateInvariantMiddleware Should log an error when a non-serializable value is in state 1`] = `
|
||||
"A non-serializable value was detected in the state, in the path: \`testSlice.a\`. Value: Map {}
|
||||
Take a look at the reducer(s) handling this action type: TEST_ACTION.
|
||||
(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
|
||||
`;
|
||||
|
||||
exports[`serializableStateInvariantMiddleware allows ignoring state entirely 1`] = `""`;
|
||||
|
||||
exports[`serializableStateInvariantMiddleware consumer tolerated structures Should log an error when a non-serializable value is nested in state 1`] = `
|
||||
"A non-serializable value was detected in the state, in the path: \`testSlice.a.entries\`. Value: [Function entries]
|
||||
Take a look at the reducer(s) handling this action type: TEST_ACTION.
|
||||
(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
|
||||
`;
|
||||
|
||||
exports[`serializableStateInvariantMiddleware consumer tolerated structures Should use consumer supplied isSerializable and getEntries options to tolerate certain structures 1`] = `
|
||||
"A non-serializable value was detected in the state, in the path: \`testSlice.a.third.bad-map-instance\`. Value: Map {}
|
||||
Take a look at the reducer(s) handling this action type: TEST_ACTION.
|
||||
(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
|
||||
`;
|
||||
|
||||
exports[`serializableStateInvariantMiddleware ignored action paths can specify (multiple) different values 1`] = `""`;
|
||||
|
||||
exports[`serializableStateInvariantMiddleware ignored action paths can specify regexp 1`] = `""`;
|
||||
|
||||
exports[`serializableStateInvariantMiddleware ignored action paths default value can be overridden 1`] = `
|
||||
"A non-serializable value was detected in an action, in the path: \`meta.arg\`. Value: Map {}
|
||||
Take a look at the logic that dispatched this action: Object {
|
||||
\\"meta\\": Object {
|
||||
\\"arg\\": Map {},
|
||||
},
|
||||
\\"type\\": \\"test\\",
|
||||
}
|
||||
(See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)
|
||||
(To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)"
|
||||
`;
|
||||
|
||||
exports[`serializableStateInvariantMiddleware ignored action paths default value: meta.arg 1`] = `""`;
|
||||
|
||||
exports[`serializableStateInvariantMiddleware should not check serializability for ignored slice names 1`] = `
|
||||
"A non-serializable value was detected in the state, in the path: \`testSlice.b.d\`. Value: Map {}
|
||||
Take a look at the reducer(s) handling this action type: TEST_ACTION.
|
||||
(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
|
||||
`;
|
||||
64
node_modules/@reduxjs/toolkit/src/tests/actionCreatorInvariantMiddleware.test.ts
generated
vendored
Normal file
64
node_modules/@reduxjs/toolkit/src/tests/actionCreatorInvariantMiddleware.test.ts
generated
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { ActionCreatorInvariantMiddlewareOptions } from '@internal/actionCreatorInvariantMiddleware'
|
||||
import { getMessage } from '@internal/actionCreatorInvariantMiddleware'
|
||||
import { createActionCreatorInvariantMiddleware } from '@internal/actionCreatorInvariantMiddleware'
|
||||
import type { Dispatch, MiddlewareAPI } from '@reduxjs/toolkit'
|
||||
import { createAction } from '@reduxjs/toolkit'
|
||||
|
||||
describe('createActionCreatorInvariantMiddleware', () => {
|
||||
const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockClear()
|
||||
})
|
||||
afterAll(() => {
|
||||
consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
const dummyAction = createAction('aSlice/anAction')
|
||||
|
||||
it('sends the action through the middleware chain', () => {
|
||||
const next: Dispatch = (action) => ({
|
||||
...action,
|
||||
returned: true,
|
||||
})
|
||||
const dispatch = createActionCreatorInvariantMiddleware()(
|
||||
{} as MiddlewareAPI
|
||||
)(next)
|
||||
|
||||
expect(dispatch(dummyAction())).toEqual({
|
||||
...dummyAction(),
|
||||
returned: true,
|
||||
})
|
||||
})
|
||||
|
||||
const makeActionTester = (
|
||||
options?: ActionCreatorInvariantMiddlewareOptions
|
||||
) =>
|
||||
createActionCreatorInvariantMiddleware(options)({} as MiddlewareAPI)(
|
||||
(action) => action
|
||||
)
|
||||
|
||||
it('logs a warning to console if an action creator is mistakenly dispatched', () => {
|
||||
const testAction = makeActionTester()
|
||||
|
||||
testAction(dummyAction())
|
||||
|
||||
expect(consoleSpy).not.toHaveBeenCalled()
|
||||
|
||||
testAction(dummyAction)
|
||||
|
||||
expect(consoleSpy).toHaveBeenLastCalledWith(getMessage(dummyAction.type))
|
||||
})
|
||||
|
||||
it('allows passing a custom predicate', () => {
|
||||
let predicateCalled = false
|
||||
const testAction = makeActionTester({
|
||||
isActionCreator(action): action is Function {
|
||||
predicateCalled = true
|
||||
return false
|
||||
},
|
||||
})
|
||||
testAction(dummyAction())
|
||||
expect(predicateCalled).toBe(true)
|
||||
})
|
||||
})
|
||||
126
node_modules/@reduxjs/toolkit/src/tests/autoBatchEnhancer.test.ts
generated
vendored
Normal file
126
node_modules/@reduxjs/toolkit/src/tests/autoBatchEnhancer.test.ts
generated
vendored
Normal file
@@ -0,0 +1,126 @@
|
||||
import { configureStore } from '../configureStore'
|
||||
import { createSlice } from '../createSlice'
|
||||
import type { AutoBatchOptions } from '../autoBatchEnhancer'
|
||||
import { autoBatchEnhancer, prepareAutoBatched } from '../autoBatchEnhancer'
|
||||
import { delay } from '../utils'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
interface CounterState {
|
||||
value: number
|
||||
}
|
||||
|
||||
const counterSlice = createSlice({
|
||||
name: 'counter',
|
||||
initialState: { value: 0 } as CounterState,
|
||||
reducers: {
|
||||
incrementBatched: {
|
||||
// Batched, low-priority
|
||||
reducer(state) {
|
||||
state.value += 1
|
||||
},
|
||||
prepare: prepareAutoBatched<void>(),
|
||||
},
|
||||
// Not batched, normal priority
|
||||
decrementUnbatched(state) {
|
||||
state.value -= 1
|
||||
},
|
||||
},
|
||||
})
|
||||
const { incrementBatched, decrementUnbatched } = counterSlice.actions
|
||||
|
||||
const makeStore = (autoBatchOptions?: AutoBatchOptions) => {
|
||||
return configureStore({
|
||||
reducer: counterSlice.reducer,
|
||||
enhancers: (existingEnhancers) => {
|
||||
return existingEnhancers.concat(autoBatchEnhancer(autoBatchOptions))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let store: ReturnType<typeof makeStore>
|
||||
|
||||
let subscriptionNotifications = 0
|
||||
|
||||
const cases: AutoBatchOptions[] = [
|
||||
{ type: 'tick' },
|
||||
{ type: 'raf' },
|
||||
{ type: 'timer', timeout: 0 },
|
||||
{ type: 'timer', timeout: 10 },
|
||||
{ type: 'timer', timeout: 20 },
|
||||
{
|
||||
type: 'callback',
|
||||
queueNotification: debounce((notify: () => void) => {
|
||||
notify()
|
||||
}, 5),
|
||||
},
|
||||
]
|
||||
|
||||
describe.each(cases)('autoBatchEnhancer: %j', (autoBatchOptions) => {
|
||||
beforeEach(() => {
|
||||
subscriptionNotifications = 0
|
||||
store = makeStore(autoBatchOptions)
|
||||
|
||||
store.subscribe(() => {
|
||||
subscriptionNotifications++
|
||||
})
|
||||
})
|
||||
test('Does not alter normal subscription notification behavior', async () => {
|
||||
store.dispatch(decrementUnbatched())
|
||||
expect(subscriptionNotifications).toBe(1)
|
||||
store.dispatch(decrementUnbatched())
|
||||
expect(subscriptionNotifications).toBe(2)
|
||||
store.dispatch(decrementUnbatched())
|
||||
expect(subscriptionNotifications).toBe(3)
|
||||
store.dispatch(decrementUnbatched())
|
||||
|
||||
await delay(25)
|
||||
|
||||
expect(subscriptionNotifications).toBe(4)
|
||||
})
|
||||
|
||||
test('Only notifies once if several batched actions are dispatched in a row', async () => {
|
||||
store.dispatch(incrementBatched())
|
||||
expect(subscriptionNotifications).toBe(0)
|
||||
store.dispatch(incrementBatched())
|
||||
expect(subscriptionNotifications).toBe(0)
|
||||
store.dispatch(incrementBatched())
|
||||
expect(subscriptionNotifications).toBe(0)
|
||||
store.dispatch(incrementBatched())
|
||||
|
||||
await delay(25)
|
||||
|
||||
expect(subscriptionNotifications).toBe(1)
|
||||
})
|
||||
|
||||
test('Notifies immediately if a non-batched action is dispatched', async () => {
|
||||
store.dispatch(incrementBatched())
|
||||
expect(subscriptionNotifications).toBe(0)
|
||||
store.dispatch(incrementBatched())
|
||||
expect(subscriptionNotifications).toBe(0)
|
||||
store.dispatch(decrementUnbatched())
|
||||
expect(subscriptionNotifications).toBe(1)
|
||||
store.dispatch(incrementBatched())
|
||||
|
||||
await delay(25)
|
||||
|
||||
expect(subscriptionNotifications).toBe(2)
|
||||
})
|
||||
|
||||
test('Does not notify at end of tick if last action was normal priority', async () => {
|
||||
store.dispatch(incrementBatched())
|
||||
expect(subscriptionNotifications).toBe(0)
|
||||
store.dispatch(incrementBatched())
|
||||
expect(subscriptionNotifications).toBe(0)
|
||||
store.dispatch(decrementUnbatched())
|
||||
expect(subscriptionNotifications).toBe(1)
|
||||
store.dispatch(incrementBatched())
|
||||
store.dispatch(decrementUnbatched())
|
||||
expect(subscriptionNotifications).toBe(2)
|
||||
store.dispatch(decrementUnbatched())
|
||||
expect(subscriptionNotifications).toBe(3)
|
||||
|
||||
await delay(25)
|
||||
|
||||
expect(subscriptionNotifications).toBe(3)
|
||||
})
|
||||
})
|
||||
133
node_modules/@reduxjs/toolkit/src/tests/combinedTest.test.ts
generated
vendored
Normal file
133
node_modules/@reduxjs/toolkit/src/tests/combinedTest.test.ts
generated
vendored
Normal file
@@ -0,0 +1,133 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||
import {
|
||||
createAsyncThunk,
|
||||
createAction,
|
||||
createSlice,
|
||||
configureStore,
|
||||
createEntityAdapter,
|
||||
} from '@reduxjs/toolkit'
|
||||
import type { EntityAdapter } from '@internal/entities/models'
|
||||
import type { BookModel } from '@internal/entities/tests/fixtures/book'
|
||||
|
||||
describe('Combined entity slice', () => {
|
||||
let adapter: EntityAdapter<BookModel>
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = createEntityAdapter({
|
||||
selectId: (book: BookModel) => book.id,
|
||||
sortComparer: (a, b) => a.title.localeCompare(b.title),
|
||||
})
|
||||
})
|
||||
|
||||
it('Entity and async features all works together', async () => {
|
||||
const upsertBook = createAction<BookModel>('otherBooks/upsert')
|
||||
|
||||
type BooksState = ReturnType<typeof adapter.getInitialState> & {
|
||||
loading: 'initial' | 'pending' | 'finished' | 'failed'
|
||||
lastRequestId: string | null
|
||||
}
|
||||
|
||||
const initialState: BooksState = adapter.getInitialState({
|
||||
loading: 'initial',
|
||||
lastRequestId: null,
|
||||
})
|
||||
|
||||
const fakeBooks: BookModel[] = [
|
||||
{ id: 'b', title: 'Second' },
|
||||
{ id: 'a', title: 'First' },
|
||||
]
|
||||
|
||||
const fetchBooksTAC = createAsyncThunk<
|
||||
BookModel[],
|
||||
void,
|
||||
{
|
||||
state: { books: BooksState }
|
||||
}
|
||||
>(
|
||||
'books/fetch',
|
||||
async (arg, { getState, dispatch, extra, requestId, signal }) => {
|
||||
const state = getState()
|
||||
return fakeBooks
|
||||
}
|
||||
)
|
||||
|
||||
const booksSlice = createSlice({
|
||||
name: 'books',
|
||||
initialState,
|
||||
reducers: {
|
||||
addOne: adapter.addOne,
|
||||
removeOne(state, action: PayloadAction<string>) {
|
||||
const sizeBefore = state.ids.length
|
||||
// Originally, having nested `produce` calls don't mutate `state` here as I would have expected.
|
||||
// (note that `state` here is actually an Immer Draft<S>, from `createReducer`)
|
||||
// One woarkound was to return the new plain result value instead
|
||||
// See https://github.com/immerjs/immer/issues/533
|
||||
// However, after tweaking `createStateOperator` to check if the argument is a draft,
|
||||
// we can just treat the operator as strictly mutating, without returning a result,
|
||||
// and the result should be correct.
|
||||
const result = adapter.removeOne(state, action)
|
||||
|
||||
const sizeAfter = state.ids.length
|
||||
if (sizeBefore > 0) {
|
||||
expect(sizeAfter).toBe(sizeBefore - 1)
|
||||
}
|
||||
|
||||
//Deliberately _don't_ return result
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(upsertBook, (state, action) => {
|
||||
return adapter.upsertOne(state, action)
|
||||
})
|
||||
builder.addCase(fetchBooksTAC.pending, (state, action) => {
|
||||
state.loading = 'pending'
|
||||
state.lastRequestId = action.meta.requestId
|
||||
})
|
||||
builder.addCase(fetchBooksTAC.fulfilled, (state, action) => {
|
||||
if (
|
||||
state.loading === 'pending' &&
|
||||
action.meta.requestId === state.lastRequestId
|
||||
) {
|
||||
adapter.setAll(state, action.payload)
|
||||
state.loading = 'finished'
|
||||
state.lastRequestId = null
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const { addOne, removeOne } = booksSlice.actions
|
||||
const { reducer } = booksSlice
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
books: reducer,
|
||||
},
|
||||
})
|
||||
|
||||
await store.dispatch(fetchBooksTAC())
|
||||
|
||||
const { books: booksAfterLoaded } = store.getState()
|
||||
// Sorted, so "First" goes first
|
||||
expect(booksAfterLoaded.ids).toEqual(['a', 'b'])
|
||||
expect(booksAfterLoaded.lastRequestId).toBe(null)
|
||||
expect(booksAfterLoaded.loading).toBe('finished')
|
||||
|
||||
store.dispatch(addOne({ id: 'd', title: 'Remove Me' }))
|
||||
store.dispatch(removeOne('d'))
|
||||
|
||||
store.dispatch(addOne({ id: 'c', title: 'Middle' }))
|
||||
|
||||
const { books: booksAfterAddOne } = store.getState()
|
||||
|
||||
// Sorted, so "Middle" goes in the middle
|
||||
expect(booksAfterAddOne.ids).toEqual(['a', 'c', 'b'])
|
||||
|
||||
store.dispatch(upsertBook({ id: 'c', title: 'Zeroth' }))
|
||||
|
||||
const { books: booksAfterUpsert } = store.getState()
|
||||
|
||||
// Sorted, so "Zeroth" goes last
|
||||
expect(booksAfterUpsert.ids).toEqual(['a', 'b', 'c'])
|
||||
})
|
||||
})
|
||||
241
node_modules/@reduxjs/toolkit/src/tests/configureStore.test.ts
generated
vendored
Normal file
241
node_modules/@reduxjs/toolkit/src/tests/configureStore.test.ts
generated
vendored
Normal file
@@ -0,0 +1,241 @@
|
||||
import type { StoreEnhancer, StoreEnhancerStoreCreator } from '@reduxjs/toolkit'
|
||||
import { configureStore } from '@reduxjs/toolkit'
|
||||
import * as RTK from '@reduxjs/toolkit'
|
||||
import * as redux from 'redux'
|
||||
import * as devtools from '@internal/devtoolsExtension'
|
||||
|
||||
describe('configureStore', () => {
|
||||
jest.spyOn(redux, 'applyMiddleware')
|
||||
jest.spyOn(redux, 'combineReducers')
|
||||
jest.spyOn(redux, 'compose')
|
||||
jest.spyOn(redux, 'createStore')
|
||||
jest.spyOn(devtools, 'composeWithDevTools') // @remap-prod-remove-line
|
||||
|
||||
const reducer: redux.Reducer = (state = {}, _action) => state
|
||||
|
||||
beforeEach(() => jest.clearAllMocks())
|
||||
|
||||
describe('given a function reducer', () => {
|
||||
it('calls createStore with the reducer', () => {
|
||||
configureStore({ reducer })
|
||||
expect(configureStore({ reducer })).toBeInstanceOf(Object)
|
||||
expect(redux.applyMiddleware).toHaveBeenCalled()
|
||||
expect(devtools.composeWithDevTools).toHaveBeenCalled() // @remap-prod-remove-line
|
||||
expect(redux.createStore).toHaveBeenCalledWith(
|
||||
reducer,
|
||||
undefined,
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('given an object of reducers', () => {
|
||||
it('calls createStore with the combined reducers', () => {
|
||||
const reducer = {
|
||||
reducer() {
|
||||
return true
|
||||
},
|
||||
}
|
||||
expect(configureStore({ reducer })).toBeInstanceOf(Object)
|
||||
expect(redux.combineReducers).toHaveBeenCalledWith(reducer)
|
||||
expect(redux.applyMiddleware).toHaveBeenCalled()
|
||||
expect(devtools.composeWithDevTools).toHaveBeenCalled() // @remap-prod-remove-line-line
|
||||
expect(redux.createStore).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
undefined,
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('given no reducer', () => {
|
||||
it('throws', () => {
|
||||
expect(configureStore).toThrow(
|
||||
'"reducer" is a required argument, and must be a function or an object of functions that can be passed to combineReducers'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('given no middleware', () => {
|
||||
it('calls createStore without any middleware', () => {
|
||||
expect(configureStore({ middleware: [], reducer })).toBeInstanceOf(Object)
|
||||
expect(redux.applyMiddleware).toHaveBeenCalledWith()
|
||||
expect(devtools.composeWithDevTools).toHaveBeenCalled() // @remap-prod-remove-line-line
|
||||
expect(redux.createStore).toHaveBeenCalledWith(
|
||||
reducer,
|
||||
undefined,
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('given undefined middleware', () => {
|
||||
it('calls createStore with default middleware', () => {
|
||||
expect(configureStore({ middleware: undefined, reducer })).toBeInstanceOf(
|
||||
Object
|
||||
)
|
||||
expect(redux.applyMiddleware).toHaveBeenCalledWith(
|
||||
expect.any(Function), // immutableCheck
|
||||
expect.any(Function), // thunk
|
||||
expect.any(Function), // serializableCheck
|
||||
expect.any(Function) // actionCreatorCheck
|
||||
)
|
||||
expect(devtools.composeWithDevTools).toHaveBeenCalled() // @remap-prod-remove-line-line
|
||||
expect(redux.createStore).toHaveBeenCalledWith(
|
||||
reducer,
|
||||
undefined,
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a middleware creation function that returns undefined', () => {
|
||||
it('throws an error', () => {
|
||||
const invalidBuilder = jest.fn((getDefaultMiddleware) => undefined as any)
|
||||
expect(() =>
|
||||
configureStore({ middleware: invalidBuilder, reducer })
|
||||
).toThrow(
|
||||
'when using a middleware builder function, an array of middleware must be returned'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a middleware creation function that returns an array with non-functions', () => {
|
||||
it('throws an error', () => {
|
||||
const invalidBuilder = jest.fn((getDefaultMiddleware) => [true] as any)
|
||||
expect(() =>
|
||||
configureStore({ middleware: invalidBuilder, reducer })
|
||||
).toThrow('each middleware provided to configureStore must be a function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('given custom middleware that contains non-functions', () => {
|
||||
it('throws an error', () => {
|
||||
expect(() =>
|
||||
configureStore({ middleware: [true] as any, reducer })
|
||||
).toThrow('each middleware provided to configureStore must be a function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('given custom middleware', () => {
|
||||
it('calls createStore with custom middleware and without default middleware', () => {
|
||||
const thank: redux.Middleware = (_store) => (next) => (action) =>
|
||||
next(action)
|
||||
expect(configureStore({ middleware: [thank], reducer })).toBeInstanceOf(
|
||||
Object
|
||||
)
|
||||
expect(redux.applyMiddleware).toHaveBeenCalledWith(thank)
|
||||
expect(devtools.composeWithDevTools).toHaveBeenCalled() // @remap-prod-remove-line-line
|
||||
expect(redux.createStore).toHaveBeenCalledWith(
|
||||
reducer,
|
||||
undefined,
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('middleware builder notation', () => {
|
||||
it('calls builder, passes getDefaultMiddleware and uses returned middlewares', () => {
|
||||
const thank = jest.fn(
|
||||
((_store) => (next) => (action) => 'foobar') as redux.Middleware
|
||||
)
|
||||
|
||||
const builder = jest.fn((getDefaultMiddleware) => {
|
||||
expect(getDefaultMiddleware).toEqual(expect.any(Function))
|
||||
expect(getDefaultMiddleware()).toEqual(expect.any(Array))
|
||||
|
||||
return [thank]
|
||||
})
|
||||
|
||||
const store = configureStore({ middleware: builder, reducer })
|
||||
|
||||
expect(builder).toHaveBeenCalled()
|
||||
|
||||
expect(store.dispatch({ type: 'test' })).toBe('foobar')
|
||||
})
|
||||
})
|
||||
|
||||
describe('with devTools disabled', () => {
|
||||
it('calls createStore without devTools enhancer', () => {
|
||||
expect(configureStore({ devTools: false, reducer })).toBeInstanceOf(
|
||||
Object
|
||||
)
|
||||
expect(redux.applyMiddleware).toHaveBeenCalled()
|
||||
expect(redux.compose).toHaveBeenCalled()
|
||||
expect(redux.createStore).toHaveBeenCalledWith(
|
||||
reducer,
|
||||
undefined,
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with devTools options', () => {
|
||||
it('calls createStore with devTools enhancer and option', () => {
|
||||
const options = {
|
||||
name: 'myApp',
|
||||
trace: true,
|
||||
}
|
||||
expect(configureStore({ devTools: options, reducer })).toBeInstanceOf(
|
||||
Object
|
||||
)
|
||||
expect(redux.applyMiddleware).toHaveBeenCalled()
|
||||
expect(devtools.composeWithDevTools).toHaveBeenCalledWith(options) // @remap-prod-remove-line
|
||||
expect(redux.createStore).toHaveBeenCalledWith(
|
||||
reducer,
|
||||
undefined,
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('given preloadedState', () => {
|
||||
it('calls createStore with preloadedState', () => {
|
||||
expect(configureStore({ reducer })).toBeInstanceOf(Object)
|
||||
expect(redux.applyMiddleware).toHaveBeenCalled()
|
||||
expect(devtools.composeWithDevTools).toHaveBeenCalled() // @remap-prod-remove-line
|
||||
expect(redux.createStore).toHaveBeenCalledWith(
|
||||
reducer,
|
||||
undefined,
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('given enhancers', () => {
|
||||
it('calls createStore with enhancers', () => {
|
||||
const enhancer: redux.StoreEnhancer = (next) => next
|
||||
expect(configureStore({ enhancers: [enhancer], reducer })).toBeInstanceOf(
|
||||
Object
|
||||
)
|
||||
expect(redux.applyMiddleware).toHaveBeenCalled()
|
||||
expect(devtools.composeWithDevTools).toHaveBeenCalled() // @remap-prod-remove-line
|
||||
expect(redux.createStore).toHaveBeenCalledWith(
|
||||
reducer,
|
||||
undefined,
|
||||
expect.any(Function)
|
||||
)
|
||||
})
|
||||
|
||||
it('accepts a callback for customizing enhancers', () => {
|
||||
let dummyEnhancerCalled = false
|
||||
|
||||
const dummyEnhancer: StoreEnhancer =
|
||||
(createStore: StoreEnhancerStoreCreator) =>
|
||||
(reducer, ...args: any[]) => {
|
||||
dummyEnhancerCalled = true
|
||||
|
||||
return createStore(reducer, ...args)
|
||||
}
|
||||
|
||||
const reducer = () => ({})
|
||||
|
||||
const store = configureStore({
|
||||
reducer,
|
||||
enhancers: (defaultEnhancers) => defaultEnhancers.concat(dummyEnhancer),
|
||||
})
|
||||
|
||||
expect(dummyEnhancerCalled).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
695
node_modules/@reduxjs/toolkit/src/tests/configureStore.typetest.ts
generated
vendored
Normal file
695
node_modules/@reduxjs/toolkit/src/tests/configureStore.typetest.ts
generated
vendored
Normal file
@@ -0,0 +1,695 @@
|
||||
/* eslint-disable no-lone-blocks */
|
||||
import type {
|
||||
Dispatch,
|
||||
AnyAction,
|
||||
Middleware,
|
||||
Reducer,
|
||||
Store,
|
||||
Action,
|
||||
StoreEnhancer,
|
||||
} from 'redux'
|
||||
import { applyMiddleware } from 'redux'
|
||||
import type { PayloadAction, ConfigureStoreOptions } from '@reduxjs/toolkit'
|
||||
import {
|
||||
configureStore,
|
||||
getDefaultMiddleware,
|
||||
createSlice,
|
||||
} from '@reduxjs/toolkit'
|
||||
import type { ThunkMiddleware, ThunkAction, ThunkDispatch } from 'redux-thunk'
|
||||
import thunk from 'redux-thunk'
|
||||
import { expectNotAny, expectType } from './helpers'
|
||||
|
||||
const _anyMiddleware: any = () => () => () => {}
|
||||
|
||||
/*
|
||||
* Test: configureStore() requires a valid reducer or reducer map.
|
||||
*/
|
||||
{
|
||||
configureStore({
|
||||
reducer: (state, action) => 0,
|
||||
})
|
||||
|
||||
configureStore({
|
||||
reducer: {
|
||||
counter1: () => 0,
|
||||
counter2: () => 1,
|
||||
},
|
||||
})
|
||||
|
||||
// @ts-expect-error
|
||||
configureStore({ reducer: 'not a reducer' })
|
||||
|
||||
// @ts-expect-error
|
||||
configureStore({ reducer: { a: 'not a reducer' } })
|
||||
|
||||
// @ts-expect-error
|
||||
configureStore({})
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: configureStore() infers the store state type.
|
||||
*/
|
||||
{
|
||||
const reducer: Reducer<number> = () => 0
|
||||
const store = configureStore({ reducer })
|
||||
const numberStore: Store<number, AnyAction> = store
|
||||
|
||||
// @ts-expect-error
|
||||
const stringStore: Store<string, AnyAction> = store
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: configureStore() infers the store action type.
|
||||
*/
|
||||
{
|
||||
const reducer: Reducer<number, PayloadAction<number>> = () => 0
|
||||
const store = configureStore({ reducer })
|
||||
const numberStore: Store<number, PayloadAction<number>> = store
|
||||
|
||||
// @ts-expect-error
|
||||
const stringStore: Store<number, PayloadAction<string>> = store
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: configureStore() accepts middleware array.
|
||||
*/
|
||||
{
|
||||
const middleware: Middleware = (store) => (next) => next
|
||||
|
||||
configureStore({
|
||||
reducer: () => 0,
|
||||
middleware: [middleware],
|
||||
})
|
||||
|
||||
configureStore({
|
||||
reducer: () => 0,
|
||||
// @ts-expect-error
|
||||
middleware: ['not middleware'],
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: configureStore() accepts devTools flag.
|
||||
*/
|
||||
{
|
||||
configureStore({
|
||||
reducer: () => 0,
|
||||
devTools: true,
|
||||
})
|
||||
|
||||
configureStore({
|
||||
reducer: () => 0,
|
||||
// @ts-expect-error
|
||||
devTools: 'true',
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: configureStore() accepts devTools EnhancerOptions.
|
||||
*/
|
||||
{
|
||||
configureStore({
|
||||
reducer: () => 0,
|
||||
devTools: { name: 'myApp' },
|
||||
})
|
||||
|
||||
configureStore({
|
||||
reducer: () => 0,
|
||||
// @ts-expect-error
|
||||
devTools: { appname: 'myApp' },
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: configureStore() accepts preloadedState.
|
||||
*/
|
||||
{
|
||||
configureStore({
|
||||
reducer: () => 0,
|
||||
preloadedState: 0,
|
||||
})
|
||||
|
||||
configureStore({
|
||||
reducer: () => 0,
|
||||
// @ts-expect-error
|
||||
preloadedState: 'non-matching state type',
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: configureStore() accepts store enhancer.
|
||||
*/
|
||||
{
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: () => 0,
|
||||
enhancers: [applyMiddleware(() => (next) => next)],
|
||||
})
|
||||
|
||||
expectType<Dispatch & ThunkDispatch<number, undefined, AnyAction>>(
|
||||
store.dispatch
|
||||
)
|
||||
}
|
||||
|
||||
configureStore({
|
||||
reducer: () => 0,
|
||||
// @ts-expect-error
|
||||
enhancers: ['not a store enhancer'],
|
||||
})
|
||||
|
||||
{
|
||||
type SomePropertyStoreEnhancer = StoreEnhancer<{ someProperty: string }>
|
||||
|
||||
const somePropertyStoreEnhancer: SomePropertyStoreEnhancer = (next) => {
|
||||
return (reducer, preloadedState) => {
|
||||
return {
|
||||
...next(reducer, preloadedState),
|
||||
someProperty: 'some value',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type AnotherPropertyStoreEnhancer = StoreEnhancer<{
|
||||
anotherProperty: number
|
||||
}>
|
||||
|
||||
const anotherPropertyStoreEnhancer: AnotherPropertyStoreEnhancer = (
|
||||
next
|
||||
) => {
|
||||
return (reducer, preloadedState) => {
|
||||
return {
|
||||
...next(reducer, preloadedState),
|
||||
anotherProperty: 123,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const store = configureStore({
|
||||
reducer: () => 0,
|
||||
enhancers: [somePropertyStoreEnhancer, anotherPropertyStoreEnhancer],
|
||||
})
|
||||
|
||||
expectType<Dispatch & ThunkDispatch<number, undefined, AnyAction>>(
|
||||
store.dispatch
|
||||
)
|
||||
expectType<string>(store.someProperty)
|
||||
expectType<number>(store.anotherProperty)
|
||||
|
||||
const storeWithCallback = configureStore({
|
||||
reducer: () => 0,
|
||||
enhancers: (defaultEnhancers) =>
|
||||
defaultEnhancers
|
||||
.prepend(anotherPropertyStoreEnhancer)
|
||||
.concat(somePropertyStoreEnhancer),
|
||||
})
|
||||
|
||||
expectType<Dispatch & ThunkDispatch<number, undefined, AnyAction>>(
|
||||
store.dispatch
|
||||
)
|
||||
expectType<string>(storeWithCallback.someProperty)
|
||||
expectType<number>(storeWithCallback.anotherProperty)
|
||||
}
|
||||
|
||||
{
|
||||
type StateExtendingEnhancer = StoreEnhancer<{}, { someProperty: string }>
|
||||
|
||||
const someStateExtendingEnhancer: StateExtendingEnhancer =
|
||||
(next) =>
|
||||
// @ts-expect-error how do you properly return an enhancer that extends state?
|
||||
(...args) => {
|
||||
const store = next(...args)
|
||||
const getState = () => ({
|
||||
...store.getState(),
|
||||
someProperty: 'some value',
|
||||
})
|
||||
return {
|
||||
...store,
|
||||
getState,
|
||||
}
|
||||
}
|
||||
|
||||
type AnotherStateExtendingEnhancer = StoreEnhancer<
|
||||
{},
|
||||
{ anotherProperty: number }
|
||||
>
|
||||
|
||||
const anotherStateExtendingEnhancer: AnotherStateExtendingEnhancer =
|
||||
(next) =>
|
||||
// @ts-expect-error any input on this would be great
|
||||
(...args) => {
|
||||
const store = next(...args)
|
||||
const getState = () => ({
|
||||
...store.getState(),
|
||||
anotherProperty: 123,
|
||||
})
|
||||
return {
|
||||
...store,
|
||||
getState,
|
||||
}
|
||||
}
|
||||
|
||||
const store = configureStore({
|
||||
reducer: () => ({ aProperty: 0 }),
|
||||
enhancers: [
|
||||
someStateExtendingEnhancer,
|
||||
anotherStateExtendingEnhancer,
|
||||
// this doesn't work without the as const
|
||||
] as const,
|
||||
})
|
||||
|
||||
const state = store.getState()
|
||||
|
||||
expectType<number>(state.aProperty)
|
||||
expectType<string>(state.someProperty)
|
||||
expectType<number>(state.anotherProperty)
|
||||
|
||||
const storeWithCallback = configureStore({
|
||||
reducer: () => ({ aProperty: 0 }),
|
||||
enhancers: (dE) =>
|
||||
dE.concat(someStateExtendingEnhancer, anotherStateExtendingEnhancer),
|
||||
})
|
||||
|
||||
const stateWithCallback = storeWithCallback.getState()
|
||||
|
||||
expectType<number>(stateWithCallback.aProperty)
|
||||
expectType<string>(stateWithCallback.someProperty)
|
||||
expectType<number>(stateWithCallback.anotherProperty)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: configureStore() state type inference works when specifying both a
|
||||
* reducer object and a partial preloaded state.
|
||||
*/
|
||||
{
|
||||
let counterReducer1: Reducer<number> = () => 0
|
||||
let counterReducer2: Reducer<number> = () => 0
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
counter1: counterReducer1,
|
||||
counter2: counterReducer2,
|
||||
},
|
||||
preloadedState: {
|
||||
counter1: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const counter1: number = store.getState().counter1
|
||||
const counter2: number = store.getState().counter2
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: Dispatch typings
|
||||
*/
|
||||
{
|
||||
type StateA = number
|
||||
const reducerA = () => 0
|
||||
function thunkA() {
|
||||
return (() => {}) as any as ThunkAction<Promise<'A'>, StateA, any, any>
|
||||
}
|
||||
|
||||
type StateB = string
|
||||
function thunkB() {
|
||||
return (dispatch: Dispatch, getState: () => StateB) => {}
|
||||
}
|
||||
/**
|
||||
* Test: by default, dispatching Thunks is possible
|
||||
*/
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: reducerA,
|
||||
})
|
||||
|
||||
store.dispatch(thunkA())
|
||||
// @ts-expect-error
|
||||
store.dispatch(thunkB())
|
||||
|
||||
const res = store.dispatch((dispatch, getState) => {
|
||||
return 42
|
||||
})
|
||||
|
||||
const action = store.dispatch({ type: 'foo' })
|
||||
}
|
||||
/**
|
||||
* Test: return type of thunks and actions is inferred correctly
|
||||
*/
|
||||
{
|
||||
const slice = createSlice({
|
||||
name: 'counter',
|
||||
initialState: {
|
||||
value: 0,
|
||||
},
|
||||
reducers: {
|
||||
incrementByAmount: (state, action: PayloadAction<number>) => {
|
||||
state.value += action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
counter: slice.reducer,
|
||||
},
|
||||
})
|
||||
|
||||
const action = slice.actions.incrementByAmount(2)
|
||||
|
||||
const dispatchResult = store.dispatch(action)
|
||||
expectType<{ type: string; payload: number }>(dispatchResult)
|
||||
|
||||
const promiseResult = store.dispatch(async (dispatch) => {
|
||||
return 42
|
||||
})
|
||||
|
||||
expectType<Promise<number>>(promiseResult)
|
||||
|
||||
const store2 = configureStore({
|
||||
reducer: {
|
||||
counter: slice.reducer,
|
||||
},
|
||||
middleware: (gDM) =>
|
||||
gDM({
|
||||
thunk: {
|
||||
extraArgument: 42,
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const dispatchResult2 = store2.dispatch(action)
|
||||
expectType<{ type: string; payload: number }>(dispatchResult2)
|
||||
}
|
||||
/**
|
||||
* Test: removing the Thunk Middleware
|
||||
*/
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: reducerA,
|
||||
middleware: [],
|
||||
})
|
||||
// @ts-expect-error
|
||||
store.dispatch(thunkA())
|
||||
// @ts-expect-error
|
||||
store.dispatch(thunkB())
|
||||
}
|
||||
/**
|
||||
* Test: adding the thunk middleware by hand
|
||||
*/
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: reducerA,
|
||||
middleware: [thunk] as [ThunkMiddleware<StateA>],
|
||||
})
|
||||
store.dispatch(thunkA())
|
||||
// @ts-expect-error
|
||||
store.dispatch(thunkB())
|
||||
}
|
||||
/**
|
||||
* Test: using getDefaultMiddleware
|
||||
*/
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: reducerA,
|
||||
middleware: getDefaultMiddleware<StateA>(),
|
||||
})
|
||||
|
||||
store.dispatch(thunkA())
|
||||
// @ts-expect-error
|
||||
store.dispatch(thunkB())
|
||||
}
|
||||
/**
|
||||
* Test: custom middleware
|
||||
*/
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: reducerA,
|
||||
middleware: [] as any as [Middleware<(a: StateA) => boolean, StateA>],
|
||||
})
|
||||
const result: boolean = store.dispatch(5)
|
||||
// @ts-expect-error
|
||||
const result2: string = store.dispatch(5)
|
||||
}
|
||||
/**
|
||||
* Test: read-only middleware tuple
|
||||
*/
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: reducerA,
|
||||
middleware: [] as any as readonly [
|
||||
Middleware<(a: StateA) => boolean, StateA>
|
||||
],
|
||||
})
|
||||
const result: boolean = store.dispatch(5)
|
||||
// @ts-expect-error
|
||||
const result2: string = store.dispatch(5)
|
||||
}
|
||||
/**
|
||||
* Test: multiple custom middleware
|
||||
*/
|
||||
{
|
||||
const middleware = [] as any as [
|
||||
Middleware<(a: 'a') => 'A', StateA>,
|
||||
Middleware<(b: 'b') => 'B', StateA>,
|
||||
ThunkMiddleware<StateA>
|
||||
]
|
||||
const store = configureStore({
|
||||
reducer: reducerA,
|
||||
middleware,
|
||||
})
|
||||
|
||||
const result: 'A' = store.dispatch('a')
|
||||
const result2: 'B' = store.dispatch('b')
|
||||
const result3: Promise<'A'> = store.dispatch(thunkA())
|
||||
}
|
||||
/**
|
||||
* Accepts thunk with `unknown`, `undefined` or `null` ThunkAction extraArgument per default
|
||||
*/
|
||||
{
|
||||
const store = configureStore({ reducer: {} })
|
||||
// undefined is the default value for the ThunkMiddleware extraArgument
|
||||
store.dispatch(function () {} as ThunkAction<
|
||||
void,
|
||||
{},
|
||||
undefined,
|
||||
AnyAction
|
||||
>)
|
||||
// `null` for the `extra` generic was previously documented in the RTK "Advanced Tutorial", but
|
||||
// is a bad pattern and users should use `unknown` instead
|
||||
// @ts-expect-error
|
||||
store.dispatch(function () {} as ThunkAction<void, {}, null, AnyAction>)
|
||||
// unknown is the best way to type a ThunkAction if you do not care
|
||||
// about the value of the extraArgument, as it will always work with every
|
||||
// ThunkMiddleware, no matter the actual extraArgument type
|
||||
store.dispatch(function () {} as ThunkAction<void, {}, unknown, AnyAction>)
|
||||
// @ts-expect-error
|
||||
store.dispatch(function () {} as ThunkAction<void, {}, boolean, AnyAction>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: custom middleware and getDefaultMiddleware
|
||||
*/
|
||||
{
|
||||
const middleware = getDefaultMiddleware<StateA>().prepend(
|
||||
(() => {}) as any as Middleware<(a: 'a') => 'A', StateA>
|
||||
)
|
||||
const store = configureStore({
|
||||
reducer: reducerA,
|
||||
middleware,
|
||||
})
|
||||
|
||||
const result1: 'A' = store.dispatch('a')
|
||||
const result2: Promise<'A'> = store.dispatch(thunkA())
|
||||
// @ts-expect-error
|
||||
store.dispatch(thunkB())
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: custom middleware and getDefaultMiddleware, using prepend
|
||||
*/
|
||||
{
|
||||
const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> = _anyMiddleware
|
||||
const concatenated = getDefaultMiddleware<StateA>().prepend(otherMiddleware)
|
||||
|
||||
expectType<
|
||||
ReadonlyArray<typeof otherMiddleware | ThunkMiddleware | Middleware<{}>>
|
||||
>(concatenated)
|
||||
|
||||
const store = configureStore({
|
||||
reducer: reducerA,
|
||||
middleware: concatenated,
|
||||
})
|
||||
const result1: 'A' = store.dispatch('a')
|
||||
const result2: Promise<'A'> = store.dispatch(thunkA())
|
||||
// @ts-expect-error
|
||||
store.dispatch(thunkB())
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: custom middleware and getDefaultMiddleware, using concat
|
||||
*/
|
||||
{
|
||||
const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> = _anyMiddleware
|
||||
const concatenated = getDefaultMiddleware<StateA>().concat(otherMiddleware)
|
||||
|
||||
expectType<
|
||||
ReadonlyArray<typeof otherMiddleware | ThunkMiddleware | Middleware<{}>>
|
||||
>(concatenated)
|
||||
|
||||
const store = configureStore({
|
||||
reducer: reducerA,
|
||||
middleware: concatenated,
|
||||
})
|
||||
const result1: 'A' = store.dispatch('a')
|
||||
const result2: Promise<'A'> = store.dispatch(thunkA())
|
||||
// @ts-expect-error
|
||||
store.dispatch(thunkB())
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: middlewareBuilder notation, getDefaultMiddleware (unconfigured)
|
||||
*/
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: reducerA,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().prepend((() => {}) as any as Middleware<
|
||||
(a: 'a') => 'A',
|
||||
StateA
|
||||
>),
|
||||
})
|
||||
const result1: 'A' = store.dispatch('a')
|
||||
const result2: Promise<'A'> = store.dispatch(thunkA())
|
||||
// @ts-expect-error
|
||||
store.dispatch(thunkB())
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: middlewareBuilder notation, getDefaultMiddleware, concat & prepend
|
||||
*/
|
||||
{
|
||||
const otherMiddleware: Middleware<(a: 'a') => 'A', StateA> = _anyMiddleware
|
||||
const otherMiddleware2: Middleware<(a: 'b') => 'B', StateA> = _anyMiddleware
|
||||
const store = configureStore({
|
||||
reducer: reducerA,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware()
|
||||
.concat(otherMiddleware)
|
||||
.prepend(otherMiddleware2),
|
||||
})
|
||||
const result1: 'A' = store.dispatch('a')
|
||||
const result2: Promise<'A'> = store.dispatch(thunkA())
|
||||
const result3: 'B' = store.dispatch('b')
|
||||
// @ts-expect-error
|
||||
store.dispatch(thunkB())
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: middlewareBuilder notation, getDefaultMiddleware (thunk: false)
|
||||
*/
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: reducerA,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware({ thunk: false }).prepend(
|
||||
(() => {}) as any as Middleware<(a: 'a') => 'A', StateA>
|
||||
),
|
||||
})
|
||||
const result1: 'A' = store.dispatch('a')
|
||||
// @ts-expect-error
|
||||
store.dispatch(thunkA())
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: badly typed middleware won't make `dispatch` `any`
|
||||
*/
|
||||
{
|
||||
const store = configureStore({
|
||||
reducer: reducerA,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(_anyMiddleware as Middleware<any>),
|
||||
})
|
||||
|
||||
expectNotAny(store.dispatch)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: decorated `configureStore` won't make `dispatch` `never`
|
||||
*/
|
||||
{
|
||||
const someSlice = createSlice({
|
||||
name: 'something',
|
||||
initialState: null as any,
|
||||
reducers: {
|
||||
set(state) {
|
||||
return state
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
function configureMyStore<S>(
|
||||
options: Omit<ConfigureStoreOptions<S>, 'reducer'>
|
||||
) {
|
||||
return configureStore({
|
||||
...options,
|
||||
reducer: someSlice.reducer,
|
||||
})
|
||||
}
|
||||
|
||||
const store = configureMyStore({})
|
||||
|
||||
expectType<Function>(store.dispatch)
|
||||
}
|
||||
|
||||
{
|
||||
interface CounterState {
|
||||
value: number
|
||||
}
|
||||
|
||||
const counterSlice = createSlice({
|
||||
name: 'counter',
|
||||
initialState: { value: 0 } as CounterState,
|
||||
reducers: {
|
||||
increment(state) {
|
||||
state.value += 1
|
||||
},
|
||||
decrement(state) {
|
||||
state.value -= 1
|
||||
},
|
||||
// Use the PayloadAction type to declare the contents of `action.payload`
|
||||
incrementByAmount: (state, action: PayloadAction<number>) => {
|
||||
state.value += action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
type Unsubscribe = () => void
|
||||
|
||||
// A fake middleware that tells TS that an unsubscribe callback is being returned for a given action
|
||||
// This is the same signature that the "listener" middleware uses
|
||||
const dummyMiddleware: Middleware<
|
||||
{
|
||||
(action: Action<'actionListenerMiddleware/add'>): Unsubscribe
|
||||
},
|
||||
CounterState
|
||||
> = (storeApi) => (next) => (action) => {}
|
||||
|
||||
const store = configureStore({
|
||||
reducer: counterSlice.reducer,
|
||||
middleware: (gDM) => gDM().prepend(dummyMiddleware),
|
||||
})
|
||||
|
||||
// Order matters here! We need the listener type to come first, otherwise
|
||||
// the thunk middleware type kicks in and TS thinks a plain action is being returned
|
||||
expectType<
|
||||
((action: Action<'actionListenerMiddleware/add'>) => Unsubscribe) &
|
||||
ThunkDispatch<CounterState, undefined, AnyAction> &
|
||||
Dispatch<AnyAction>
|
||||
>(store.dispatch)
|
||||
|
||||
const unsubscribe = store.dispatch({
|
||||
type: 'actionListenerMiddleware/add',
|
||||
} as const)
|
||||
|
||||
expectType<Unsubscribe>(unsubscribe)
|
||||
}
|
||||
}
|
||||
151
node_modules/@reduxjs/toolkit/src/tests/createAction.test.ts
generated
vendored
Normal file
151
node_modules/@reduxjs/toolkit/src/tests/createAction.test.ts
generated
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
import { createAction, getType, isAction } from '@reduxjs/toolkit'
|
||||
|
||||
describe('createAction', () => {
|
||||
it('should create an action', () => {
|
||||
const actionCreator = createAction<string>('A_TYPE')
|
||||
expect(actionCreator('something')).toEqual({
|
||||
type: 'A_TYPE',
|
||||
payload: 'something',
|
||||
})
|
||||
})
|
||||
|
||||
describe('when stringifying action', () => {
|
||||
it('should return the action type', () => {
|
||||
const actionCreator = createAction('A_TYPE')
|
||||
expect(`${actionCreator}`).toEqual('A_TYPE')
|
||||
})
|
||||
})
|
||||
|
||||
describe('when passing a prepareAction method only returning a payload', () => {
|
||||
it('should use the payload returned from the prepareAction method', () => {
|
||||
const actionCreator = createAction('A_TYPE', (a: number) => ({
|
||||
payload: a * 2,
|
||||
}))
|
||||
expect(actionCreator(5).payload).toBe(10)
|
||||
})
|
||||
it('should not have a meta attribute on the resulting Action', () => {
|
||||
const actionCreator = createAction('A_TYPE', (a: number) => ({
|
||||
payload: a * 2,
|
||||
}))
|
||||
expect('meta' in actionCreator(5)).toBeFalsy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when passing a prepareAction method returning a payload and meta', () => {
|
||||
it('should use the payload returned from the prepareAction method', () => {
|
||||
const actionCreator = createAction('A_TYPE', (a: number) => ({
|
||||
payload: a * 2,
|
||||
meta: a / 2,
|
||||
}))
|
||||
expect(actionCreator(5).payload).toBe(10)
|
||||
})
|
||||
it('should use the meta returned from the prepareAction method', () => {
|
||||
const actionCreator = createAction('A_TYPE', (a: number) => ({
|
||||
payload: a * 2,
|
||||
meta: a / 2,
|
||||
}))
|
||||
expect(actionCreator(10).meta).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when passing a prepareAction method returning a payload and error', () => {
|
||||
it('should use the payload returned from the prepareAction method', () => {
|
||||
const actionCreator = createAction('A_TYPE', (a: number) => ({
|
||||
payload: a * 2,
|
||||
error: true,
|
||||
}))
|
||||
expect(actionCreator(5).payload).toBe(10)
|
||||
})
|
||||
it('should use the error returned from the prepareAction method', () => {
|
||||
const actionCreator = createAction('A_TYPE', (a: number) => ({
|
||||
payload: a * 2,
|
||||
error: true,
|
||||
}))
|
||||
expect(actionCreator(10).error).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when passing a prepareAction method returning a payload, meta and error', () => {
|
||||
it('should use the payload returned from the prepareAction method', () => {
|
||||
const actionCreator = createAction('A_TYPE', (a: number) => ({
|
||||
payload: a * 2,
|
||||
meta: a / 2,
|
||||
error: true,
|
||||
}))
|
||||
expect(actionCreator(5).payload).toBe(10)
|
||||
})
|
||||
it('should use the error returned from the prepareAction method', () => {
|
||||
const actionCreator = createAction('A_TYPE', (a: number) => ({
|
||||
payload: a * 2,
|
||||
meta: a / 2,
|
||||
error: true,
|
||||
}))
|
||||
expect(actionCreator(10).error).toBe(true)
|
||||
})
|
||||
it('should use the meta returned from the prepareAction method', () => {
|
||||
const actionCreator = createAction('A_TYPE', (a: number) => ({
|
||||
payload: a * 2,
|
||||
meta: a / 2,
|
||||
error: true,
|
||||
}))
|
||||
expect(actionCreator(10).meta).toBe(5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when passing a prepareAction that accepts multiple arguments', () => {
|
||||
it('should pass all arguments of the resulting actionCreator to prepareAction', () => {
|
||||
const actionCreator = createAction(
|
||||
'A_TYPE',
|
||||
(a: string, b: string, c: string) => ({
|
||||
payload: a + b + c,
|
||||
})
|
||||
)
|
||||
expect(actionCreator('1', '2', '3').payload).toBe('123')
|
||||
})
|
||||
})
|
||||
|
||||
describe('actionCreator.match', () => {
|
||||
test('should return true for actions generated by own actionCreator', () => {
|
||||
const actionCreator = createAction('test')
|
||||
expect(actionCreator.match(actionCreator())).toBe(true)
|
||||
})
|
||||
|
||||
test('should return true for matching actions', () => {
|
||||
const actionCreator = createAction('test')
|
||||
expect(actionCreator.match({ type: 'test' })).toBe(true)
|
||||
})
|
||||
|
||||
test('should return false for other actions', () => {
|
||||
const actionCreator = createAction('test')
|
||||
expect(actionCreator.match({ type: 'test-abc' })).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('isAction', () => {
|
||||
it('should only return true for plain objects with a type property', () => {
|
||||
const actionCreator = createAction('anAction')
|
||||
class Action {
|
||||
type = 'totally an action'
|
||||
}
|
||||
const testCases: [action: unknown, expected: boolean][] = [
|
||||
[{ type: 'an action' }, true],
|
||||
[{ type: 'more props', extra: true }, true],
|
||||
[actionCreator(), true],
|
||||
[actionCreator, false],
|
||||
[Promise.resolve({ type: 'an action' }), false],
|
||||
[new Action(), false],
|
||||
['a string', false],
|
||||
]
|
||||
for (const [action, expected] of testCases) {
|
||||
expect(isAction(action)).toBe(expected)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('getType', () => {
|
||||
it('should return the action type', () => {
|
||||
const actionCreator = createAction('A_TYPE')
|
||||
expect(getType(actionCreator)).toEqual('A_TYPE')
|
||||
})
|
||||
})
|
||||
359
node_modules/@reduxjs/toolkit/src/tests/createAction.typetest.tsx
generated
vendored
Normal file
359
node_modules/@reduxjs/toolkit/src/tests/createAction.typetest.tsx
generated
vendored
Normal file
@@ -0,0 +1,359 @@
|
||||
import React from 'react'
|
||||
import type { Action, AnyAction, ActionCreator } from 'redux'
|
||||
import type {
|
||||
PayloadAction,
|
||||
PayloadActionCreator,
|
||||
ActionCreatorWithoutPayload,
|
||||
ActionCreatorWithOptionalPayload,
|
||||
ActionCreatorWithPayload,
|
||||
ActionCreatorWithNonInferrablePayload,
|
||||
ActionCreatorWithPreparedPayload,
|
||||
} from '@reduxjs/toolkit'
|
||||
import { createAction } from '@reduxjs/toolkit'
|
||||
import type { IsAny } from '@internal/tsHelpers'
|
||||
import { expectType } from './helpers'
|
||||
|
||||
/* PayloadAction */
|
||||
|
||||
/*
|
||||
* Test: PayloadAction has type parameter for the payload.
|
||||
*/
|
||||
{
|
||||
const action: PayloadAction<number> = { type: '', payload: 5 }
|
||||
const numberPayload: number = action.payload
|
||||
|
||||
// @ts-expect-error
|
||||
const stringPayload: string = action.payload
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: PayloadAction type parameter is required.
|
||||
*/
|
||||
{
|
||||
// @ts-expect-error
|
||||
const action: PayloadAction = { type: '', payload: 5 }
|
||||
// @ts-expect-error
|
||||
const numberPayload: number = action.payload
|
||||
// @ts-expect-error
|
||||
const stringPayload: string = action.payload
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: PayloadAction has a string type tag.
|
||||
*/
|
||||
{
|
||||
const action: PayloadAction<number> = { type: '', payload: 5 }
|
||||
|
||||
// @ts-expect-error
|
||||
const action2: PayloadAction = { type: 1, payload: 5 }
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: PayloadAction is compatible with Action<string>
|
||||
*/
|
||||
{
|
||||
const action: PayloadAction<number> = { type: '', payload: 5 }
|
||||
const stringAction: Action<string> = action
|
||||
}
|
||||
|
||||
/* PayloadActionCreator */
|
||||
|
||||
/*
|
||||
* Test: PayloadActionCreator returns correctly typed PayloadAction depending
|
||||
* on whether a payload is passed.
|
||||
*/
|
||||
{
|
||||
const actionCreator = Object.assign(
|
||||
(payload?: number) => ({
|
||||
type: 'action',
|
||||
payload,
|
||||
}),
|
||||
{ type: 'action' }
|
||||
) as PayloadActionCreator<number | undefined>
|
||||
|
||||
expectType<PayloadAction<number | undefined>>(actionCreator(1))
|
||||
expectType<PayloadAction<number | undefined>>(actionCreator())
|
||||
expectType<PayloadAction<number | undefined>>(actionCreator(undefined))
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<PayloadAction<number>>(actionCreator())
|
||||
// @ts-expect-error
|
||||
expectType<PayloadAction<undefined>>(actionCreator(1))
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: PayloadActionCreator is compatible with ActionCreator.
|
||||
*/
|
||||
{
|
||||
const payloadActionCreator = Object.assign(
|
||||
(payload?: number) => ({
|
||||
type: 'action',
|
||||
payload,
|
||||
}),
|
||||
{ type: 'action' }
|
||||
) as PayloadActionCreator
|
||||
const actionCreator: ActionCreator<AnyAction> = payloadActionCreator
|
||||
|
||||
const payloadActionCreator2 = Object.assign(
|
||||
(payload?: number) => ({
|
||||
type: 'action',
|
||||
payload: payload || 1,
|
||||
}),
|
||||
{ type: 'action' }
|
||||
) as PayloadActionCreator<number>
|
||||
|
||||
const actionCreator2: ActionCreator<PayloadAction<number>> =
|
||||
payloadActionCreator2
|
||||
}
|
||||
|
||||
/* createAction() */
|
||||
|
||||
/*
|
||||
* Test: createAction() has type parameter for the action payload.
|
||||
*/
|
||||
{
|
||||
const increment = createAction<number, 'increment'>('increment')
|
||||
const n: number = increment(1).payload
|
||||
|
||||
// @ts-expect-error
|
||||
increment('').payload
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: createAction() type parameter is required, not inferred (defaults to `void`).
|
||||
*/
|
||||
{
|
||||
const increment = createAction('increment')
|
||||
// @ts-expect-error
|
||||
const n: number = increment(1).payload
|
||||
}
|
||||
/*
|
||||
* Test: createAction().type is a string literal.
|
||||
*/
|
||||
{
|
||||
const increment = createAction<number, 'increment'>('increment')
|
||||
const n: string = increment(1).type
|
||||
const s: 'increment' = increment(1).type
|
||||
|
||||
// @ts-expect-error
|
||||
const r: 'other' = increment(1).type
|
||||
// @ts-expect-error
|
||||
const q: number = increment(1).type
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: type still present when using prepareAction
|
||||
*/
|
||||
{
|
||||
const strLenAction = createAction('strLen', (payload: string) => ({
|
||||
payload: payload.length,
|
||||
}))
|
||||
|
||||
expectType<string>(strLenAction('test').type)
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: changing payload type with prepareAction
|
||||
*/
|
||||
{
|
||||
const strLenAction = createAction('strLen', (payload: string) => ({
|
||||
payload: payload.length,
|
||||
}))
|
||||
expectType<number>(strLenAction('test').payload)
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<string>(strLenAction('test').payload)
|
||||
// @ts-expect-error
|
||||
const error: any = strLenAction('test').error
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: adding metadata with prepareAction
|
||||
*/
|
||||
{
|
||||
const strLenMetaAction = createAction('strLenMeta', (payload: string) => ({
|
||||
payload,
|
||||
meta: payload.length,
|
||||
}))
|
||||
|
||||
expectType<number>(strLenMetaAction('test').meta)
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<string>(strLenMetaAction('test').meta)
|
||||
// @ts-expect-error
|
||||
const error: any = strLenMetaAction('test').error
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: adding boolean error with prepareAction
|
||||
*/
|
||||
{
|
||||
const boolErrorAction = createAction('boolError', (payload: string) => ({
|
||||
payload,
|
||||
error: true,
|
||||
}))
|
||||
|
||||
expectType<boolean>(boolErrorAction('test').error)
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<string>(boolErrorAction('test').error)
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: adding string error with prepareAction
|
||||
*/
|
||||
{
|
||||
const strErrorAction = createAction('strError', (payload: string) => ({
|
||||
payload,
|
||||
error: 'this is an error',
|
||||
}))
|
||||
|
||||
expectType<string>(strErrorAction('test').error)
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<boolean>(strErrorAction('test').error)
|
||||
}
|
||||
|
||||
/*
|
||||
* regression test for https://github.com/reduxjs/redux-toolkit/issues/214
|
||||
*/
|
||||
{
|
||||
const action = createAction<{ input?: string }>('ACTION')
|
||||
const t: string | undefined = action({ input: '' }).payload.input
|
||||
|
||||
// @ts-expect-error
|
||||
const u: number = action({ input: '' }).payload.input
|
||||
// @ts-expect-error
|
||||
const v: number = action({ input: 3 }).payload.input
|
||||
}
|
||||
|
||||
/*
|
||||
* regression test for https://github.com/reduxjs/redux-toolkit/issues/224
|
||||
*/
|
||||
{
|
||||
const oops = createAction('oops', (x: any) => ({
|
||||
payload: x,
|
||||
error: x,
|
||||
meta: x,
|
||||
}))
|
||||
|
||||
type Ret = ReturnType<typeof oops>
|
||||
|
||||
const payload: IsAny<Ret['payload'], true, false> = true
|
||||
const error: IsAny<Ret['error'], true, false> = true
|
||||
const meta: IsAny<Ret['meta'], true, false> = true
|
||||
|
||||
// @ts-expect-error
|
||||
const payloadNotAny: IsAny<Ret['payload'], true, false> = false
|
||||
// @ts-expect-error
|
||||
const errorNotAny: IsAny<Ret['error'], true, false> = false
|
||||
// @ts-expect-error
|
||||
const metaNotAny: IsAny<Ret['meta'], true, false> = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: createAction.match()
|
||||
*/
|
||||
{
|
||||
// simple use case
|
||||
{
|
||||
const actionCreator = createAction<string, 'test'>('test')
|
||||
const x: Action<unknown> = {} as any
|
||||
if (actionCreator.match(x)) {
|
||||
expectType<'test'>(x.type)
|
||||
expectType<string>(x.payload)
|
||||
} else {
|
||||
// @ts-expect-error
|
||||
expectType<'test'>(x.type)
|
||||
// @ts-expect-error
|
||||
expectType<any>(x.payload)
|
||||
}
|
||||
}
|
||||
|
||||
// special case: optional argument
|
||||
{
|
||||
const actionCreator = createAction<string | undefined, 'test'>('test')
|
||||
const x: Action<unknown> = {} as any
|
||||
if (actionCreator.match(x)) {
|
||||
expectType<'test'>(x.type)
|
||||
expectType<string | undefined>(x.payload)
|
||||
}
|
||||
}
|
||||
|
||||
// special case: without argument
|
||||
{
|
||||
const actionCreator = createAction('test')
|
||||
const x: Action<unknown> = {} as any
|
||||
if (actionCreator.match(x)) {
|
||||
expectType<'test'>(x.type)
|
||||
// @ts-expect-error
|
||||
expectType<{}>(x.payload)
|
||||
}
|
||||
}
|
||||
|
||||
// special case: with prepareAction
|
||||
{
|
||||
const actionCreator = createAction('test', () => ({
|
||||
payload: '',
|
||||
meta: '',
|
||||
error: false,
|
||||
}))
|
||||
const x: Action<unknown> = {} as any
|
||||
if (actionCreator.match(x)) {
|
||||
expectType<'test'>(x.type)
|
||||
expectType<string>(x.payload)
|
||||
expectType<string>(x.meta)
|
||||
expectType<boolean>(x.error)
|
||||
// @ts-expect-error
|
||||
expectType<number>(x.payload)
|
||||
// @ts-expect-error
|
||||
expectType<number>(x.meta)
|
||||
// @ts-expect-error
|
||||
expectType<number>(x.error)
|
||||
}
|
||||
}
|
||||
// potential use: as array filter
|
||||
{
|
||||
const actionCreator = createAction<string, 'test'>('test')
|
||||
const x: Array<Action<unknown>> = []
|
||||
expectType<Array<PayloadAction<string, 'test'>>>(
|
||||
x.filter(actionCreator.match)
|
||||
)
|
||||
|
||||
expectType<Array<PayloadAction<number, 'test'>>>(
|
||||
// @ts-expect-error
|
||||
x.filter(actionCreator.match)
|
||||
)
|
||||
}
|
||||
}
|
||||
{
|
||||
expectType<ActionCreatorWithOptionalPayload<string | undefined>>(
|
||||
createAction<string | undefined>('')
|
||||
)
|
||||
expectType<ActionCreatorWithoutPayload>(createAction<void>(''))
|
||||
expectType<ActionCreatorWithNonInferrablePayload>(createAction(''))
|
||||
expectType<ActionCreatorWithPayload<string>>(createAction<string>(''))
|
||||
expectType<ActionCreatorWithPreparedPayload<[0], 1, '', 2, 3>>(
|
||||
createAction('', (_: 0) => ({
|
||||
payload: 1 as 1,
|
||||
error: 2 as 2,
|
||||
meta: 3 as 3,
|
||||
}))
|
||||
)
|
||||
const anyCreator = createAction<any>('')
|
||||
expectType<ActionCreatorWithPayload<any>>(anyCreator)
|
||||
type AnyPayload = ReturnType<typeof anyCreator>['payload']
|
||||
expectType<IsAny<AnyPayload, true, false>>(true)
|
||||
}
|
||||
|
||||
// Verify action creators should not be passed directly as arguments
|
||||
// to React event handlers if there shouldn't be a payload
|
||||
{
|
||||
const emptyAction = createAction<void>('empty/action')
|
||||
function TestComponent() {
|
||||
// This typically leads to an error like:
|
||||
// // A non-serializable value was detected in an action, in the path: `payload`.
|
||||
// @ts-expect-error Should error because `void` and `MouseEvent` aren't compatible
|
||||
return <button onClick={emptyAction}>+</button>
|
||||
}
|
||||
}
|
||||
986
node_modules/@reduxjs/toolkit/src/tests/createAsyncThunk.test.ts
generated
vendored
Normal file
986
node_modules/@reduxjs/toolkit/src/tests/createAsyncThunk.test.ts
generated
vendored
Normal file
@@ -0,0 +1,986 @@
|
||||
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'
|
||||
import { delay } from '../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('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('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 = jest.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: 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('idGenerator should be called with thunkArg', async () => {
|
||||
const customIdGenerator = jest.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<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
|
||||
}
|
||||
})
|
||||
|
||||
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.fulfilled.type).toBe('a/fulfilled')
|
||||
})
|
||||
})
|
||||
768
node_modules/@reduxjs/toolkit/src/tests/createAsyncThunk.typetest.ts
generated
vendored
Normal file
768
node_modules/@reduxjs/toolkit/src/tests/createAsyncThunk.typetest.ts
generated
vendored
Normal file
@@ -0,0 +1,768 @@
|
||||
/* eslint-disable no-lone-blocks */
|
||||
import type { AnyAction, SerializedError, AsyncThunk } from '@reduxjs/toolkit'
|
||||
import {
|
||||
createAsyncThunk,
|
||||
createReducer,
|
||||
unwrapResult,
|
||||
createSlice,
|
||||
configureStore,
|
||||
} from '@reduxjs/toolkit'
|
||||
import type { ThunkDispatch } from 'redux-thunk'
|
||||
|
||||
import type { AxiosError } from 'axios'
|
||||
import apiRequest from 'axios'
|
||||
import type { IsAny, IsUnknown } from '@internal/tsHelpers'
|
||||
import { expectExactType, expectType } from './helpers'
|
||||
import type {
|
||||
AsyncThunkFulfilledActionCreator,
|
||||
AsyncThunkRejectedActionCreator,
|
||||
} from '@internal/createAsyncThunk'
|
||||
import type { TSVersion } from '@phryneas/ts-version'
|
||||
|
||||
const ANY = {} as any
|
||||
const defaultDispatch = (() => {}) as ThunkDispatch<{}, any, AnyAction>
|
||||
const anyAction = { type: 'foo' } as AnyAction
|
||||
|
||||
// basic usage
|
||||
;(async function () {
|
||||
const async = createAsyncThunk('test', (id: number) =>
|
||||
Promise.resolve(id * 2)
|
||||
)
|
||||
|
||||
const reducer = createReducer({}, (builder) =>
|
||||
builder
|
||||
.addCase(async.pending, (_, action) => {
|
||||
expectType<ReturnType<typeof async['pending']>>(action)
|
||||
})
|
||||
.addCase(async.fulfilled, (_, action) => {
|
||||
expectType<ReturnType<typeof async['fulfilled']>>(action)
|
||||
expectType<number>(action.payload)
|
||||
})
|
||||
.addCase(async.rejected, (_, action) => {
|
||||
expectType<ReturnType<typeof async['rejected']>>(action)
|
||||
expectType<Partial<Error> | undefined>(action.error)
|
||||
})
|
||||
)
|
||||
|
||||
const promise = defaultDispatch(async(3))
|
||||
|
||||
expectType<string>(promise.requestId)
|
||||
expectType<number>(promise.arg)
|
||||
expectType<(reason?: string) => void>(promise.abort)
|
||||
|
||||
const result = await promise
|
||||
|
||||
if (async.fulfilled.match(result)) {
|
||||
expectType<ReturnType<typeof async['fulfilled']>>(result)
|
||||
// @ts-expect-error
|
||||
expectType<ReturnType<typeof async['rejected']>>(result)
|
||||
} else {
|
||||
expectType<ReturnType<typeof async['rejected']>>(result)
|
||||
// @ts-expect-error
|
||||
expectType<ReturnType<typeof async['fulfilled']>>(result)
|
||||
}
|
||||
|
||||
promise
|
||||
.then(unwrapResult)
|
||||
.then((result) => {
|
||||
expectType<number>(result)
|
||||
// @ts-expect-error
|
||||
expectType<Error>(result)
|
||||
})
|
||||
.catch((error) => {
|
||||
// catch is always any-typed, nothing we can do here
|
||||
})
|
||||
})()
|
||||
|
||||
// More complex usage of thunk args
|
||||
;(async function () {
|
||||
interface BookModel {
|
||||
id: string
|
||||
title: string
|
||||
}
|
||||
|
||||
type BooksState = BookModel[]
|
||||
|
||||
const fakeBooks: BookModel[] = [
|
||||
{ id: 'b', title: 'Second' },
|
||||
{ id: 'a', title: 'First' },
|
||||
]
|
||||
|
||||
const correctDispatch = (() => {}) as ThunkDispatch<
|
||||
BookModel[],
|
||||
{ userAPI: Function },
|
||||
AnyAction
|
||||
>
|
||||
|
||||
// Verify that the the first type args to createAsyncThunk line up right
|
||||
const fetchBooksTAC = createAsyncThunk<
|
||||
BookModel[],
|
||||
number,
|
||||
{
|
||||
state: BooksState
|
||||
extra: { userAPI: Function }
|
||||
}
|
||||
>(
|
||||
'books/fetch',
|
||||
async (arg, { getState, dispatch, extra, requestId, signal }) => {
|
||||
const state = getState()
|
||||
|
||||
expectType<number>(arg)
|
||||
expectType<BookModel[]>(state)
|
||||
expectType<{ userAPI: Function }>(extra)
|
||||
return fakeBooks
|
||||
}
|
||||
)
|
||||
|
||||
correctDispatch(fetchBooksTAC(1))
|
||||
// @ts-expect-error
|
||||
defaultDispatch(fetchBooksTAC(1))
|
||||
})()
|
||||
/**
|
||||
* returning a rejected action from the promise creator is possible
|
||||
*/
|
||||
;(async () => {
|
||||
type ReturnValue = { data: 'success' }
|
||||
type RejectValue = { data: 'error' }
|
||||
|
||||
const fetchBooksTAC = createAsyncThunk<
|
||||
ReturnValue,
|
||||
number,
|
||||
{
|
||||
rejectValue: RejectValue
|
||||
}
|
||||
>('books/fetch', async (arg, { rejectWithValue }) => {
|
||||
return rejectWithValue({ data: 'error' })
|
||||
})
|
||||
|
||||
const returned = await defaultDispatch(fetchBooksTAC(1))
|
||||
if (fetchBooksTAC.rejected.match(returned)) {
|
||||
expectType<undefined | RejectValue>(returned.payload)
|
||||
expectType<RejectValue>(returned.payload!)
|
||||
} else {
|
||||
expectType<ReturnValue>(returned.payload)
|
||||
}
|
||||
|
||||
expectType<ReturnValue>(unwrapResult(returned))
|
||||
// @ts-expect-error
|
||||
expectType<RejectValue>(unwrapResult(returned))
|
||||
})()
|
||||
|
||||
/**
|
||||
* regression #1156: union return values fall back to allowing only single member
|
||||
*/
|
||||
;(async () => {
|
||||
const fn = createAsyncThunk('session/isAdmin', async () => {
|
||||
const response: boolean = false
|
||||
return response
|
||||
})
|
||||
})()
|
||||
|
||||
/**
|
||||
* Should handle reject withvalue within a try catch block
|
||||
*
|
||||
* Note:
|
||||
* this is a sample code taken from #1605
|
||||
*
|
||||
*/
|
||||
;(async () => {
|
||||
type ResultType = {
|
||||
text: string
|
||||
}
|
||||
const demoPromise = async (): Promise<ResultType> =>
|
||||
new Promise((resolve, _) => resolve({ text: '' }))
|
||||
const thunk = createAsyncThunk('thunk', async (args, thunkAPI) => {
|
||||
try {
|
||||
const result = await demoPromise()
|
||||
return result
|
||||
} catch (error) {
|
||||
return thunkAPI.rejectWithValue(error)
|
||||
}
|
||||
})
|
||||
createReducer({}, (builder) =>
|
||||
builder.addCase(thunk.fulfilled, (s, action) => {
|
||||
expectType<ResultType>(action.payload)
|
||||
})
|
||||
)
|
||||
})()
|
||||
|
||||
{
|
||||
interface Item {
|
||||
name: string
|
||||
}
|
||||
|
||||
interface ErrorFromServer {
|
||||
error: string
|
||||
}
|
||||
|
||||
interface CallsResponse {
|
||||
data: Item[]
|
||||
}
|
||||
|
||||
const fetchLiveCallsError = createAsyncThunk<
|
||||
Item[],
|
||||
string,
|
||||
{
|
||||
rejectValue: ErrorFromServer
|
||||
}
|
||||
>('calls/fetchLiveCalls', async (organizationId, { rejectWithValue }) => {
|
||||
try {
|
||||
const result = await apiRequest.get<CallsResponse>(
|
||||
`organizations/${organizationId}/calls/live/iwill404`
|
||||
)
|
||||
return result.data.data
|
||||
} catch (err) {
|
||||
let error: AxiosError<ErrorFromServer> = err as any // cast for access to AxiosError properties
|
||||
if (!error.response) {
|
||||
// let it be handled as any other unknown error
|
||||
throw err
|
||||
}
|
||||
return rejectWithValue(error.response && error.response.data)
|
||||
}
|
||||
})
|
||||
|
||||
defaultDispatch(fetchLiveCallsError('asd')).then((result) => {
|
||||
if (fetchLiveCallsError.fulfilled.match(result)) {
|
||||
//success
|
||||
expectType<ReturnType<typeof fetchLiveCallsError['fulfilled']>>(result)
|
||||
expectType<Item[]>(result.payload)
|
||||
} else {
|
||||
expectType<ReturnType<typeof fetchLiveCallsError['rejected']>>(result)
|
||||
if (result.payload) {
|
||||
// rejected with value
|
||||
expectType<ErrorFromServer>(result.payload)
|
||||
} else {
|
||||
// rejected by throw
|
||||
expectType<undefined>(result.payload)
|
||||
expectType<SerializedError>(result.error)
|
||||
// @ts-expect-error
|
||||
expectType<IsAny<typeof result['error'], true, false>>(true)
|
||||
}
|
||||
}
|
||||
defaultDispatch(fetchLiveCallsError('asd'))
|
||||
.then((result) => {
|
||||
expectType<Item[] | ErrorFromServer | undefined>(result.payload)
|
||||
// @ts-expect-error
|
||||
expectType<Item[]>(unwrapped)
|
||||
return result
|
||||
})
|
||||
.then(unwrapResult)
|
||||
.then((unwrapped) => {
|
||||
expectType<Item[]>(unwrapped)
|
||||
// @ts-expect-error
|
||||
expectType<ErrorFromServer>(unwrapResult(unwrapped))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* payloadCreator first argument type has impact on asyncThunk argument
|
||||
*/
|
||||
{
|
||||
// no argument: asyncThunk has no argument
|
||||
{
|
||||
const asyncThunk = createAsyncThunk('test', () => 0)
|
||||
expectType<() => any>(asyncThunk)
|
||||
// @ts-expect-error cannot be called with an argument
|
||||
asyncThunk(0 as any)
|
||||
}
|
||||
|
||||
// one argument, specified as undefined: asyncThunk has no argument
|
||||
{
|
||||
const asyncThunk = createAsyncThunk('test', (arg: undefined) => 0)
|
||||
expectType<() => any>(asyncThunk)
|
||||
// @ts-expect-error cannot be called with an argument
|
||||
asyncThunk(0 as any)
|
||||
}
|
||||
|
||||
// one argument, specified as void: asyncThunk has no argument
|
||||
{
|
||||
const asyncThunk = createAsyncThunk('test', (arg: void) => 0)
|
||||
expectType<() => any>(asyncThunk)
|
||||
// @ts-expect-error cannot be called with an argument
|
||||
asyncThunk(0 as any)
|
||||
}
|
||||
|
||||
// one argument, specified as optional number: asyncThunk has optional number argument
|
||||
// this test will fail with strictNullChecks: false, that is to be expected
|
||||
// in that case, we have to forbid this behaviour or it will make arguments optional everywhere
|
||||
{
|
||||
const asyncThunk = createAsyncThunk('test', (arg?: number) => 0)
|
||||
|
||||
// Per https://github.com/reduxjs/redux-toolkit/issues/3758#issuecomment-1742152774 , this is a bug in
|
||||
// TS 5.1 and 5.2, that is fixed in 5.3. Conditionally run the TS assertion here.
|
||||
type IsTS51Or52 = TSVersion.Major extends 5
|
||||
? TSVersion.Minor extends 1 | 2
|
||||
? true
|
||||
: false
|
||||
: false
|
||||
|
||||
type expectedType = IsTS51Or52 extends true
|
||||
? (arg: number) => any
|
||||
: (arg?: number) => any
|
||||
expectType<expectedType>(asyncThunk)
|
||||
// We _should_ be able to call this with no arguments, but we run into that error in 5.1 and 5.2.
|
||||
// Disabling this for now.
|
||||
// asyncThunk()
|
||||
asyncThunk(5)
|
||||
// @ts-expect-error
|
||||
asyncThunk('string')
|
||||
}
|
||||
|
||||
// one argument, specified as number|undefined: asyncThunk has optional number argument
|
||||
// this test will fail with strictNullChecks: false, that is to be expected
|
||||
// in that case, we have to forbid this behaviour or it will make arguments optional everywhere
|
||||
{
|
||||
const asyncThunk = createAsyncThunk('test', (arg: number | undefined) => 0)
|
||||
expectType<(arg?: number) => any>(asyncThunk)
|
||||
asyncThunk()
|
||||
asyncThunk(5)
|
||||
// @ts-expect-error
|
||||
asyncThunk('string')
|
||||
}
|
||||
|
||||
// one argument, specified as number|void: asyncThunk has optional number argument
|
||||
{
|
||||
const asyncThunk = createAsyncThunk('test', (arg: number | void) => 0)
|
||||
expectType<(arg?: number) => any>(asyncThunk)
|
||||
asyncThunk()
|
||||
asyncThunk(5)
|
||||
// @ts-expect-error
|
||||
asyncThunk('string')
|
||||
}
|
||||
|
||||
// one argument, specified as any: asyncThunk has required any argument
|
||||
{
|
||||
const asyncThunk = createAsyncThunk('test', (arg: any) => 0)
|
||||
expectType<IsAny<Parameters<typeof asyncThunk>[0], true, false>>(true)
|
||||
asyncThunk(5)
|
||||
// @ts-expect-error
|
||||
asyncThunk()
|
||||
}
|
||||
|
||||
// one argument, specified as unknown: asyncThunk has required unknown argument
|
||||
{
|
||||
const asyncThunk = createAsyncThunk('test', (arg: unknown) => 0)
|
||||
expectType<IsUnknown<Parameters<typeof asyncThunk>[0], true, false>>(true)
|
||||
asyncThunk(5)
|
||||
// @ts-expect-error
|
||||
asyncThunk()
|
||||
}
|
||||
|
||||
// one argument, specified as number: asyncThunk has required number argument
|
||||
{
|
||||
const asyncThunk = createAsyncThunk('test', (arg: number) => 0)
|
||||
expectType<(arg: number) => any>(asyncThunk)
|
||||
asyncThunk(5)
|
||||
// @ts-expect-error
|
||||
asyncThunk()
|
||||
}
|
||||
|
||||
// two arguments, first specified as undefined: asyncThunk has no argument
|
||||
{
|
||||
const asyncThunk = createAsyncThunk('test', (arg: undefined, thunkApi) => 0)
|
||||
expectType<() => any>(asyncThunk)
|
||||
// @ts-expect-error cannot be called with an argument
|
||||
asyncThunk(0 as any)
|
||||
}
|
||||
|
||||
// two arguments, first specified as void: asyncThunk has no argument
|
||||
{
|
||||
const asyncThunk = createAsyncThunk('test', (arg: void, thunkApi) => 0)
|
||||
expectType<() => any>(asyncThunk)
|
||||
// @ts-expect-error cannot be called with an argument
|
||||
asyncThunk(0 as any)
|
||||
}
|
||||
|
||||
// two arguments, first specified as number|undefined: asyncThunk has optional number argument
|
||||
// this test will fail with strictNullChecks: false, that is to be expected
|
||||
// in that case, we have to forbid this behaviour or it will make arguments optional everywhere
|
||||
{
|
||||
const asyncThunk = createAsyncThunk(
|
||||
'test',
|
||||
(arg: number | undefined, thunkApi) => 0
|
||||
)
|
||||
expectType<(arg?: number) => any>(asyncThunk)
|
||||
asyncThunk()
|
||||
asyncThunk(5)
|
||||
// @ts-expect-error
|
||||
asyncThunk('string')
|
||||
}
|
||||
|
||||
// two arguments, first specified as number|void: asyncThunk has optional number argument
|
||||
{
|
||||
const asyncThunk = createAsyncThunk(
|
||||
'test',
|
||||
(arg: number | void, thunkApi) => 0
|
||||
)
|
||||
expectType<(arg?: number) => any>(asyncThunk)
|
||||
asyncThunk()
|
||||
asyncThunk(5)
|
||||
// @ts-expect-error
|
||||
asyncThunk('string')
|
||||
}
|
||||
|
||||
// two arguments, first specified as any: asyncThunk has required any argument
|
||||
{
|
||||
const asyncThunk = createAsyncThunk('test', (arg: any, thunkApi) => 0)
|
||||
expectType<IsAny<Parameters<typeof asyncThunk>[0], true, false>>(true)
|
||||
asyncThunk(5)
|
||||
// @ts-expect-error
|
||||
asyncThunk()
|
||||
}
|
||||
|
||||
// two arguments, first specified as unknown: asyncThunk has required unknown argument
|
||||
{
|
||||
const asyncThunk = createAsyncThunk('test', (arg: unknown, thunkApi) => 0)
|
||||
expectType<IsUnknown<Parameters<typeof asyncThunk>[0], true, false>>(true)
|
||||
asyncThunk(5)
|
||||
// @ts-expect-error
|
||||
asyncThunk()
|
||||
}
|
||||
|
||||
// two arguments, first specified as number: asyncThunk has required number argument
|
||||
{
|
||||
const asyncThunk = createAsyncThunk('test', (arg: number, thunkApi) => 0)
|
||||
expectType<(arg: number) => any>(asyncThunk)
|
||||
asyncThunk(5)
|
||||
// @ts-expect-error
|
||||
asyncThunk()
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
// createAsyncThunk without generics
|
||||
const thunk = createAsyncThunk('test', () => {
|
||||
return 'ret' as const
|
||||
})
|
||||
expectType<AsyncThunk<'ret', void, {}>>(thunk)
|
||||
}
|
||||
|
||||
{
|
||||
// createAsyncThunk without generics, accessing `api` does not break return type
|
||||
const thunk = createAsyncThunk('test', (_: void, api) => {
|
||||
console.log(api)
|
||||
return 'ret' as const
|
||||
})
|
||||
expectType<AsyncThunk<'ret', void, {}>>(thunk)
|
||||
}
|
||||
|
||||
// createAsyncThunk rejectWithValue without generics: Expect correct return type
|
||||
{
|
||||
const asyncThunk = createAsyncThunk(
|
||||
'test',
|
||||
(_: void, { rejectWithValue }) => {
|
||||
try {
|
||||
return Promise.resolve(true)
|
||||
} catch (e) {
|
||||
return rejectWithValue(e)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
defaultDispatch(asyncThunk())
|
||||
.then((result) => {
|
||||
if (asyncThunk.fulfilled.match(result)) {
|
||||
expectType<ReturnType<AsyncThunkFulfilledActionCreator<boolean, void>>>(
|
||||
result
|
||||
)
|
||||
expectType<boolean>(result.payload)
|
||||
// @ts-expect-error
|
||||
expectType<any>(result.error)
|
||||
} else {
|
||||
expectType<ReturnType<AsyncThunkRejectedActionCreator<unknown, void>>>(
|
||||
result
|
||||
)
|
||||
expectType<SerializedError>(result.error)
|
||||
expectType<unknown>(result.payload)
|
||||
}
|
||||
|
||||
return result
|
||||
})
|
||||
.then(unwrapResult)
|
||||
.then((unwrapped) => {
|
||||
expectType<boolean>(unwrapped)
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
type Funky = { somethingElse: 'Funky!' }
|
||||
function funkySerializeError(err: any): Funky {
|
||||
return { somethingElse: 'Funky!' }
|
||||
}
|
||||
|
||||
// has to stay on one line or type tests fail in older TS versions
|
||||
// prettier-ignore
|
||||
// @ts-expect-error
|
||||
const shouldFail = createAsyncThunk('without generics', () => {}, { serializeError: funkySerializeError })
|
||||
|
||||
const shouldWork = createAsyncThunk<
|
||||
any,
|
||||
void,
|
||||
{ serializedErrorType: Funky }
|
||||
>('with generics', () => {}, {
|
||||
serializeError: funkySerializeError,
|
||||
})
|
||||
|
||||
if (shouldWork.rejected.match(anyAction)) {
|
||||
expectType<Funky>(anyAction.error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `idGenerator` option takes no arguments, and returns a string
|
||||
*/
|
||||
{
|
||||
const returnsNumWithArgs = (foo: any) => 100
|
||||
// has to stay on one line or type tests fail in older TS versions
|
||||
// prettier-ignore
|
||||
// @ts-expect-error
|
||||
const shouldFailNumWithArgs = createAsyncThunk('foo', () => {}, { idGenerator: returnsNumWithArgs })
|
||||
|
||||
const returnsNumWithoutArgs = () => 100
|
||||
// prettier-ignore
|
||||
// @ts-expect-error
|
||||
const shouldFailNumWithoutArgs = createAsyncThunk('foo', () => {}, { idGenerator: returnsNumWithoutArgs })
|
||||
|
||||
const returnsStrWithNumberArg = (foo: number) => 'foo'
|
||||
// prettier-ignore
|
||||
// @ts-expect-error
|
||||
const shouldFailWrongArgs = createAsyncThunk('foo', (arg: string) => {}, { idGenerator: returnsStrWithNumberArg })
|
||||
|
||||
const returnsStrWithStringArg = (foo: string) => 'foo'
|
||||
const shoulducceedCorrectArgs = createAsyncThunk('foo', (arg: string) => {}, {
|
||||
idGenerator: returnsStrWithStringArg,
|
||||
})
|
||||
|
||||
const returnsStrWithoutArgs = () => 'foo'
|
||||
const shouldSucceed = createAsyncThunk('foo', () => {}, {
|
||||
idGenerator: returnsStrWithoutArgs,
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
// https://github.com/reduxjs/redux-toolkit/issues/2886
|
||||
// fulfillWithValue should infer return value
|
||||
|
||||
const initialState = {
|
||||
loading: false,
|
||||
obj: { magic: '' },
|
||||
}
|
||||
|
||||
const getObj = createAsyncThunk(
|
||||
'slice/getObj',
|
||||
async (_: any, { fulfillWithValue, rejectWithValue }) => {
|
||||
try {
|
||||
return fulfillWithValue({ magic: 'object' })
|
||||
} catch (rejected: any) {
|
||||
return rejectWithValue(rejected?.response?.error || rejected)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
createSlice({
|
||||
name: 'slice',
|
||||
initialState,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(getObj.fulfilled, (state, action) => {
|
||||
expectExactType<{ magic: string }>(ANY)(action.payload)
|
||||
})
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// meta return values
|
||||
{
|
||||
// return values
|
||||
createAsyncThunk<'ret', void, {}>('test', (_, api) => 'ret' as const)
|
||||
createAsyncThunk<'ret', void, {}>('test', async (_, api) => 'ret' as const)
|
||||
createAsyncThunk<'ret', void, { fulfilledMeta: string }>('test', (_, api) =>
|
||||
api.fulfillWithValue('ret' as const, '')
|
||||
)
|
||||
createAsyncThunk<'ret', void, { fulfilledMeta: string }>(
|
||||
'test',
|
||||
async (_, api) => api.fulfillWithValue('ret' as const, '')
|
||||
)
|
||||
createAsyncThunk<'ret', void, { fulfilledMeta: string }>(
|
||||
'test',
|
||||
// @ts-expect-error has to be a fulfilledWithValue call
|
||||
(_, api) => 'ret' as const
|
||||
)
|
||||
createAsyncThunk<'ret', void, { fulfilledMeta: string }>(
|
||||
'test',
|
||||
// @ts-expect-error has to be a fulfilledWithValue call
|
||||
async (_, api) => 'ret' as const
|
||||
)
|
||||
createAsyncThunk<'ret', void, { fulfilledMeta: string }>(
|
||||
'test', // @ts-expect-error should only allow returning with 'test'
|
||||
(_, api) => api.fulfillWithValue(5, '')
|
||||
)
|
||||
createAsyncThunk<'ret', void, { fulfilledMeta: string }>(
|
||||
'test', // @ts-expect-error should only allow returning with 'test'
|
||||
async (_, api) => api.fulfillWithValue(5, '')
|
||||
)
|
||||
|
||||
// reject values
|
||||
createAsyncThunk<'ret', void, { rejectValue: string }>('test', (_, api) =>
|
||||
api.rejectWithValue('ret')
|
||||
)
|
||||
createAsyncThunk<'ret', void, { rejectValue: string }>(
|
||||
'test',
|
||||
async (_, api) => api.rejectWithValue('ret')
|
||||
)
|
||||
createAsyncThunk<'ret', void, { rejectValue: string; rejectedMeta: number }>(
|
||||
'test',
|
||||
(_, api) => api.rejectWithValue('ret', 5)
|
||||
)
|
||||
createAsyncThunk<'ret', void, { rejectValue: string; rejectedMeta: number }>(
|
||||
'test',
|
||||
async (_, api) => api.rejectWithValue('ret', 5)
|
||||
)
|
||||
createAsyncThunk<'ret', void, { rejectValue: string; rejectedMeta: number }>(
|
||||
'test',
|
||||
(_, api) => api.rejectWithValue('ret', 5)
|
||||
)
|
||||
createAsyncThunk<'ret', void, { rejectValue: string; rejectedMeta: number }>(
|
||||
'test',
|
||||
// @ts-expect-error wrong rejectedMeta type
|
||||
(_, api) => api.rejectWithValue('ret', '')
|
||||
)
|
||||
createAsyncThunk<'ret', void, { rejectValue: string; rejectedMeta: number }>(
|
||||
'test',
|
||||
// @ts-expect-error wrong rejectedMeta type
|
||||
async (_, api) => api.rejectWithValue('ret', '')
|
||||
)
|
||||
createAsyncThunk<'ret', void, { rejectValue: string; rejectedMeta: number }>(
|
||||
'test',
|
||||
// @ts-expect-error wrong rejectValue type
|
||||
(_, api) => api.rejectWithValue(5, '')
|
||||
)
|
||||
createAsyncThunk<'ret', void, { rejectValue: string; rejectedMeta: number }>(
|
||||
'test',
|
||||
// @ts-expect-error wrong rejectValue type
|
||||
async (_, api) => api.rejectWithValue(5, '')
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
const typedCAT = createAsyncThunk.withTypes<{
|
||||
state: RootState
|
||||
dispatch: AppDispatch
|
||||
rejectValue: string
|
||||
extra: { s: string; n: number }
|
||||
}>()
|
||||
|
||||
// inferred usage
|
||||
const thunk = typedCAT('foo', (arg: number, api) => {
|
||||
// correct getState Type
|
||||
const test1: number = api.getState().foo.value
|
||||
// correct dispatch type
|
||||
const test2: number = api.dispatch((dispatch, getState) => {
|
||||
expectExactType<
|
||||
ThunkDispatch<{ foo: { value: number } }, undefined, AnyAction>
|
||||
>(ANY)(dispatch)
|
||||
expectExactType<() => { foo: { value: number } }>(ANY)(getState)
|
||||
return getState().foo.value
|
||||
})
|
||||
|
||||
// correct extra type
|
||||
const { s, n } = api.extra
|
||||
expectExactType<string>(ANY)(s)
|
||||
expectExactType<number>(ANY)(n)
|
||||
|
||||
if (1 < 2)
|
||||
// @ts-expect-error
|
||||
return api.rejectWithValue(5)
|
||||
if (1 < 2) return api.rejectWithValue('test')
|
||||
return test1 + test2
|
||||
})
|
||||
|
||||
// usage with two generics
|
||||
const thunk2 = typedCAT<number, string>('foo', (arg, api) => {
|
||||
expectExactType('' as string)(arg)
|
||||
// correct getState Type
|
||||
const test1: number = api.getState().foo.value
|
||||
// correct dispatch type
|
||||
const test2: number = api.dispatch((dispatch, getState) => {
|
||||
expectExactType<
|
||||
ThunkDispatch<{ foo: { value: number } }, undefined, AnyAction>
|
||||
>(ANY)(dispatch)
|
||||
expectExactType<() => { foo: { value: number } }>(ANY)(getState)
|
||||
return getState().foo.value
|
||||
})
|
||||
// correct extra type
|
||||
const { s, n } = api.extra
|
||||
expectExactType<string>(ANY)(s)
|
||||
expectExactType<number>(ANY)(n)
|
||||
|
||||
if (1 < 2)
|
||||
// @ts-expect-error
|
||||
return api.rejectWithValue(5)
|
||||
if (1 < 2) return api.rejectWithValue('test')
|
||||
return test1 + test2
|
||||
})
|
||||
|
||||
// usage with config override generic
|
||||
const thunk3 = typedCAT<number, string, { rejectValue: number }>(
|
||||
'foo',
|
||||
(arg, api) => {
|
||||
expectExactType('' as string)(arg)
|
||||
// correct getState Type
|
||||
const test1: number = api.getState().foo.value
|
||||
// correct dispatch type
|
||||
const test2: number = api.dispatch((dispatch, getState) => {
|
||||
expectExactType<
|
||||
ThunkDispatch<{ foo: { value: number } }, undefined, AnyAction>
|
||||
>(ANY)(dispatch)
|
||||
expectExactType<() => { foo: { value: number } }>(ANY)(getState)
|
||||
return getState().foo.value
|
||||
})
|
||||
// correct extra type
|
||||
const { s, n } = api.extra
|
||||
expectExactType<string>(ANY)(s)
|
||||
expectExactType<number>(ANY)(n)
|
||||
if (1 < 2) return api.rejectWithValue(5)
|
||||
if (1 < 2)
|
||||
// @ts-expect-error
|
||||
return api.rejectWithValue('test')
|
||||
return 5
|
||||
}
|
||||
)
|
||||
|
||||
const slice = createSlice({
|
||||
name: 'foo',
|
||||
initialState: { value: 0 },
|
||||
reducers: {},
|
||||
extraReducers(builder) {
|
||||
builder
|
||||
.addCase(thunk.fulfilled, (state, action) => {
|
||||
state.value += action.payload
|
||||
})
|
||||
.addCase(thunk.rejected, (state, action) => {
|
||||
expectExactType('' as string | undefined)(action.payload)
|
||||
})
|
||||
.addCase(thunk2.fulfilled, (state, action) => {
|
||||
state.value += action.payload
|
||||
})
|
||||
.addCase(thunk2.rejected, (state, action) => {
|
||||
expectExactType('' as string | undefined)(action.payload)
|
||||
})
|
||||
.addCase(thunk3.fulfilled, (state, action) => {
|
||||
state.value += action.payload
|
||||
})
|
||||
.addCase(thunk3.rejected, (state, action) => {
|
||||
expectExactType(0 as number | undefined)(action.payload)
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
foo: slice.reducer,
|
||||
},
|
||||
})
|
||||
|
||||
type RootState = ReturnType<typeof store.getState>
|
||||
type AppDispatch = typeof store.dispatch
|
||||
}
|
||||
36
node_modules/@reduxjs/toolkit/src/tests/createDraftSafeSelector.test.ts
generated
vendored
Normal file
36
node_modules/@reduxjs/toolkit/src/tests/createDraftSafeSelector.test.ts
generated
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createDraftSafeSelector, createSelector } from '@reduxjs/toolkit'
|
||||
import { produce } from 'immer'
|
||||
|
||||
type State = { value: number }
|
||||
const selectSelf = (state: State) => state
|
||||
|
||||
test('handles normal values correctly', () => {
|
||||
const unsafeSelector = createSelector(selectSelf, (x) => x.value)
|
||||
const draftSafeSelector = createDraftSafeSelector(selectSelf, (x) => x.value)
|
||||
|
||||
let state = { value: 1 }
|
||||
expect(unsafeSelector(state)).toBe(1)
|
||||
expect(draftSafeSelector(state)).toBe(1)
|
||||
|
||||
state = { value: 2 }
|
||||
expect(unsafeSelector(state)).toBe(2)
|
||||
expect(draftSafeSelector(state)).toBe(2)
|
||||
})
|
||||
|
||||
test('handles drafts correctly', () => {
|
||||
const unsafeSelector = createSelector(selectSelf, (state) => state.value)
|
||||
const draftSafeSelector = createDraftSafeSelector(
|
||||
selectSelf,
|
||||
(state) => state.value
|
||||
)
|
||||
|
||||
produce({ value: 1 }, (state) => {
|
||||
expect(unsafeSelector(state)).toBe(1)
|
||||
expect(draftSafeSelector(state)).toBe(1)
|
||||
|
||||
state.value = 2
|
||||
|
||||
expect(unsafeSelector(state)).toBe(1)
|
||||
expect(draftSafeSelector(state)).toBe(2)
|
||||
})
|
||||
})
|
||||
132
node_modules/@reduxjs/toolkit/src/tests/createEntityAdapter.typetest.ts
generated
vendored
Normal file
132
node_modules/@reduxjs/toolkit/src/tests/createEntityAdapter.typetest.ts
generated
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
import type {
|
||||
EntityAdapter,
|
||||
ActionCreatorWithPayload,
|
||||
ActionCreatorWithoutPayload,
|
||||
EntityStateAdapter,
|
||||
EntityId,
|
||||
Update,
|
||||
} from '@reduxjs/toolkit'
|
||||
import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'
|
||||
import { expectType } from './helpers'
|
||||
|
||||
function extractReducers<T>(
|
||||
adapter: EntityAdapter<T>
|
||||
): Omit<EntityStateAdapter<T>, 'map'> {
|
||||
const { selectId, sortComparer, getInitialState, getSelectors, ...rest } =
|
||||
adapter
|
||||
return rest
|
||||
}
|
||||
|
||||
/**
|
||||
* should be usable in a slice, with all the "reducer-like" functions
|
||||
*/
|
||||
{
|
||||
type Entity = {
|
||||
value: string
|
||||
}
|
||||
const adapter = createEntityAdapter<Entity>()
|
||||
const slice = createSlice({
|
||||
name: 'test',
|
||||
initialState: adapter.getInitialState(),
|
||||
reducers: {
|
||||
...extractReducers(adapter),
|
||||
},
|
||||
})
|
||||
|
||||
expectType<ActionCreatorWithPayload<Entity>>(slice.actions.addOne)
|
||||
expectType<
|
||||
ActionCreatorWithPayload<ReadonlyArray<Entity> | Record<string, Entity>>
|
||||
>(slice.actions.addMany)
|
||||
expectType<
|
||||
ActionCreatorWithPayload<ReadonlyArray<Entity> | Record<string, Entity>>
|
||||
>(slice.actions.setAll)
|
||||
expectType<ActionCreatorWithPayload<Entity[] | Record<string, Entity>>>(
|
||||
// @ts-expect-error
|
||||
slice.actions.addMany
|
||||
)
|
||||
expectType<ActionCreatorWithPayload<Entity[] | Record<string, Entity>>>(
|
||||
// @ts-expect-error
|
||||
slice.actions.setAll
|
||||
)
|
||||
expectType<ActionCreatorWithPayload<EntityId>>(slice.actions.removeOne)
|
||||
expectType<ActionCreatorWithPayload<ReadonlyArray<EntityId>>>(
|
||||
slice.actions.removeMany
|
||||
)
|
||||
// @ts-expect-error
|
||||
expectType<ActionCreatorWithPayload<EntityId[]>>(slice.actions.removeMany)
|
||||
expectType<ActionCreatorWithoutPayload>(slice.actions.removeAll)
|
||||
expectType<ActionCreatorWithPayload<Update<Entity>>>(slice.actions.updateOne)
|
||||
expectType<ActionCreatorWithPayload<Update<Entity>[]>>(
|
||||
// @ts-expect-error
|
||||
slice.actions.updateMany
|
||||
)
|
||||
expectType<ActionCreatorWithPayload<ReadonlyArray<Update<Entity>>>>(
|
||||
slice.actions.updateMany
|
||||
)
|
||||
expectType<ActionCreatorWithPayload<Entity>>(slice.actions.upsertOne)
|
||||
expectType<
|
||||
ActionCreatorWithPayload<ReadonlyArray<Entity> | Record<string, Entity>>
|
||||
>(slice.actions.upsertMany)
|
||||
expectType<ActionCreatorWithPayload<Entity[] | Record<string, Entity>>>(
|
||||
// @ts-expect-error
|
||||
slice.actions.upsertMany
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* should not be able to mix with a different EntityAdapter
|
||||
*/
|
||||
{
|
||||
type Entity = {
|
||||
value: string
|
||||
}
|
||||
type Entity2 = {
|
||||
value2: string
|
||||
}
|
||||
const adapter = createEntityAdapter<Entity>()
|
||||
const adapter2 = createEntityAdapter<Entity2>()
|
||||
createSlice({
|
||||
name: 'test',
|
||||
initialState: adapter.getInitialState(),
|
||||
reducers: {
|
||||
addOne: adapter.addOne,
|
||||
// @ts-expect-error
|
||||
addOne2: adapter2.addOne,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* should be usable in a slice with extra properties
|
||||
*/
|
||||
{
|
||||
type Entity = {
|
||||
value: string
|
||||
}
|
||||
const adapter = createEntityAdapter<Entity>()
|
||||
createSlice({
|
||||
name: 'test',
|
||||
initialState: adapter.getInitialState({ extraData: 'test' }),
|
||||
reducers: {
|
||||
addOne: adapter.addOne,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* should not be usable in a slice with an unfitting state
|
||||
*/
|
||||
{
|
||||
type Entity = {
|
||||
value: string
|
||||
}
|
||||
const adapter = createEntityAdapter<Entity>()
|
||||
createSlice({
|
||||
name: 'test',
|
||||
initialState: { somethingElse: '' },
|
||||
reducers: {
|
||||
// @ts-expect-error
|
||||
addOne: adapter.addOne,
|
||||
},
|
||||
})
|
||||
}
|
||||
741
node_modules/@reduxjs/toolkit/src/tests/createReducer.test.ts
generated
vendored
Normal file
741
node_modules/@reduxjs/toolkit/src/tests/createReducer.test.ts
generated
vendored
Normal file
@@ -0,0 +1,741 @@
|
||||
import type {
|
||||
CaseReducer,
|
||||
PayloadAction,
|
||||
Draft,
|
||||
Reducer,
|
||||
AnyAction,
|
||||
} from '@reduxjs/toolkit'
|
||||
import { createReducer, createAction, createNextState } from '@reduxjs/toolkit'
|
||||
import {
|
||||
mockConsole,
|
||||
createConsole,
|
||||
getLog,
|
||||
} from 'console-testing-library/pure'
|
||||
|
||||
interface Todo {
|
||||
text: string
|
||||
completed?: boolean
|
||||
}
|
||||
|
||||
interface AddTodoPayload {
|
||||
newTodo: Todo
|
||||
}
|
||||
|
||||
interface ToggleTodoPayload {
|
||||
index: number
|
||||
}
|
||||
|
||||
type TodoState = Todo[]
|
||||
type TodosReducer = Reducer<TodoState, PayloadAction<any>>
|
||||
type AddTodoReducer = CaseReducer<TodoState, PayloadAction<AddTodoPayload>>
|
||||
|
||||
type ToggleTodoReducer = CaseReducer<
|
||||
TodoState,
|
||||
PayloadAction<ToggleTodoPayload>
|
||||
>
|
||||
|
||||
type CreateReducer = typeof createReducer
|
||||
|
||||
describe('createReducer', () => {
|
||||
let restore: () => void
|
||||
|
||||
beforeEach(() => {
|
||||
restore = mockConsole(createConsole())
|
||||
})
|
||||
describe('given impure reducers with immer', () => {
|
||||
const addTodo: AddTodoReducer = (state, action) => {
|
||||
const { newTodo } = action.payload
|
||||
|
||||
// Can safely call state.push() here
|
||||
state.push({ ...newTodo, completed: false })
|
||||
}
|
||||
|
||||
const toggleTodo: ToggleTodoReducer = (state, action) => {
|
||||
const { index } = action.payload
|
||||
|
||||
const todo = state[index]
|
||||
// Can directly modify the todo object
|
||||
todo.completed = !todo.completed
|
||||
}
|
||||
|
||||
const todosReducer = createReducer([] as TodoState, {
|
||||
ADD_TODO: addTodo,
|
||||
TOGGLE_TODO: toggleTodo,
|
||||
})
|
||||
|
||||
behavesLikeReducer(todosReducer)
|
||||
})
|
||||
|
||||
describe('Deprecation warnings', () => {
|
||||
let originalNodeEnv = process.env.NODE_ENV
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env.NODE_ENV = originalNodeEnv
|
||||
})
|
||||
|
||||
it('Warns about object notation deprecation, once', () => {
|
||||
const { createReducer } = require('../createReducer')
|
||||
let dummyReducer = (createReducer as CreateReducer)([] as TodoState, {})
|
||||
|
||||
expect(getLog().levels.warn).toMatch(
|
||||
/The object notation for `createReducer` is deprecated/
|
||||
)
|
||||
restore = mockConsole(createConsole())
|
||||
|
||||
dummyReducer = (createReducer as CreateReducer)([] as TodoState, {})
|
||||
expect(getLog().levels.warn).toBe('')
|
||||
})
|
||||
|
||||
it('Does not warn in production', () => {
|
||||
process.env.NODE_ENV = 'production'
|
||||
const { createReducer } = require('../createReducer')
|
||||
let dummyReducer = (createReducer as CreateReducer)([] as TodoState, {})
|
||||
|
||||
expect(getLog().levels.warn).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Immer in a production environment', () => {
|
||||
let originalNodeEnv = process.env.NODE_ENV
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
process.env.NODE_ENV = 'production'
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env.NODE_ENV = originalNodeEnv
|
||||
})
|
||||
|
||||
test('Freezes data in production', () => {
|
||||
const { createReducer } = require('../createReducer')
|
||||
const addTodo: AddTodoReducer = (state, action) => {
|
||||
const { newTodo } = action.payload
|
||||
state.push({ ...newTodo, completed: false })
|
||||
}
|
||||
|
||||
const toggleTodo: ToggleTodoReducer = (state, action) => {
|
||||
const { index } = action.payload
|
||||
const todo = state[index]
|
||||
todo.completed = !todo.completed
|
||||
}
|
||||
|
||||
const todosReducer = createReducer([] as TodoState, {
|
||||
ADD_TODO: addTodo,
|
||||
TOGGLE_TODO: toggleTodo,
|
||||
})
|
||||
|
||||
const result = todosReducer([], {
|
||||
type: 'ADD_TODO',
|
||||
payload: { text: 'Buy milk' },
|
||||
})
|
||||
|
||||
const mutateStateOutsideReducer = () => (result[0].text = 'edited')
|
||||
expect(mutateStateOutsideReducer).toThrowError(
|
||||
'Cannot add property text, object is not extensible'
|
||||
)
|
||||
})
|
||||
|
||||
test('Freezes initial state', () => {
|
||||
const initialState = [{ text: 'Buy milk' }]
|
||||
const todosReducer = createReducer(initialState, {})
|
||||
const frozenInitialState = todosReducer(undefined, { type: 'dummy' })
|
||||
|
||||
const mutateStateOutsideReducer = () =>
|
||||
(frozenInitialState[0].text = 'edited')
|
||||
expect(mutateStateOutsideReducer).toThrowError(
|
||||
/Cannot assign to read only property/
|
||||
)
|
||||
})
|
||||
test('does not throw error if initial state is not draftable', () => {
|
||||
expect(() => createReducer(new URLSearchParams(), {})).not.toThrowError()
|
||||
})
|
||||
})
|
||||
|
||||
describe('given pure reducers with immutable updates', () => {
|
||||
const addTodo: AddTodoReducer = (state, action) => {
|
||||
const { newTodo } = action.payload
|
||||
|
||||
// Updates the state immutably without relying on immer
|
||||
return state.concat({ ...newTodo, completed: false })
|
||||
}
|
||||
|
||||
const toggleTodo: ToggleTodoReducer = (state, action) => {
|
||||
const { index } = action.payload
|
||||
|
||||
// Updates the todo object immutably withot relying on immer
|
||||
return state.map((todo, i) => {
|
||||
if (i !== index) return todo
|
||||
return { ...todo, completed: !todo.completed }
|
||||
})
|
||||
}
|
||||
|
||||
const todosReducer = createReducer([] as TodoState, {
|
||||
ADD_TODO: addTodo,
|
||||
TOGGLE_TODO: toggleTodo,
|
||||
})
|
||||
|
||||
behavesLikeReducer(todosReducer)
|
||||
})
|
||||
|
||||
describe('Accepts a lazy state init function to generate initial state', () => {
|
||||
const addTodo: AddTodoReducer = (state, action) => {
|
||||
const { newTodo } = action.payload
|
||||
state.push({ ...newTodo, completed: false })
|
||||
}
|
||||
|
||||
const toggleTodo: ToggleTodoReducer = (state, action) => {
|
||||
const { index } = action.payload
|
||||
const todo = state[index]
|
||||
todo.completed = !todo.completed
|
||||
}
|
||||
|
||||
const lazyStateInit = () => [] as TodoState
|
||||
|
||||
const todosReducer = createReducer(lazyStateInit, {
|
||||
ADD_TODO: addTodo,
|
||||
TOGGLE_TODO: toggleTodo,
|
||||
})
|
||||
|
||||
behavesLikeReducer(todosReducer)
|
||||
|
||||
it('Should only call the init function when `undefined` state is passed in', () => {
|
||||
const spy = jest.fn().mockReturnValue(42)
|
||||
|
||||
const dummyReducer = createReducer(spy, {})
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
|
||||
dummyReducer(123, { type: 'dummy' })
|
||||
expect(spy).not.toHaveBeenCalled()
|
||||
|
||||
const initialState = dummyReducer(undefined, { type: 'dummy' })
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('given draft state from immer', () => {
|
||||
const addTodo: AddTodoReducer = (state, action) => {
|
||||
const { newTodo } = action.payload
|
||||
|
||||
// Can safely call state.push() here
|
||||
state.push({ ...newTodo, completed: false })
|
||||
}
|
||||
|
||||
const toggleTodo: ToggleTodoReducer = (state, action) => {
|
||||
const { index } = action.payload
|
||||
|
||||
const todo = state[index]
|
||||
// Can directly modify the todo object
|
||||
todo.completed = !todo.completed
|
||||
}
|
||||
|
||||
const todosReducer = createReducer([] as TodoState, {
|
||||
ADD_TODO: addTodo,
|
||||
TOGGLE_TODO: toggleTodo,
|
||||
})
|
||||
|
||||
const wrappedReducer: TodosReducer = (state = [], action) => {
|
||||
return createNextState(state, (draft: Draft<TodoState>) => {
|
||||
todosReducer(draft, action)
|
||||
})
|
||||
}
|
||||
|
||||
behavesLikeReducer(wrappedReducer)
|
||||
})
|
||||
|
||||
describe('actionMatchers argument', () => {
|
||||
const prepareNumberAction = (payload: number) => ({
|
||||
payload,
|
||||
meta: { type: 'number_action' },
|
||||
})
|
||||
const prepareStringAction = (payload: string) => ({
|
||||
payload,
|
||||
meta: { type: 'string_action' },
|
||||
})
|
||||
|
||||
const numberActionMatcher = (a: AnyAction): a is PayloadAction<number> =>
|
||||
a.meta && a.meta.type === 'number_action'
|
||||
const stringActionMatcher = (a: AnyAction): a is PayloadAction<string> =>
|
||||
a.meta && a.meta.type === 'string_action'
|
||||
|
||||
const incrementBy = createAction('increment', prepareNumberAction)
|
||||
const decrementBy = createAction('decrement', prepareNumberAction)
|
||||
const concatWith = createAction('concat', prepareStringAction)
|
||||
|
||||
const initialState = { numberActions: 0, stringActions: 0 }
|
||||
const numberActionsCounter = {
|
||||
matcher: numberActionMatcher,
|
||||
reducer(state: typeof initialState) {
|
||||
state.numberActions = state.numberActions * 10 + 1
|
||||
},
|
||||
}
|
||||
const stringActionsCounter = {
|
||||
matcher: stringActionMatcher,
|
||||
reducer(state: typeof initialState) {
|
||||
state.stringActions = state.stringActions * 10 + 1
|
||||
},
|
||||
}
|
||||
|
||||
test('uses the reducer of matching actionMatchers', () => {
|
||||
const reducer = createReducer(initialState, {}, [
|
||||
numberActionsCounter,
|
||||
stringActionsCounter,
|
||||
])
|
||||
expect(reducer(undefined, incrementBy(1))).toEqual({
|
||||
numberActions: 1,
|
||||
stringActions: 0,
|
||||
})
|
||||
expect(reducer(undefined, decrementBy(1))).toEqual({
|
||||
numberActions: 1,
|
||||
stringActions: 0,
|
||||
})
|
||||
expect(reducer(undefined, concatWith('foo'))).toEqual({
|
||||
numberActions: 0,
|
||||
stringActions: 1,
|
||||
})
|
||||
})
|
||||
test('fallback to default case', () => {
|
||||
const reducer = createReducer(
|
||||
initialState,
|
||||
{},
|
||||
[numberActionsCounter, stringActionsCounter],
|
||||
(state) => {
|
||||
state.numberActions = -1
|
||||
state.stringActions = -1
|
||||
}
|
||||
)
|
||||
expect(reducer(undefined, { type: 'somethingElse' })).toEqual({
|
||||
numberActions: -1,
|
||||
stringActions: -1,
|
||||
})
|
||||
})
|
||||
test('runs reducer cases followed by all matching actionMatchers', () => {
|
||||
const reducer = createReducer(
|
||||
initialState,
|
||||
{
|
||||
[incrementBy.type](state) {
|
||||
state.numberActions = state.numberActions * 10 + 2
|
||||
},
|
||||
},
|
||||
[
|
||||
{
|
||||
matcher: numberActionMatcher,
|
||||
reducer(state) {
|
||||
state.numberActions = state.numberActions * 10 + 3
|
||||
},
|
||||
},
|
||||
numberActionsCounter,
|
||||
stringActionsCounter,
|
||||
]
|
||||
)
|
||||
expect(reducer(undefined, incrementBy(1))).toEqual({
|
||||
numberActions: 231,
|
||||
stringActions: 0,
|
||||
})
|
||||
expect(reducer(undefined, decrementBy(1))).toEqual({
|
||||
numberActions: 31,
|
||||
stringActions: 0,
|
||||
})
|
||||
expect(reducer(undefined, concatWith('foo'))).toEqual({
|
||||
numberActions: 0,
|
||||
stringActions: 1,
|
||||
})
|
||||
})
|
||||
test('works with `actionCreator.match`', () => {
|
||||
const reducer = createReducer(initialState, {}, [
|
||||
{
|
||||
matcher: incrementBy.match,
|
||||
reducer(state) {
|
||||
state.numberActions += 100
|
||||
},
|
||||
},
|
||||
])
|
||||
expect(reducer(undefined, incrementBy(1))).toEqual({
|
||||
numberActions: 100,
|
||||
stringActions: 0,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('alternative builder callback for actionMap', () => {
|
||||
const increment = createAction<number, 'increment'>('increment')
|
||||
const decrement = createAction<number, 'decrement'>('decrement')
|
||||
|
||||
test('can be used with ActionCreators', () => {
|
||||
const reducer = createReducer(0, (builder) =>
|
||||
builder
|
||||
.addCase(increment, (state, action) => state + action.payload)
|
||||
.addCase(decrement, (state, action) => state - action.payload)
|
||||
)
|
||||
expect(reducer(0, increment(5))).toBe(5)
|
||||
expect(reducer(5, decrement(5))).toBe(0)
|
||||
})
|
||||
test('can be used with string types', () => {
|
||||
const reducer = createReducer(0, (builder) =>
|
||||
builder
|
||||
.addCase(
|
||||
'increment',
|
||||
(state, action: { type: 'increment'; payload: number }) =>
|
||||
state + action.payload
|
||||
)
|
||||
.addCase(
|
||||
'decrement',
|
||||
(state, action: { type: 'decrement'; payload: number }) =>
|
||||
state - action.payload
|
||||
)
|
||||
)
|
||||
expect(reducer(0, increment(5))).toBe(5)
|
||||
expect(reducer(5, decrement(5))).toBe(0)
|
||||
})
|
||||
test('can be used with ActionCreators and string types combined', () => {
|
||||
const reducer = createReducer(0, (builder) =>
|
||||
builder
|
||||
.addCase(increment, (state, action) => state + action.payload)
|
||||
.addCase(
|
||||
'decrement',
|
||||
(state, action: { type: 'decrement'; payload: number }) =>
|
||||
state - action.payload
|
||||
)
|
||||
)
|
||||
expect(reducer(0, increment(5))).toBe(5)
|
||||
expect(reducer(5, decrement(5))).toBe(0)
|
||||
})
|
||||
test('will throw an error when returning undefined from a non-draftable state', () => {
|
||||
const reducer = createReducer(0, (builder) =>
|
||||
builder.addCase(
|
||||
'decrement',
|
||||
(state, action: { type: 'decrement'; payload: number }) => {}
|
||||
)
|
||||
)
|
||||
expect(() => reducer(5, decrement(5))).toThrowErrorMatchingInlineSnapshot(
|
||||
`"A case reducer on a non-draftable value must not return undefined"`
|
||||
)
|
||||
})
|
||||
test('allows you to return undefined if the state was null, thus skipping an update', () => {
|
||||
const reducer = createReducer(null as number | null, (builder) =>
|
||||
builder.addCase(
|
||||
'decrement',
|
||||
(state, action: { type: 'decrement'; payload: number }) => {
|
||||
if (typeof state === 'number') {
|
||||
return state - action.payload
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
)
|
||||
)
|
||||
expect(reducer(0, decrement(5))).toBe(-5)
|
||||
expect(reducer(null, decrement(5))).toBe(null)
|
||||
})
|
||||
test('allows you to return null', () => {
|
||||
const reducer = createReducer(0 as number | null, (builder) =>
|
||||
builder.addCase(
|
||||
'decrement',
|
||||
(state, action: { type: 'decrement'; payload: number }) => {
|
||||
return null
|
||||
}
|
||||
)
|
||||
)
|
||||
expect(reducer(5, decrement(5))).toBe(null)
|
||||
})
|
||||
test('allows you to return 0', () => {
|
||||
const reducer = createReducer(0, (builder) =>
|
||||
builder.addCase(
|
||||
'decrement',
|
||||
(state, action: { type: 'decrement'; payload: number }) =>
|
||||
state - action.payload
|
||||
)
|
||||
)
|
||||
expect(reducer(5, decrement(5))).toBe(0)
|
||||
})
|
||||
test('will throw if the same type is used twice', () => {
|
||||
expect(() =>
|
||||
createReducer(0, (builder) =>
|
||||
builder
|
||||
.addCase(increment, (state, action) => state + action.payload)
|
||||
.addCase(increment, (state, action) => state + action.payload)
|
||||
.addCase(decrement, (state, action) => state - action.payload)
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
'"`builder.addCase` cannot be called with two reducers for the same action type"'
|
||||
)
|
||||
expect(() =>
|
||||
createReducer(0, (builder) =>
|
||||
builder
|
||||
.addCase(increment, (state, action) => state + action.payload)
|
||||
.addCase('increment', (state) => state + 1)
|
||||
.addCase(decrement, (state, action) => state - action.payload)
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
'"`builder.addCase` cannot be called with two reducers for the same action type"'
|
||||
)
|
||||
})
|
||||
|
||||
test('will throw if an empty type is used', () => {
|
||||
const customActionCreator = (payload: number) => ({
|
||||
type: 'custom_action',
|
||||
payload,
|
||||
})
|
||||
customActionCreator.type = ""
|
||||
expect(() =>
|
||||
createReducer(0, (builder) =>
|
||||
builder.addCase(
|
||||
customActionCreator,
|
||||
(state, action) => state + action.payload
|
||||
)
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
'"`builder.addCase` cannot be called with an empty action type"'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('builder "addMatcher" method', () => {
|
||||
const prepareNumberAction = (payload: number) => ({
|
||||
payload,
|
||||
meta: { type: 'number_action' },
|
||||
})
|
||||
const prepareStringAction = (payload: string) => ({
|
||||
payload,
|
||||
meta: { type: 'string_action' },
|
||||
})
|
||||
|
||||
const numberActionMatcher = (a: AnyAction): a is PayloadAction<number> =>
|
||||
a.meta && a.meta.type === 'number_action'
|
||||
const stringActionMatcher = (a: AnyAction): a is PayloadAction<string> =>
|
||||
a.meta && a.meta.type === 'string_action'
|
||||
|
||||
const incrementBy = createAction('increment', prepareNumberAction)
|
||||
const decrementBy = createAction('decrement', prepareNumberAction)
|
||||
const concatWith = createAction('concat', prepareStringAction)
|
||||
|
||||
const initialState = { numberActions: 0, stringActions: 0 }
|
||||
|
||||
test('uses the reducer of matching actionMatchers', () => {
|
||||
const reducer = createReducer(initialState, (builder) =>
|
||||
builder
|
||||
.addMatcher(numberActionMatcher, (state) => {
|
||||
state.numberActions += 1
|
||||
})
|
||||
.addMatcher(stringActionMatcher, (state) => {
|
||||
state.stringActions += 1
|
||||
})
|
||||
)
|
||||
expect(reducer(undefined, incrementBy(1))).toEqual({
|
||||
numberActions: 1,
|
||||
stringActions: 0,
|
||||
})
|
||||
expect(reducer(undefined, decrementBy(1))).toEqual({
|
||||
numberActions: 1,
|
||||
stringActions: 0,
|
||||
})
|
||||
expect(reducer(undefined, concatWith('foo'))).toEqual({
|
||||
numberActions: 0,
|
||||
stringActions: 1,
|
||||
})
|
||||
})
|
||||
test('falls back to defaultCase', () => {
|
||||
const reducer = createReducer(initialState, (builder) =>
|
||||
builder
|
||||
.addCase(concatWith, (state) => {
|
||||
state.stringActions += 1
|
||||
})
|
||||
.addMatcher(numberActionMatcher, (state) => {
|
||||
state.numberActions += 1
|
||||
})
|
||||
.addDefaultCase((state) => {
|
||||
state.numberActions = -1
|
||||
state.stringActions = -1
|
||||
})
|
||||
)
|
||||
expect(reducer(undefined, { type: 'somethingElse' })).toEqual({
|
||||
numberActions: -1,
|
||||
stringActions: -1,
|
||||
})
|
||||
})
|
||||
test('runs reducer cases followed by all matching actionMatchers', () => {
|
||||
const reducer = createReducer(initialState, (builder) =>
|
||||
builder
|
||||
.addCase(incrementBy, (state) => {
|
||||
state.numberActions = state.numberActions * 10 + 1
|
||||
})
|
||||
.addMatcher(numberActionMatcher, (state) => {
|
||||
state.numberActions = state.numberActions * 10 + 2
|
||||
})
|
||||
.addMatcher(stringActionMatcher, (state) => {
|
||||
state.stringActions = state.stringActions * 10 + 1
|
||||
})
|
||||
.addMatcher(numberActionMatcher, (state) => {
|
||||
state.numberActions = state.numberActions * 10 + 3
|
||||
})
|
||||
)
|
||||
expect(reducer(undefined, incrementBy(1))).toEqual({
|
||||
numberActions: 123,
|
||||
stringActions: 0,
|
||||
})
|
||||
expect(reducer(undefined, decrementBy(1))).toEqual({
|
||||
numberActions: 23,
|
||||
stringActions: 0,
|
||||
})
|
||||
expect(reducer(undefined, concatWith('foo'))).toEqual({
|
||||
numberActions: 0,
|
||||
stringActions: 1,
|
||||
})
|
||||
})
|
||||
test('works with `actionCreator.match`', () => {
|
||||
const reducer = createReducer(initialState, (builder) =>
|
||||
builder.addMatcher(incrementBy.match, (state) => {
|
||||
state.numberActions += 100
|
||||
})
|
||||
)
|
||||
expect(reducer(undefined, incrementBy(1))).toEqual({
|
||||
numberActions: 100,
|
||||
stringActions: 0,
|
||||
})
|
||||
})
|
||||
test('calling addCase, addMatcher and addDefaultCase in a nonsensical order should result in an error in development mode', () => {
|
||||
expect(() =>
|
||||
createReducer(initialState, (builder: any) =>
|
||||
builder
|
||||
.addMatcher(numberActionMatcher, () => {})
|
||||
.addCase(incrementBy, () => {})
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"\`builder.addCase\` should only be called before calling \`builder.addMatcher\`"`
|
||||
)
|
||||
expect(() =>
|
||||
createReducer(initialState, (builder: any) =>
|
||||
builder.addDefaultCase(() => {}).addCase(incrementBy, () => {})
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"\`builder.addCase\` should only be called before calling \`builder.addDefaultCase\`"`
|
||||
)
|
||||
expect(() =>
|
||||
createReducer(initialState, (builder: any) =>
|
||||
builder
|
||||
.addDefaultCase(() => {})
|
||||
.addMatcher(numberActionMatcher, () => {})
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"\`builder.addMatcher\` should only be called before calling \`builder.addDefaultCase\`"`
|
||||
)
|
||||
expect(() =>
|
||||
createReducer(initialState, (builder: any) =>
|
||||
builder.addDefaultCase(() => {}).addDefaultCase(() => {})
|
||||
)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"\`builder.addDefaultCase\` can only be called once"`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function behavesLikeReducer(todosReducer: TodosReducer) {
|
||||
it('should handle initial state', () => {
|
||||
const initialAction = { type: '', payload: undefined }
|
||||
expect(todosReducer(undefined, initialAction)).toEqual([])
|
||||
})
|
||||
|
||||
it('should handle ADD_TODO', () => {
|
||||
expect(
|
||||
todosReducer([], {
|
||||
type: 'ADD_TODO',
|
||||
payload: { newTodo: { text: 'Run the tests' } },
|
||||
})
|
||||
).toEqual([
|
||||
{
|
||||
text: 'Run the tests',
|
||||
completed: false,
|
||||
},
|
||||
])
|
||||
|
||||
expect(
|
||||
todosReducer(
|
||||
[
|
||||
{
|
||||
text: 'Run the tests',
|
||||
completed: false,
|
||||
},
|
||||
],
|
||||
{
|
||||
type: 'ADD_TODO',
|
||||
payload: { newTodo: { text: 'Use Redux' } },
|
||||
}
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
text: 'Run the tests',
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
text: 'Use Redux',
|
||||
completed: false,
|
||||
},
|
||||
])
|
||||
|
||||
expect(
|
||||
todosReducer(
|
||||
[
|
||||
{
|
||||
text: 'Run the tests',
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
text: 'Use Redux',
|
||||
completed: false,
|
||||
},
|
||||
],
|
||||
{
|
||||
type: 'ADD_TODO',
|
||||
payload: { newTodo: { text: 'Fix the tests' } },
|
||||
}
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
text: 'Run the tests',
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
text: 'Use Redux',
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
text: 'Fix the tests',
|
||||
completed: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should handle TOGGLE_TODO', () => {
|
||||
expect(
|
||||
todosReducer(
|
||||
[
|
||||
{
|
||||
text: 'Run the tests',
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
text: 'Use Redux',
|
||||
completed: false,
|
||||
},
|
||||
],
|
||||
{
|
||||
type: 'TOGGLE_TODO',
|
||||
payload: { index: 0 },
|
||||
}
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
text: 'Run the tests',
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
text: 'Use Redux',
|
||||
completed: false,
|
||||
},
|
||||
])
|
||||
})
|
||||
}
|
||||
78
node_modules/@reduxjs/toolkit/src/tests/createReducer.typetest.ts
generated
vendored
Normal file
78
node_modules/@reduxjs/toolkit/src/tests/createReducer.typetest.ts
generated
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { Reducer } from 'redux'
|
||||
import type { ActionReducerMapBuilder } from '@reduxjs/toolkit'
|
||||
import { createReducer, createAction } from '@reduxjs/toolkit'
|
||||
import { expectType } from './helpers'
|
||||
|
||||
/*
|
||||
* Test: createReducer() infers type of returned reducer.
|
||||
*/
|
||||
{
|
||||
type CounterAction =
|
||||
| { type: 'increment'; payload: number }
|
||||
| { type: 'decrement'; payload: number }
|
||||
|
||||
const incrementHandler = (state: number, action: CounterAction) => state + 1
|
||||
const decrementHandler = (state: number, action: CounterAction) => state - 1
|
||||
|
||||
const reducer = createReducer(0 as number, {
|
||||
increment: incrementHandler,
|
||||
decrement: decrementHandler,
|
||||
})
|
||||
|
||||
const numberReducer: Reducer<number> = reducer
|
||||
|
||||
// @ts-expect-error
|
||||
const stringReducer: Reducer<string> = reducer
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: createReducer() state type can be specified expliclity.
|
||||
*/
|
||||
{
|
||||
type CounterAction =
|
||||
| { type: 'increment'; payload: number }
|
||||
| { type: 'decrement'; payload: number }
|
||||
|
||||
const incrementHandler = (state: number, action: CounterAction) =>
|
||||
state + action.payload
|
||||
|
||||
const decrementHandler = (state: number, action: CounterAction) =>
|
||||
state - action.payload
|
||||
|
||||
createReducer<number>(0, {
|
||||
increment: incrementHandler,
|
||||
decrement: decrementHandler,
|
||||
})
|
||||
|
||||
// @ts-expect-error
|
||||
createReducer<string>(0, {
|
||||
increment: incrementHandler,
|
||||
decrement: decrementHandler,
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: createReducer() ensures state type is mutable within a case reducer.
|
||||
*/
|
||||
{
|
||||
const initialState: { readonly counter: number } = { counter: 0 }
|
||||
|
||||
createReducer(initialState, {
|
||||
increment: (state) => {
|
||||
state.counter += 1
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** Test: alternative builder callback for actionMap */
|
||||
{
|
||||
const increment = createAction<number, 'increment'>('increment')
|
||||
|
||||
const reducer = createReducer(0, (builder) =>
|
||||
expectType<ActionReducerMapBuilder<number>>(builder)
|
||||
)
|
||||
|
||||
expectType<number>(reducer(0, increment(5)))
|
||||
// @ts-expect-error
|
||||
expectType<string>(reducer(0, increment(5)))
|
||||
}
|
||||
434
node_modules/@reduxjs/toolkit/src/tests/createSlice.test.ts
generated
vendored
Normal file
434
node_modules/@reduxjs/toolkit/src/tests/createSlice.test.ts
generated
vendored
Normal file
@@ -0,0 +1,434 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||
import { createSlice, createAction } from '@reduxjs/toolkit'
|
||||
import {
|
||||
mockConsole,
|
||||
createConsole,
|
||||
getLog,
|
||||
} from 'console-testing-library/pure'
|
||||
|
||||
type CreateSlice = typeof createSlice
|
||||
|
||||
describe('createSlice', () => {
|
||||
let restore: () => void
|
||||
|
||||
beforeEach(() => {
|
||||
restore = mockConsole(createConsole())
|
||||
})
|
||||
|
||||
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', () => {
|
||||
it('should throw an error', () => {
|
||||
createSlice({
|
||||
name: 'test',
|
||||
reducers: {},
|
||||
initialState: undefined,
|
||||
})
|
||||
|
||||
expect(getLog().log).toBe(
|
||||
'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: {
|
||||
[addMore.type]: (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('alternative 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(
|
||||
`"addCase cannot be called with two reducers for the same action type"`
|
||||
)
|
||||
})
|
||||
|
||||
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.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('behaviour with enhanced case reducers', () => {
|
||||
it('should pass all arguments to the prepare function', () => {
|
||||
const prepare = jest.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 = jest.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.only('Deprecation warnings', () => {
|
||||
let originalNodeEnv = process.env.NODE_ENV
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
restore = mockConsole(createConsole())
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
process.env.NODE_ENV = originalNodeEnv
|
||||
})
|
||||
|
||||
// NOTE: This needs to be in front of the later `createReducer` call to check the one-time warning
|
||||
it('Warns about object notation deprecation, once', () => {
|
||||
const { createSlice } = require('../createSlice')
|
||||
|
||||
let dummySlice = (createSlice as CreateSlice)({
|
||||
name: 'dummy',
|
||||
initialState: [],
|
||||
reducers: {},
|
||||
extraReducers: {
|
||||
a: () => [],
|
||||
},
|
||||
})
|
||||
// Have to trigger the lazy creation
|
||||
let { reducer } = dummySlice
|
||||
reducer(undefined, { type: 'dummy' })
|
||||
|
||||
expect(getLog().levels.warn).toMatch(
|
||||
/The object notation for `createSlice.extraReducers` is deprecated/
|
||||
)
|
||||
restore = mockConsole(createConsole())
|
||||
|
||||
dummySlice = (createSlice as CreateSlice)({
|
||||
name: 'dummy',
|
||||
initialState: [],
|
||||
reducers: {},
|
||||
extraReducers: {
|
||||
a: () => [],
|
||||
},
|
||||
})
|
||||
reducer = dummySlice.reducer
|
||||
reducer(undefined, { type: 'dummy' })
|
||||
expect(getLog().levels.warn).toBe('')
|
||||
})
|
||||
|
||||
it('Does not warn in production', () => {
|
||||
process.env.NODE_ENV = 'production'
|
||||
const { createSlice } = require('../createSlice')
|
||||
|
||||
let dummySlice = (createSlice as CreateSlice)({
|
||||
name: 'dummy',
|
||||
initialState: [],
|
||||
reducers: {},
|
||||
extraReducers: {},
|
||||
})
|
||||
expect(getLog().levels.warn).toBe('')
|
||||
})
|
||||
})
|
||||
})
|
||||
501
node_modules/@reduxjs/toolkit/src/tests/createSlice.typetest.ts
generated
vendored
Normal file
501
node_modules/@reduxjs/toolkit/src/tests/createSlice.typetest.ts
generated
vendored
Normal file
@@ -0,0 +1,501 @@
|
||||
import type { Action, AnyAction, Reducer } from 'redux'
|
||||
import type {
|
||||
ActionCreatorWithNonInferrablePayload,
|
||||
ActionCreatorWithOptionalPayload,
|
||||
ActionCreatorWithoutPayload,
|
||||
ActionCreatorWithPayload,
|
||||
ActionCreatorWithPreparedPayload,
|
||||
ActionReducerMapBuilder,
|
||||
PayloadAction,
|
||||
SliceCaseReducers,
|
||||
ValidateSliceCaseReducers,
|
||||
} from '@reduxjs/toolkit'
|
||||
import { createAction, createSlice } from '@reduxjs/toolkit'
|
||||
import { expectType } from './helpers'
|
||||
|
||||
/*
|
||||
* Test: Slice name is strongly typed.
|
||||
*/
|
||||
|
||||
const counterSlice = createSlice({
|
||||
name: 'counter',
|
||||
initialState: 0,
|
||||
reducers: {
|
||||
increment: (state: number, action) => state + action.payload,
|
||||
decrement: (state: number, action) => state - action.payload,
|
||||
},
|
||||
})
|
||||
|
||||
const uiSlice = createSlice({
|
||||
name: 'ui',
|
||||
initialState: 0,
|
||||
reducers: {
|
||||
goToNext: (state: number, action) => state + action.payload,
|
||||
goToPrevious: (state: number, action) => state - action.payload,
|
||||
},
|
||||
})
|
||||
|
||||
const actionCreators = {
|
||||
[counterSlice.name]: { ...counterSlice.actions },
|
||||
[uiSlice.name]: { ...uiSlice.actions },
|
||||
}
|
||||
|
||||
expectType<typeof counterSlice.actions>(actionCreators.counter)
|
||||
expectType<typeof uiSlice.actions>(actionCreators.ui)
|
||||
|
||||
// @ts-expect-error
|
||||
const value = actionCreators.anyKey
|
||||
|
||||
/*
|
||||
* Test: createSlice() infers the returned slice's type.
|
||||
*/
|
||||
{
|
||||
const firstAction = createAction<{ count: number }>('FIRST_ACTION')
|
||||
|
||||
const slice = createSlice({
|
||||
name: 'counter',
|
||||
initialState: 0,
|
||||
reducers: {
|
||||
increment: (state: number, action) => state + action.payload,
|
||||
decrement: (state: number, action) => state - action.payload,
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(
|
||||
firstAction,
|
||||
(state, action) => state + action.payload.count
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
/* Reducer */
|
||||
|
||||
expectType<Reducer<number, PayloadAction>>(slice.reducer)
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<Reducer<string, PayloadAction>>(slice.reducer)
|
||||
// @ts-expect-error
|
||||
expectType<Reducer<string, AnyAction>>(slice.reducer)
|
||||
/* Actions */
|
||||
|
||||
slice.actions.increment(1)
|
||||
slice.actions.decrement(1)
|
||||
|
||||
// @ts-expect-error
|
||||
slice.actions.other(1)
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: Slice action creator types are inferred.
|
||||
*/
|
||||
{
|
||||
const counter = createSlice({
|
||||
name: 'counter',
|
||||
initialState: 0,
|
||||
reducers: {
|
||||
increment: (state) => state + 1,
|
||||
decrement: (state, { payload = 1 }: PayloadAction<number | undefined>) =>
|
||||
state - payload,
|
||||
multiply: (state, { payload }: PayloadAction<number | number[]>) =>
|
||||
Array.isArray(payload)
|
||||
? payload.reduce((acc, val) => acc * val, state)
|
||||
: state * payload,
|
||||
addTwo: {
|
||||
reducer: (s, { payload }: PayloadAction<number>) => s + payload,
|
||||
prepare: (a: number, b: number) => ({
|
||||
payload: a + b,
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expectType<ActionCreatorWithoutPayload>(counter.actions.increment)
|
||||
counter.actions.increment()
|
||||
|
||||
expectType<ActionCreatorWithOptionalPayload<number | undefined>>(
|
||||
counter.actions.decrement
|
||||
)
|
||||
counter.actions.decrement()
|
||||
counter.actions.decrement(2)
|
||||
|
||||
expectType<ActionCreatorWithPayload<number | number[]>>(
|
||||
counter.actions.multiply
|
||||
)
|
||||
counter.actions.multiply(2)
|
||||
counter.actions.multiply([2, 3, 4])
|
||||
|
||||
expectType<ActionCreatorWithPreparedPayload<[number, number], number>>(
|
||||
counter.actions.addTwo
|
||||
)
|
||||
counter.actions.addTwo(1, 2)
|
||||
|
||||
// @ts-expect-error
|
||||
counter.actions.multiply()
|
||||
|
||||
// @ts-expect-error
|
||||
counter.actions.multiply('2')
|
||||
|
||||
// @ts-expect-error
|
||||
counter.actions.addTwo(1)
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: Slice action creator types properties are "string"
|
||||
*/
|
||||
{
|
||||
const counter = createSlice({
|
||||
name: 'counter',
|
||||
initialState: 0,
|
||||
reducers: {
|
||||
increment: (state) => state + 1,
|
||||
decrement: (state) => state - 1,
|
||||
multiply: (state, { payload }: PayloadAction<number | number[]>) =>
|
||||
Array.isArray(payload)
|
||||
? payload.reduce((acc, val) => acc * val, state)
|
||||
: state * payload,
|
||||
},
|
||||
})
|
||||
|
||||
const s: 'counter/increment' = counter.actions.increment.type
|
||||
const sa: 'counter/increment' = counter.actions.increment().type
|
||||
const t: 'counter/decrement' = counter.actions.decrement.type
|
||||
const ta: 'counter/decrement' = counter.actions.decrement().type
|
||||
const u: 'counter/multiply' = counter.actions.multiply.type
|
||||
const ua: 'counter/multiply' = counter.actions.multiply(1).type
|
||||
|
||||
// @ts-expect-error
|
||||
const y: 'increment' = counter.actions.increment.type
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: Slice action creator types are inferred for enhanced reducers.
|
||||
*/
|
||||
{
|
||||
const counter = createSlice({
|
||||
name: 'test',
|
||||
initialState: { counter: 0, concat: '' },
|
||||
reducers: {
|
||||
incrementByStrLen: {
|
||||
reducer: (state, action: PayloadAction<number>) => {
|
||||
state.counter += action.payload
|
||||
},
|
||||
prepare: (payload: string) => ({
|
||||
payload: payload.length,
|
||||
}),
|
||||
},
|
||||
concatMetaStrLen: {
|
||||
reducer: (state, action: PayloadAction<string>) => {
|
||||
state.concat += action.payload
|
||||
},
|
||||
prepare: (payload: string) => ({
|
||||
payload,
|
||||
meta: payload.length,
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expectType<'test/incrementByStrLen'>(
|
||||
counter.actions.incrementByStrLen('test').type
|
||||
)
|
||||
expectType<number>(counter.actions.incrementByStrLen('test').payload)
|
||||
expectType<string>(counter.actions.concatMetaStrLen('test').payload)
|
||||
expectType<number>(counter.actions.concatMetaStrLen('test').meta)
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<string>(counter.actions.incrementByStrLen('test').payload)
|
||||
|
||||
// @ts-expect-error
|
||||
expectType<string>(counter.actions.concatMetaStrLen('test').meta)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: access meta and error from reducer
|
||||
*/
|
||||
{
|
||||
const counter = createSlice({
|
||||
name: 'test',
|
||||
initialState: { counter: 0, concat: '' },
|
||||
reducers: {
|
||||
// case: meta and error not used in reducer
|
||||
testDefaultMetaAndError: {
|
||||
reducer(_, action: PayloadAction<number, string>) {},
|
||||
prepare: (payload: number) => ({
|
||||
payload,
|
||||
meta: 'meta' as 'meta',
|
||||
error: 'error' as 'error',
|
||||
}),
|
||||
},
|
||||
// case: meta and error marked as "unknown" in reducer
|
||||
testUnknownMetaAndError: {
|
||||
reducer(_, action: PayloadAction<number, string, unknown, unknown>) {},
|
||||
prepare: (payload: number) => ({
|
||||
payload,
|
||||
meta: 'meta' as 'meta',
|
||||
error: 'error' as 'error',
|
||||
}),
|
||||
},
|
||||
// case: meta and error are typed in the reducer as returned by prepare
|
||||
testMetaAndError: {
|
||||
reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {},
|
||||
prepare: (payload: number) => ({
|
||||
payload,
|
||||
meta: 'meta' as 'meta',
|
||||
error: 'error' as 'error',
|
||||
}),
|
||||
},
|
||||
// case: meta is typed differently in the reducer than returned from prepare
|
||||
testErroneousMeta: {
|
||||
reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {},
|
||||
// @ts-expect-error
|
||||
prepare: (payload: number) => ({
|
||||
payload,
|
||||
meta: 1,
|
||||
error: 'error' as 'error',
|
||||
}),
|
||||
},
|
||||
// case: error is typed differently in the reducer than returned from prepare
|
||||
testErroneousError: {
|
||||
reducer(_, action: PayloadAction<number, string, 'meta', 'error'>) {},
|
||||
// @ts-expect-error
|
||||
prepare: (payload: number) => ({
|
||||
payload,
|
||||
meta: 'meta' as 'meta',
|
||||
error: 1,
|
||||
}),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: returned case reducer has the correct type
|
||||
*/
|
||||
{
|
||||
const counter = createSlice({
|
||||
name: 'counter',
|
||||
initialState: 0,
|
||||
reducers: {
|
||||
increment(state, action: PayloadAction<number>) {
|
||||
return state + action.payload
|
||||
},
|
||||
decrement: {
|
||||
reducer(state, action: PayloadAction<number>) {
|
||||
return state - action.payload
|
||||
},
|
||||
prepare(amount: number) {
|
||||
return { payload: amount }
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Should match positively
|
||||
expectType<(state: number, action: PayloadAction<number>) => number | void>(
|
||||
counter.caseReducers.increment
|
||||
)
|
||||
|
||||
// Should match positively for reducers with prepare callback
|
||||
expectType<(state: number, action: PayloadAction<number>) => number | void>(
|
||||
counter.caseReducers.decrement
|
||||
)
|
||||
|
||||
// Should not mismatch the payload if it's a simple reducer
|
||||
|
||||
expectType<(state: number, action: PayloadAction<string>) => number | void>(
|
||||
// @ts-expect-error
|
||||
counter.caseReducers.increment
|
||||
)
|
||||
|
||||
// Should not mismatch the payload if it's a reducer with a prepare callback
|
||||
|
||||
expectType<(state: number, action: PayloadAction<string>) => number | void>(
|
||||
// @ts-expect-error
|
||||
counter.caseReducers.decrement
|
||||
)
|
||||
|
||||
// Should not include entries that don't exist
|
||||
|
||||
expectType<(state: number, action: PayloadAction<string>) => number | void>(
|
||||
// @ts-expect-error
|
||||
counter.caseReducers.someThingNonExistant
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: prepared payload does not match action payload - should cause an error.
|
||||
*/
|
||||
{
|
||||
const counter = createSlice({
|
||||
name: 'counter',
|
||||
initialState: { counter: 0 },
|
||||
reducers: {
|
||||
increment: {
|
||||
reducer(state, action: PayloadAction<string>) {
|
||||
state.counter += action.payload.length
|
||||
},
|
||||
// @ts-expect-error
|
||||
prepare(x: string) {
|
||||
return {
|
||||
payload: 6,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: if no Payload Type is specified, accept any payload
|
||||
* see https://github.com/reduxjs/redux-toolkit/issues/165
|
||||
*/
|
||||
{
|
||||
const initialState = {
|
||||
name: null,
|
||||
}
|
||||
|
||||
const mySlice = createSlice({
|
||||
name: 'name',
|
||||
initialState,
|
||||
reducers: {
|
||||
setName: (state, action) => {
|
||||
state.name = action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expectType<ActionCreatorWithNonInferrablePayload>(mySlice.actions.setName)
|
||||
|
||||
const x = mySlice.actions.setName
|
||||
|
||||
mySlice.actions.setName(null)
|
||||
mySlice.actions.setName('asd')
|
||||
mySlice.actions.setName(5)
|
||||
}
|
||||
|
||||
/**
|
||||
* Test: actions.x.match()
|
||||
*/
|
||||
{
|
||||
const mySlice = createSlice({
|
||||
name: 'name',
|
||||
initialState: { name: 'test' },
|
||||
reducers: {
|
||||
setName: (state, action: PayloadAction<string>) => {
|
||||
state.name = action.payload
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const x: Action<unknown> = {} as any
|
||||
if (mySlice.actions.setName.match(x)) {
|
||||
expectType<'name/setName'>(x.type)
|
||||
expectType<string>(x.payload)
|
||||
} else {
|
||||
// @ts-expect-error
|
||||
expectType<string>(x.type)
|
||||
// @ts-expect-error
|
||||
expectType<string>(x.payload)
|
||||
}
|
||||
}
|
||||
|
||||
/** Test: alternative builder callback for extraReducers */
|
||||
{
|
||||
createSlice({
|
||||
name: 'test',
|
||||
initialState: 0,
|
||||
reducers: {},
|
||||
extraReducers: (builder) => {
|
||||
expectType<ActionReducerMapBuilder<number>>(builder)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** Test: wrapping createSlice should be possible */
|
||||
{
|
||||
interface GenericState<T> {
|
||||
data?: T
|
||||
status: 'loading' | 'finished' | 'error'
|
||||
}
|
||||
|
||||
const createGenericSlice = <
|
||||
T,
|
||||
Reducers extends SliceCaseReducers<GenericState<T>>
|
||||
>({
|
||||
name = '',
|
||||
initialState,
|
||||
reducers,
|
||||
}: {
|
||||
name: string
|
||||
initialState: GenericState<T>
|
||||
reducers: ValidateSliceCaseReducers<GenericState<T>, Reducers>
|
||||
}) => {
|
||||
return createSlice({
|
||||
name,
|
||||
initialState,
|
||||
reducers: {
|
||||
start(state) {
|
||||
state.status = 'loading'
|
||||
},
|
||||
success(state: GenericState<T>, action: PayloadAction<T>) {
|
||||
state.data = action.payload
|
||||
state.status = 'finished'
|
||||
},
|
||||
...reducers,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const wrappedSlice = createGenericSlice({
|
||||
name: 'test',
|
||||
initialState: { status: 'loading' } as GenericState<string>,
|
||||
reducers: {
|
||||
magic(state) {
|
||||
expectType<GenericState<string>>(state)
|
||||
// @ts-expect-error
|
||||
expectType<GenericState<number>>(state)
|
||||
|
||||
state.status = 'finished'
|
||||
state.data = 'hocus pocus'
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expectType<ActionCreatorWithPayload<string>>(wrappedSlice.actions.success)
|
||||
expectType<ActionCreatorWithoutPayload<string>>(wrappedSlice.actions.magic)
|
||||
}
|
||||
|
||||
{
|
||||
interface GenericState<T> {
|
||||
data: T | null
|
||||
}
|
||||
|
||||
function createDataSlice<
|
||||
T,
|
||||
Reducers extends SliceCaseReducers<GenericState<T>>
|
||||
>(
|
||||
name: string,
|
||||
reducers: ValidateSliceCaseReducers<GenericState<T>, Reducers>,
|
||||
initialState: GenericState<T>
|
||||
) {
|
||||
const doNothing = createAction<undefined>('doNothing')
|
||||
const setData = createAction<T>('setData')
|
||||
|
||||
const slice = createSlice({
|
||||
name,
|
||||
initialState,
|
||||
reducers,
|
||||
extraReducers: (builder) => {
|
||||
builder.addCase(doNothing, (state) => {
|
||||
return { ...state }
|
||||
})
|
||||
builder.addCase(setData, (state, { payload }) => {
|
||||
return {
|
||||
...state,
|
||||
data: payload,
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
return { doNothing, setData, slice }
|
||||
}
|
||||
}
|
||||
322
node_modules/@reduxjs/toolkit/src/tests/getDefaultMiddleware.test.ts
generated
vendored
Normal file
322
node_modules/@reduxjs/toolkit/src/tests/getDefaultMiddleware.test.ts
generated
vendored
Normal file
@@ -0,0 +1,322 @@
|
||||
import type {
|
||||
AnyAction,
|
||||
Middleware,
|
||||
ThunkAction,
|
||||
Action,
|
||||
ThunkDispatch,
|
||||
Dispatch,
|
||||
} from '@reduxjs/toolkit'
|
||||
import {
|
||||
getDefaultMiddleware,
|
||||
MiddlewareArray,
|
||||
configureStore,
|
||||
} from '@reduxjs/toolkit'
|
||||
import thunk from 'redux-thunk'
|
||||
import type { ThunkMiddleware } from 'redux-thunk'
|
||||
|
||||
import { expectType } from './helpers'
|
||||
import { BaseActionCreator } from '@internal/createAction'
|
||||
|
||||
describe('getDefaultMiddleware', () => {
|
||||
const ORIGINAL_NODE_ENV = process.env.NODE_ENV
|
||||
|
||||
afterEach(() => {
|
||||
process.env.NODE_ENV = ORIGINAL_NODE_ENV
|
||||
})
|
||||
|
||||
it('returns an array with only redux-thunk in production', () => {
|
||||
process.env.NODE_ENV = 'production'
|
||||
|
||||
expect(getDefaultMiddleware()).toEqual([thunk]) // @remap-prod-remove-line
|
||||
})
|
||||
|
||||
it('returns an array with additional middleware in development', () => {
|
||||
const middleware = getDefaultMiddleware()
|
||||
expect(middleware).toContain(thunk)
|
||||
expect(middleware.length).toBeGreaterThan(1)
|
||||
})
|
||||
|
||||
const defaultMiddleware = getDefaultMiddleware()
|
||||
|
||||
it('removes the thunk middleware if disabled', () => {
|
||||
const middleware = getDefaultMiddleware({ thunk: false })
|
||||
// @ts-ignore
|
||||
expect(middleware.includes(thunk)).toBe(false)
|
||||
expect(middleware.length).toBe(defaultMiddleware.length - 1)
|
||||
})
|
||||
|
||||
it('removes the immutable middleware if disabled', () => {
|
||||
const middleware = getDefaultMiddleware({ immutableCheck: false })
|
||||
expect(middleware.length).toBe(defaultMiddleware.length - 1)
|
||||
})
|
||||
|
||||
it('removes the serializable middleware if disabled', () => {
|
||||
const middleware = getDefaultMiddleware({ serializableCheck: false })
|
||||
expect(middleware.length).toBe(defaultMiddleware.length - 1)
|
||||
})
|
||||
|
||||
it('removes the action creator middleware if disabled', () => {
|
||||
const middleware = getDefaultMiddleware({ actionCreatorCheck: false })
|
||||
expect(middleware.length).toBe(defaultMiddleware.length - 1)
|
||||
})
|
||||
|
||||
it('allows passing options to thunk', () => {
|
||||
const extraArgument = 42 as const
|
||||
const middleware = getDefaultMiddleware({
|
||||
thunk: { extraArgument },
|
||||
immutableCheck: false,
|
||||
serializableCheck: false,
|
||||
actionCreatorCheck: false,
|
||||
})
|
||||
|
||||
const m2 = getDefaultMiddleware({
|
||||
thunk: false,
|
||||
})
|
||||
|
||||
expectType<MiddlewareArray<[]>>(m2)
|
||||
|
||||
const dummyMiddleware: Middleware<
|
||||
{
|
||||
(action: Action<'actionListenerMiddleware/add'>): () => void
|
||||
},
|
||||
{ counter: number }
|
||||
> = (storeApi) => (next) => (action) => {}
|
||||
|
||||
const dummyMiddleware2: Middleware = (storeApi) => (next) => (action) => {}
|
||||
|
||||
const m3 = middleware.concat(dummyMiddleware, dummyMiddleware2)
|
||||
|
||||
expectType<
|
||||
MiddlewareArray<
|
||||
[
|
||||
ThunkMiddleware<any, AnyAction, 42>,
|
||||
Middleware<
|
||||
(action: Action<'actionListenerMiddleware/add'>) => () => void,
|
||||
{
|
||||
counter: number
|
||||
},
|
||||
Dispatch<AnyAction>
|
||||
>,
|
||||
Middleware<{}, any, Dispatch<AnyAction>>
|
||||
]
|
||||
>
|
||||
>(m3)
|
||||
|
||||
const testThunk: ThunkAction<void, {}, number, AnyAction> = (
|
||||
dispatch,
|
||||
getState,
|
||||
extraArg
|
||||
) => {
|
||||
expect(extraArg).toBe(extraArgument)
|
||||
}
|
||||
|
||||
const reducer = () => ({})
|
||||
|
||||
const store = configureStore({
|
||||
reducer,
|
||||
middleware,
|
||||
})
|
||||
|
||||
expectType<ThunkDispatch<any, 42, AnyAction> & Dispatch<AnyAction>>(
|
||||
store.dispatch
|
||||
)
|
||||
|
||||
store.dispatch(testThunk)
|
||||
})
|
||||
|
||||
it('allows passing options to immutableCheck', () => {
|
||||
let immutableCheckWasCalled = false
|
||||
|
||||
const middleware = getDefaultMiddleware({
|
||||
thunk: false,
|
||||
immutableCheck: {
|
||||
isImmutable: () => {
|
||||
immutableCheckWasCalled = true
|
||||
return true
|
||||
},
|
||||
},
|
||||
serializableCheck: false,
|
||||
actionCreatorCheck: false,
|
||||
})
|
||||
|
||||
const reducer = () => ({})
|
||||
|
||||
const store = configureStore({
|
||||
reducer,
|
||||
middleware,
|
||||
})
|
||||
|
||||
expect(immutableCheckWasCalled).toBe(true)
|
||||
})
|
||||
|
||||
it('allows passing options to serializableCheck', () => {
|
||||
let serializableCheckWasCalled = false
|
||||
|
||||
const middleware = getDefaultMiddleware({
|
||||
thunk: false,
|
||||
immutableCheck: false,
|
||||
serializableCheck: {
|
||||
isSerializable: () => {
|
||||
serializableCheckWasCalled = true
|
||||
return true
|
||||
},
|
||||
},
|
||||
actionCreatorCheck: false,
|
||||
})
|
||||
|
||||
const reducer = () => ({})
|
||||
|
||||
const store = configureStore({
|
||||
reducer,
|
||||
middleware,
|
||||
})
|
||||
|
||||
store.dispatch({ type: 'TEST_ACTION' })
|
||||
|
||||
expect(serializableCheckWasCalled).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it('allows passing options to actionCreatorCheck', () => {
|
||||
let actionCreatorCheckWasCalled = false
|
||||
|
||||
const middleware = getDefaultMiddleware({
|
||||
thunk: false,
|
||||
immutableCheck: false,
|
||||
serializableCheck: false,
|
||||
actionCreatorCheck: {
|
||||
isActionCreator: (action: unknown): action is Function => {
|
||||
actionCreatorCheckWasCalled = true
|
||||
return false
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const reducer = () => ({})
|
||||
|
||||
const store = configureStore({
|
||||
reducer,
|
||||
middleware,
|
||||
})
|
||||
|
||||
store.dispatch({ type: 'TEST_ACTION' })
|
||||
|
||||
expect(actionCreatorCheckWasCalled).toBe(true)
|
||||
})
|
||||
|
||||
describe('MiddlewareArray functionality', () => {
|
||||
const middleware1: Middleware = () => (next) => (action) => next(action)
|
||||
const middleware2: Middleware = () => (next) => (action) => next(action)
|
||||
const defaultMiddleware = getDefaultMiddleware()
|
||||
const originalDefaultMiddleware = [...defaultMiddleware]
|
||||
|
||||
test('allows to prepend a single value', () => {
|
||||
const prepended = defaultMiddleware.prepend(middleware1)
|
||||
|
||||
// value is prepended
|
||||
expect(prepended).toEqual([middleware1, ...defaultMiddleware])
|
||||
// returned value is of correct type
|
||||
expect(prepended).toBeInstanceOf(MiddlewareArray)
|
||||
// prepended is a new array
|
||||
expect(prepended).not.toEqual(defaultMiddleware)
|
||||
// defaultMiddleware is not modified
|
||||
expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
|
||||
})
|
||||
|
||||
test('allows to prepend multiple values (array as first argument)', () => {
|
||||
const prepended = defaultMiddleware.prepend([middleware1, middleware2])
|
||||
|
||||
// value is prepended
|
||||
expect(prepended).toEqual([middleware1, middleware2, ...defaultMiddleware])
|
||||
// returned value is of correct type
|
||||
expect(prepended).toBeInstanceOf(MiddlewareArray)
|
||||
// prepended is a new array
|
||||
expect(prepended).not.toEqual(defaultMiddleware)
|
||||
// defaultMiddleware is not modified
|
||||
expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
|
||||
})
|
||||
|
||||
test('allows to prepend multiple values (rest)', () => {
|
||||
const prepended = defaultMiddleware.prepend(middleware1, middleware2)
|
||||
|
||||
// value is prepended
|
||||
expect(prepended).toEqual([middleware1, middleware2, ...defaultMiddleware])
|
||||
// returned value is of correct type
|
||||
expect(prepended).toBeInstanceOf(MiddlewareArray)
|
||||
// prepended is a new array
|
||||
expect(prepended).not.toEqual(defaultMiddleware)
|
||||
// defaultMiddleware is not modified
|
||||
expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
|
||||
})
|
||||
|
||||
test('allows to concat a single value', () => {
|
||||
const concatenated = defaultMiddleware.concat(middleware1)
|
||||
|
||||
// value is concatenated
|
||||
expect(concatenated).toEqual([...defaultMiddleware, middleware1])
|
||||
// returned value is of correct type
|
||||
expect(concatenated).toBeInstanceOf(MiddlewareArray)
|
||||
// concatenated is a new array
|
||||
expect(concatenated).not.toEqual(defaultMiddleware)
|
||||
// defaultMiddleware is not modified
|
||||
expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
|
||||
})
|
||||
|
||||
test('allows to concat multiple values (array as first argument)', () => {
|
||||
const concatenated = defaultMiddleware.concat([middleware1, middleware2])
|
||||
|
||||
// value is concatenated
|
||||
expect(concatenated).toEqual([
|
||||
...defaultMiddleware,
|
||||
middleware1,
|
||||
middleware2,
|
||||
])
|
||||
// returned value is of correct type
|
||||
expect(concatenated).toBeInstanceOf(MiddlewareArray)
|
||||
// concatenated is a new array
|
||||
expect(concatenated).not.toEqual(defaultMiddleware)
|
||||
// defaultMiddleware is not modified
|
||||
expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
|
||||
})
|
||||
|
||||
test('allows to concat multiple values (rest)', () => {
|
||||
const concatenated = defaultMiddleware.concat(middleware1, middleware2)
|
||||
|
||||
// value is concatenated
|
||||
expect(concatenated).toEqual([
|
||||
...defaultMiddleware,
|
||||
middleware1,
|
||||
middleware2,
|
||||
])
|
||||
// returned value is of correct type
|
||||
expect(concatenated).toBeInstanceOf(MiddlewareArray)
|
||||
// concatenated is a new array
|
||||
expect(concatenated).not.toEqual(defaultMiddleware)
|
||||
// defaultMiddleware is not modified
|
||||
expect(defaultMiddleware).toEqual(originalDefaultMiddleware)
|
||||
})
|
||||
|
||||
test('allows to concat and then prepend', () => {
|
||||
const concatenated = defaultMiddleware
|
||||
.concat(middleware1)
|
||||
.prepend(middleware2)
|
||||
|
||||
expect(concatenated).toEqual([
|
||||
middleware2,
|
||||
...defaultMiddleware,
|
||||
middleware1,
|
||||
])
|
||||
})
|
||||
|
||||
test('allows to prepend and then concat', () => {
|
||||
const concatenated = defaultMiddleware
|
||||
.prepend(middleware2)
|
||||
.concat(middleware1)
|
||||
|
||||
expect(concatenated).toEqual([
|
||||
middleware2,
|
||||
...defaultMiddleware,
|
||||
middleware1,
|
||||
])
|
||||
})
|
||||
})
|
||||
48
node_modules/@reduxjs/toolkit/src/tests/helpers.ts
generated
vendored
Normal file
48
node_modules/@reduxjs/toolkit/src/tests/helpers.ts
generated
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
import type { IsAny, IsUnknown } from '../../src/tsHelpers'
|
||||
|
||||
export function expectType<T>(t: T): T {
|
||||
return t
|
||||
}
|
||||
|
||||
type Equals<T, U> = IsAny<
|
||||
T,
|
||||
never,
|
||||
IsAny<U, never, [T] extends [U] ? ([U] extends [T] ? any : never) : never>
|
||||
>
|
||||
export function expectExactType<T>(t: T) {
|
||||
return <U extends Equals<T, U>>(u: U) => {}
|
||||
}
|
||||
|
||||
type EnsureUnknown<T extends any> = IsUnknown<T, any, never>
|
||||
export function expectUnknown<T extends EnsureUnknown<T>>(t: T) {
|
||||
return t
|
||||
}
|
||||
|
||||
type EnsureAny<T extends any> = IsAny<T, any, never>
|
||||
export function expectExactAny<T extends EnsureAny<T>>(t: T) {
|
||||
return t
|
||||
}
|
||||
|
||||
type IsNotAny<T> = IsAny<T, never, any>
|
||||
export function expectNotAny<T extends IsNotAny<T>>(t: T): T {
|
||||
return t
|
||||
}
|
||||
|
||||
expectType<string>('5' as string)
|
||||
expectType<string>('5' as const)
|
||||
expectType<string>('5' as any)
|
||||
expectExactType('5' as const)('5' as const)
|
||||
// @ts-expect-error
|
||||
expectExactType('5' as string)('5' as const)
|
||||
// @ts-expect-error
|
||||
expectExactType('5' as any)('5' as const)
|
||||
expectUnknown('5' as unknown)
|
||||
// @ts-expect-error
|
||||
expectUnknown('5' as const)
|
||||
// @ts-expect-error
|
||||
expectUnknown('5' as any)
|
||||
expectExactAny('5' as any)
|
||||
// @ts-expect-error
|
||||
expectExactAny('5' as const)
|
||||
// @ts-expect-error
|
||||
expectExactAny('5' as unknown)
|
||||
510
node_modules/@reduxjs/toolkit/src/tests/immutableStateInvariantMiddleware.test.ts
generated
vendored
Normal file
510
node_modules/@reduxjs/toolkit/src/tests/immutableStateInvariantMiddleware.test.ts
generated
vendored
Normal file
@@ -0,0 +1,510 @@
|
||||
import type {
|
||||
Store,
|
||||
MiddlewareAPI,
|
||||
Dispatch,
|
||||
ImmutableStateInvariantMiddlewareOptions,
|
||||
} from '@reduxjs/toolkit'
|
||||
import {
|
||||
createImmutableStateInvariantMiddleware,
|
||||
isImmutableDefault,
|
||||
} from '@reduxjs/toolkit'
|
||||
|
||||
import { trackForMutations } from '@internal/immutableStateInvariantMiddleware'
|
||||
import { mockConsole, createConsole, getLog } from 'console-testing-library'
|
||||
|
||||
describe('createImmutableStateInvariantMiddleware', () => {
|
||||
let state: { foo: { bar: number[]; baz: string } }
|
||||
const getState: Store['getState'] = () => state
|
||||
|
||||
function middleware(options: ImmutableStateInvariantMiddlewareOptions = {}) {
|
||||
return createImmutableStateInvariantMiddleware(options)({
|
||||
getState,
|
||||
} as MiddlewareAPI)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
state = { foo: { bar: [2, 3, 4], baz: 'baz' } }
|
||||
})
|
||||
|
||||
it('sends the action through the middleware chain', () => {
|
||||
const next: Dispatch = (action) => ({ ...action, returned: true })
|
||||
const dispatch = middleware()(next)
|
||||
|
||||
expect(dispatch({ type: 'SOME_ACTION' })).toEqual({
|
||||
type: 'SOME_ACTION',
|
||||
returned: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('throws if mutating inside the dispatch', () => {
|
||||
const next: Dispatch = (action) => {
|
||||
state.foo.bar.push(5)
|
||||
return action
|
||||
}
|
||||
|
||||
const dispatch = middleware()(next)
|
||||
|
||||
expect(() => {
|
||||
dispatch({ type: 'SOME_ACTION' })
|
||||
}).toThrow(new RegExp('foo\\.bar\\.3'))
|
||||
})
|
||||
|
||||
it('throws if mutating between dispatches', () => {
|
||||
const next: Dispatch = (action) => action
|
||||
|
||||
const dispatch = middleware()(next)
|
||||
|
||||
dispatch({ type: 'SOME_ACTION' })
|
||||
state.foo.bar.push(5)
|
||||
expect(() => {
|
||||
dispatch({ type: 'SOME_OTHER_ACTION' })
|
||||
}).toThrow(new RegExp('foo\\.bar\\.3'))
|
||||
})
|
||||
|
||||
it('does not throw if not mutating inside the dispatch', () => {
|
||||
const next: Dispatch = (action) => {
|
||||
state = { ...state, foo: { ...state.foo, baz: 'changed!' } }
|
||||
return action
|
||||
}
|
||||
|
||||
const dispatch = middleware()(next)
|
||||
|
||||
expect(() => {
|
||||
dispatch({ type: 'SOME_ACTION' })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('does not throw if not mutating between dispatches', () => {
|
||||
const next: Dispatch = (action) => action
|
||||
|
||||
const dispatch = middleware()(next)
|
||||
|
||||
dispatch({ type: 'SOME_ACTION' })
|
||||
state = { ...state, foo: { ...state.foo, baz: 'changed!' } }
|
||||
expect(() => {
|
||||
dispatch({ type: 'SOME_OTHER_ACTION' })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('works correctly with circular references', () => {
|
||||
const next: Dispatch = (action) => action
|
||||
|
||||
const dispatch = middleware()(next)
|
||||
|
||||
let x: any = {}
|
||||
let y: any = {}
|
||||
x.y = y
|
||||
y.x = x
|
||||
|
||||
expect(() => {
|
||||
dispatch({ type: 'SOME_ACTION', x })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('respects "isImmutable" option', function () {
|
||||
const isImmutable = (value: any) => true
|
||||
const next: Dispatch = (action) => {
|
||||
state.foo.bar.push(5)
|
||||
return action
|
||||
}
|
||||
|
||||
const dispatch = middleware({ isImmutable })(next)
|
||||
|
||||
expect(() => {
|
||||
dispatch({ type: 'SOME_ACTION' })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('respects "ignoredPaths" option', () => {
|
||||
const next: Dispatch = (action) => {
|
||||
state.foo.bar.push(5)
|
||||
return action
|
||||
}
|
||||
|
||||
const dispatch1 = middleware({ ignoredPaths: ['foo.bar'] })(next)
|
||||
|
||||
expect(() => {
|
||||
dispatch1({ type: 'SOME_ACTION' })
|
||||
}).not.toThrow()
|
||||
|
||||
const dispatch2 = middleware({ ignoredPaths: [/^foo/] })(next)
|
||||
|
||||
expect(() => {
|
||||
dispatch2({ type: 'SOME_ACTION' })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('alias "ignore" to "ignoredPath" and respects option', () => {
|
||||
const next: Dispatch = (action) => {
|
||||
state.foo.bar.push(5)
|
||||
return action
|
||||
}
|
||||
|
||||
const dispatch = middleware({ ignore: ['foo.bar'] })(next)
|
||||
|
||||
expect(() => {
|
||||
dispatch({ type: 'SOME_ACTION' })
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
it('Should print a warning if execution takes too long', () => {
|
||||
state.foo.bar = new Array(10000).fill({ value: 'more' })
|
||||
|
||||
const next: Dispatch = (action) => action
|
||||
|
||||
const dispatch = middleware({ warnAfter: 4 })(next)
|
||||
|
||||
const restore = mockConsole(createConsole())
|
||||
try {
|
||||
dispatch({ type: 'SOME_ACTION' })
|
||||
expect(getLog().log).toMatch(
|
||||
/^ImmutableStateInvariantMiddleware took \d*ms, which is more than the warning threshold of 4ms./
|
||||
)
|
||||
} finally {
|
||||
restore()
|
||||
}
|
||||
})
|
||||
|
||||
it('Should not print a warning if "next" takes too long', () => {
|
||||
const next: Dispatch = (action) => {
|
||||
const started = Date.now()
|
||||
while (Date.now() - started < 8) {}
|
||||
return action
|
||||
}
|
||||
|
||||
const dispatch = middleware({ warnAfter: 4 })(next)
|
||||
|
||||
const restore = mockConsole(createConsole())
|
||||
try {
|
||||
dispatch({ type: 'SOME_ACTION' })
|
||||
expect(getLog().log).toEqual('')
|
||||
} finally {
|
||||
restore()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('trackForMutations', () => {
|
||||
function testCasesForMutation(spec: any) {
|
||||
it('returns true and the mutated path', () => {
|
||||
const state = spec.getState()
|
||||
const options = spec.middlewareOptions || {}
|
||||
const { isImmutable = isImmutableDefault, ignoredPaths } = options
|
||||
const tracker = trackForMutations(isImmutable, ignoredPaths, state)
|
||||
const newState = spec.fn(state)
|
||||
|
||||
expect(tracker.detectMutations()).toEqual({
|
||||
wasMutated: true,
|
||||
path: spec.path.join('.'),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function testCasesForNonMutation(spec: any) {
|
||||
it('returns false', () => {
|
||||
const state = spec.getState()
|
||||
const options = spec.middlewareOptions || {}
|
||||
const { isImmutable = isImmutableDefault, ignoredPaths } = options
|
||||
const tracker = trackForMutations(isImmutable, ignoredPaths, state)
|
||||
const newState = spec.fn(state)
|
||||
|
||||
expect(tracker.detectMutations()).toEqual({ wasMutated: false })
|
||||
})
|
||||
}
|
||||
|
||||
interface TestConfig {
|
||||
getState: Store['getState']
|
||||
fn: (s: any) => typeof s | object
|
||||
middlewareOptions?: ImmutableStateInvariantMiddlewareOptions
|
||||
path?: string[]
|
||||
}
|
||||
|
||||
const mutations: Record<string, TestConfig> = {
|
||||
'adding to nested array': {
|
||||
getState: () => ({
|
||||
foo: {
|
||||
bar: [2, 3, 4],
|
||||
baz: 'baz',
|
||||
},
|
||||
stuff: [],
|
||||
}),
|
||||
fn: (s) => {
|
||||
s.foo.bar.push(5)
|
||||
return s
|
||||
},
|
||||
path: ['foo', 'bar', '3'],
|
||||
},
|
||||
'adding to nested array and setting new root object': {
|
||||
getState: () => ({
|
||||
foo: {
|
||||
bar: [2, 3, 4],
|
||||
baz: 'baz',
|
||||
},
|
||||
stuff: [],
|
||||
}),
|
||||
fn: (s) => {
|
||||
s.foo.bar.push(5)
|
||||
return { ...s }
|
||||
},
|
||||
path: ['foo', 'bar', '3'],
|
||||
},
|
||||
'changing nested string': {
|
||||
getState: () => ({
|
||||
foo: {
|
||||
bar: [2, 3, 4],
|
||||
baz: 'baz',
|
||||
},
|
||||
stuff: [],
|
||||
}),
|
||||
fn: (s) => {
|
||||
s.foo.baz = 'changed!'
|
||||
return s
|
||||
},
|
||||
path: ['foo', 'baz'],
|
||||
},
|
||||
'removing nested state': {
|
||||
getState: () => ({
|
||||
foo: {
|
||||
bar: [2, 3, 4],
|
||||
baz: 'baz',
|
||||
},
|
||||
stuff: [],
|
||||
}),
|
||||
fn: (s) => {
|
||||
delete s.foo
|
||||
return s
|
||||
},
|
||||
path: ['foo'],
|
||||
},
|
||||
'adding to array': {
|
||||
getState: () => ({
|
||||
foo: {
|
||||
bar: [2, 3, 4],
|
||||
baz: 'baz',
|
||||
},
|
||||
stuff: [],
|
||||
}),
|
||||
fn: (s) => {
|
||||
s.stuff.push(1)
|
||||
return s
|
||||
},
|
||||
path: ['stuff', '0'],
|
||||
},
|
||||
'adding object to array': {
|
||||
getState: () => ({
|
||||
stuff: [],
|
||||
}),
|
||||
fn: (s) => {
|
||||
s.stuff.push({ foo: 1, bar: 2 })
|
||||
return s
|
||||
},
|
||||
path: ['stuff', '0'],
|
||||
},
|
||||
'mutating previous state and returning new state': {
|
||||
getState: () => ({ counter: 0 }),
|
||||
fn: (s) => {
|
||||
s.mutation = true
|
||||
return { ...s, counter: s.counter + 1 }
|
||||
},
|
||||
path: ['mutation'],
|
||||
},
|
||||
'mutating previous state with non immutable type and returning new state': {
|
||||
getState: () => ({ counter: 0 }),
|
||||
fn: (s) => {
|
||||
s.mutation = [1, 2, 3]
|
||||
return { ...s, counter: s.counter + 1 }
|
||||
},
|
||||
path: ['mutation'],
|
||||
},
|
||||
'mutating previous state with non immutable type and returning new state without that property':
|
||||
{
|
||||
getState: () => ({ counter: 0 }),
|
||||
fn: (s) => {
|
||||
s.mutation = [1, 2, 3]
|
||||
return { counter: s.counter + 1 }
|
||||
},
|
||||
path: ['mutation'],
|
||||
},
|
||||
'mutating previous state with non immutable type and returning new simple state':
|
||||
{
|
||||
getState: () => ({ counter: 0 }),
|
||||
fn: (s) => {
|
||||
s.mutation = [1, 2, 3]
|
||||
return 1
|
||||
},
|
||||
path: ['mutation'],
|
||||
},
|
||||
'mutating previous state by deleting property and returning new state without that property':
|
||||
{
|
||||
getState: () => ({ counter: 0, toBeDeleted: true }),
|
||||
fn: (s) => {
|
||||
delete s.toBeDeleted
|
||||
return { counter: s.counter + 1 }
|
||||
},
|
||||
path: ['toBeDeleted'],
|
||||
},
|
||||
'mutating previous state by deleting nested property': {
|
||||
getState: () => ({ nested: { counter: 0, toBeDeleted: true }, foo: 1 }),
|
||||
fn: (s) => {
|
||||
delete s.nested.toBeDeleted
|
||||
return { nested: { counter: s.counter + 1 } }
|
||||
},
|
||||
path: ['nested', 'toBeDeleted'],
|
||||
},
|
||||
'update reference': {
|
||||
getState: () => ({ foo: {} }),
|
||||
fn: (s) => {
|
||||
s.foo = {}
|
||||
return s
|
||||
},
|
||||
path: ['foo'],
|
||||
},
|
||||
'cannot ignore root state': {
|
||||
getState: () => ({ foo: {} }),
|
||||
fn: (s) => {
|
||||
s.foo = {}
|
||||
return s
|
||||
},
|
||||
middlewareOptions: {
|
||||
ignoredPaths: [''],
|
||||
},
|
||||
path: ['foo'],
|
||||
},
|
||||
'catching state mutation in non-ignored branch': {
|
||||
getState: () => ({
|
||||
foo: {
|
||||
bar: [1, 2],
|
||||
},
|
||||
boo: {
|
||||
yah: [1, 2],
|
||||
},
|
||||
}),
|
||||
fn: (s) => {
|
||||
s.foo.bar.push(3)
|
||||
s.boo.yah.push(3)
|
||||
return s
|
||||
},
|
||||
middlewareOptions: {
|
||||
ignoredPaths: ['foo'],
|
||||
},
|
||||
path: ['boo', 'yah', '2'],
|
||||
},
|
||||
}
|
||||
|
||||
Object.keys(mutations).forEach((mutationDesc) => {
|
||||
describe(mutationDesc, () => {
|
||||
testCasesForMutation(mutations[mutationDesc])
|
||||
})
|
||||
})
|
||||
|
||||
const nonMutations: Record<string, TestConfig> = {
|
||||
'not doing anything': {
|
||||
getState: () => ({ a: 1, b: 2 }),
|
||||
fn: (s) => s,
|
||||
},
|
||||
'from undefined to something': {
|
||||
getState: () => undefined,
|
||||
fn: (s) => ({ foo: 'bar' }),
|
||||
},
|
||||
'returning same state': {
|
||||
getState: () => ({
|
||||
foo: {
|
||||
bar: [2, 3, 4],
|
||||
baz: 'baz',
|
||||
},
|
||||
stuff: [],
|
||||
}),
|
||||
fn: (s) => s,
|
||||
},
|
||||
'returning a new state object with nested new string': {
|
||||
getState: () => ({
|
||||
foo: {
|
||||
bar: [2, 3, 4],
|
||||
baz: 'baz',
|
||||
},
|
||||
stuff: [],
|
||||
}),
|
||||
fn: (s) => {
|
||||
return { ...s, foo: { ...s.foo, baz: 'changed!' } }
|
||||
},
|
||||
},
|
||||
'returning a new state object with nested new array': {
|
||||
getState: () => ({
|
||||
foo: {
|
||||
bar: [2, 3, 4],
|
||||
baz: 'baz',
|
||||
},
|
||||
stuff: [],
|
||||
}),
|
||||
fn: (s) => {
|
||||
return { ...s, foo: { ...s.foo, bar: [...s.foo.bar, 5] } }
|
||||
},
|
||||
},
|
||||
'removing nested state': {
|
||||
getState: () => ({
|
||||
foo: {
|
||||
bar: [2, 3, 4],
|
||||
baz: 'baz',
|
||||
},
|
||||
stuff: [],
|
||||
}),
|
||||
fn: (s) => {
|
||||
return { ...s, foo: {} }
|
||||
},
|
||||
},
|
||||
'having a NaN in the state': {
|
||||
getState: () => ({ a: NaN, b: Number.NaN }),
|
||||
fn: (s) => s,
|
||||
},
|
||||
'ignoring branches from mutation detection': {
|
||||
getState: () => ({
|
||||
foo: {
|
||||
bar: 'bar',
|
||||
},
|
||||
}),
|
||||
fn: (s) => {
|
||||
s.foo.bar = 'baz'
|
||||
return s
|
||||
},
|
||||
middlewareOptions: {
|
||||
ignoredPaths: ['foo'],
|
||||
},
|
||||
},
|
||||
'ignoring nested branches from mutation detection': {
|
||||
getState: () => ({
|
||||
foo: {
|
||||
bar: [1, 2],
|
||||
boo: {
|
||||
yah: [1, 2],
|
||||
},
|
||||
},
|
||||
}),
|
||||
fn: (s) => {
|
||||
s.foo.bar.push(3)
|
||||
s.foo.boo.yah.push(3)
|
||||
return s
|
||||
},
|
||||
middlewareOptions: {
|
||||
ignoredPaths: ['foo.bar', 'foo.boo.yah'],
|
||||
},
|
||||
},
|
||||
'ignoring nested array indices from mutation detection': {
|
||||
getState: () => ({
|
||||
stuff: [{ a: 1 }, { a: 2 }],
|
||||
}),
|
||||
fn: (s) => {
|
||||
s.stuff[1].a = 3
|
||||
return s
|
||||
},
|
||||
middlewareOptions: {
|
||||
ignoredPaths: ['stuff.1'],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
Object.keys(nonMutations).forEach((nonMutationDesc) => {
|
||||
describe(nonMutationDesc, () => {
|
||||
testCasesForNonMutation(nonMutations[nonMutationDesc])
|
||||
})
|
||||
})
|
||||
})
|
||||
64
node_modules/@reduxjs/toolkit/src/tests/injectableCombineReducers.example.ts
generated
vendored
Normal file
64
node_modules/@reduxjs/toolkit/src/tests/injectableCombineReducers.example.ts
generated
vendored
Normal file
@@ -0,0 +1,64 @@
|
||||
/* eslint-disable import/first */
|
||||
// @ts-nocheck
|
||||
|
||||
// reducer.ts or whatever
|
||||
|
||||
import { combineSlices } from '@reduxjs/toolkit'
|
||||
|
||||
import { sliceA } from 'fileA'
|
||||
import { sliceB } from 'fileB'
|
||||
import { lazySliceC } from 'fileC'
|
||||
import type { lazySliceD } from 'fileD'
|
||||
|
||||
import { anotherReducer } from 'somewhere'
|
||||
|
||||
export interface LazyLoadedSlices {}
|
||||
|
||||
export const rootReducer = combineSlices(sliceA, sliceB, {
|
||||
another: anotherReducer,
|
||||
}).withLazyLoadedSlices<LazyLoadedSlices>()
|
||||
/*
|
||||
results in a return type of
|
||||
{
|
||||
[sliceA.name]: SliceAState,
|
||||
[sliceB.name]: SliceBState,
|
||||
another: AnotherState,
|
||||
[lazySliceC.name]?: SliceCState, // see fileC.ts to understand why this appears here
|
||||
[lazySliceD.name]?: SliceDState, // see fileD.ts to understand why this appears here
|
||||
}
|
||||
*/
|
||||
|
||||
// fileC.ts
|
||||
// "naive" approach
|
||||
|
||||
import type { RootState } from './reducer';
|
||||
import { rootReducer } from './reducer'
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
|
||||
interface SliceCState {
|
||||
foo: string
|
||||
}
|
||||
|
||||
declare module './reducer' {
|
||||
export interface LazyLoadedSlices {
|
||||
[lazySliceC.name]: SliceCState
|
||||
}
|
||||
}
|
||||
|
||||
export const lazySliceC = createSlice({
|
||||
/* ... */
|
||||
})
|
||||
/**
|
||||
* Synchronously call `injectSlice` in file.
|
||||
*/
|
||||
rootReducer.injectSlice(lazySliceC)
|
||||
|
||||
// might want to add code for HMR as well here
|
||||
|
||||
// this will still error - `lazySliceC` is optional here
|
||||
const naiveSelectFoo = (state: RootState) => state.lazySliceC.foo
|
||||
|
||||
const selectFoo = rootReducer.withSlice(lazySliceC).selector((state) => {
|
||||
// `lazySlice` is guaranteed to not be `undefined` here.
|
||||
return state.lazySlice.foo
|
||||
})
|
||||
25
node_modules/@reduxjs/toolkit/src/tests/isPlainObject.test.ts
generated
vendored
Normal file
25
node_modules/@reduxjs/toolkit/src/tests/isPlainObject.test.ts
generated
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
import { isPlainObject } from '@reduxjs/toolkit'
|
||||
import vm from 'vm'
|
||||
|
||||
describe('isPlainObject', () => {
|
||||
it('returns true only if plain object', () => {
|
||||
class Test {
|
||||
prop: number
|
||||
constructor() {
|
||||
this.prop = 1
|
||||
}
|
||||
}
|
||||
|
||||
const sandbox = { fromAnotherRealm: false }
|
||||
vm.runInNewContext('fromAnotherRealm = {}', sandbox)
|
||||
|
||||
expect(isPlainObject(sandbox.fromAnotherRealm)).toBe(true)
|
||||
expect(isPlainObject(new Test())).toBe(false)
|
||||
expect(isPlainObject(new Date())).toBe(false)
|
||||
expect(isPlainObject([1, 2, 3])).toBe(false)
|
||||
expect(isPlainObject(null)).toBe(false)
|
||||
expect(isPlainObject(undefined)).toBe(false)
|
||||
expect(isPlainObject({ x: 1, y: 2 })).toBe(true)
|
||||
expect(isPlainObject(Object.create(null))).toBe(true)
|
||||
})
|
||||
})
|
||||
248
node_modules/@reduxjs/toolkit/src/tests/mapBuilders.typetest.ts
generated
vendored
Normal file
248
node_modules/@reduxjs/toolkit/src/tests/mapBuilders.typetest.ts
generated
vendored
Normal file
@@ -0,0 +1,248 @@
|
||||
import type { SerializedError } from '@internal/createAsyncThunk'
|
||||
import { createAsyncThunk } from '@internal/createAsyncThunk'
|
||||
import { executeReducerBuilderCallback } from '@internal/mapBuilders'
|
||||
import type { AnyAction } from '@reduxjs/toolkit'
|
||||
import { createAction } from '@reduxjs/toolkit'
|
||||
import { expectExactType, expectType } from './helpers'
|
||||
|
||||
/** Test: alternative builder callback for actionMap */
|
||||
{
|
||||
const increment = createAction<number, 'increment'>('increment')
|
||||
const decrement = createAction<number, 'decrement'>('decrement')
|
||||
|
||||
executeReducerBuilderCallback<number>((builder) => {
|
||||
builder.addCase(increment, (state, action) => {
|
||||
expectType<number>(state)
|
||||
expectType<{ type: 'increment'; payload: number }>(action)
|
||||
// @ts-expect-error
|
||||
expectType<string>(state)
|
||||
// @ts-expect-error
|
||||
expectType<{ type: 'increment'; payload: string }>(action)
|
||||
// @ts-expect-error
|
||||
expectType<{ type: 'decrement'; payload: number }>(action)
|
||||
})
|
||||
|
||||
builder.addCase('increment', (state, action) => {
|
||||
expectType<number>(state)
|
||||
expectType<{ type: 'increment' }>(action)
|
||||
// @ts-expect-error
|
||||
expectType<{ type: 'decrement' }>(action)
|
||||
// @ts-expect-error - this cannot be inferred and has to be manually specified
|
||||
expectType<{ type: 'increment'; payload: number }>(action)
|
||||
})
|
||||
|
||||
builder.addCase(
|
||||
increment,
|
||||
(state, action: ReturnType<typeof increment>) => state
|
||||
)
|
||||
// @ts-expect-error
|
||||
builder.addCase(
|
||||
increment,
|
||||
(state, action: ReturnType<typeof decrement>) => state
|
||||
)
|
||||
|
||||
builder.addCase(
|
||||
'increment',
|
||||
(state, action: ReturnType<typeof increment>) => state
|
||||
)
|
||||
// @ts-expect-error
|
||||
builder.addCase(
|
||||
'decrement',
|
||||
(state, action: ReturnType<typeof increment>) => state
|
||||
)
|
||||
|
||||
// action type is inferred
|
||||
builder.addMatcher(increment.match, (state, action) => {
|
||||
expectType<ReturnType<typeof increment>>(action)
|
||||
})
|
||||
|
||||
{
|
||||
// action type is inferred when type predicate lacks `type` property
|
||||
type PredicateWithoutTypeProperty = {
|
||||
payload: number
|
||||
}
|
||||
|
||||
builder.addMatcher(
|
||||
(action): action is PredicateWithoutTypeProperty => true,
|
||||
(state, action) => {
|
||||
expectType<PredicateWithoutTypeProperty>(action)
|
||||
expectType<AnyAction>(action)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// action type defaults to AnyAction if no type predicate matcher is passed
|
||||
builder.addMatcher(
|
||||
() => true,
|
||||
(state, action) => {
|
||||
expectExactType({} as AnyAction)(action)
|
||||
}
|
||||
)
|
||||
|
||||
// with a boolean checker, action can also be typed by type argument
|
||||
builder.addMatcher<{ foo: boolean }>(
|
||||
() => true,
|
||||
(state, action) => {
|
||||
expectType<{ foo: boolean }>(action)
|
||||
expectType<AnyAction>(action)
|
||||
}
|
||||
)
|
||||
|
||||
// addCase().addMatcher() is possible, action type inferred correctly
|
||||
builder
|
||||
.addCase(
|
||||
'increment',
|
||||
(state, action: ReturnType<typeof increment>) => state
|
||||
)
|
||||
.addMatcher(decrement.match, (state, action) => {
|
||||
expectType<ReturnType<typeof decrement>>(action)
|
||||
})
|
||||
|
||||
// addCase().addDefaultCase() is possible, action type is AnyAction
|
||||
builder
|
||||
.addCase(
|
||||
'increment',
|
||||
(state, action: ReturnType<typeof increment>) => state
|
||||
)
|
||||
.addDefaultCase((state, action) => {
|
||||
expectType<AnyAction>(action)
|
||||
})
|
||||
|
||||
{
|
||||
// addMatcher() should prevent further calls to addCase()
|
||||
const b = builder.addMatcher(increment.match, () => {})
|
||||
// @ts-expect-error
|
||||
b.addCase(increment, () => {})
|
||||
b.addMatcher(increment.match, () => {})
|
||||
b.addDefaultCase(() => {})
|
||||
}
|
||||
|
||||
{
|
||||
// addDefaultCase() should prevent further calls to addCase(), addMatcher() and addDefaultCase
|
||||
const b = builder.addDefaultCase(() => {})
|
||||
// @ts-expect-error
|
||||
b.addCase(increment, () => {})
|
||||
// @ts-expect-error
|
||||
b.addMatcher(increment.match, () => {})
|
||||
// @ts-expect-error
|
||||
b.addDefaultCase(() => {})
|
||||
}
|
||||
|
||||
// `createAsyncThunk` actions work with `mapBuilder`
|
||||
{
|
||||
// case 1: normal `createAsyncThunk`
|
||||
{
|
||||
const thunk = createAsyncThunk('test', () => {
|
||||
return 'ret' as const
|
||||
})
|
||||
builder.addCase(thunk.pending, (_, action) => {
|
||||
expectType<{
|
||||
payload: undefined
|
||||
meta: {
|
||||
arg: void
|
||||
requestId: string
|
||||
requestStatus: 'pending'
|
||||
}
|
||||
}>(action)
|
||||
})
|
||||
|
||||
builder.addCase(thunk.rejected, (_, action) => {
|
||||
expectType<{
|
||||
payload: unknown
|
||||
error: SerializedError
|
||||
meta: {
|
||||
arg: void
|
||||
requestId: string
|
||||
requestStatus: 'rejected'
|
||||
aborted: boolean
|
||||
condition: boolean
|
||||
rejectedWithValue: boolean
|
||||
}
|
||||
}>(action)
|
||||
})
|
||||
builder.addCase(thunk.fulfilled, (_, action) => {
|
||||
expectType<{
|
||||
payload: 'ret'
|
||||
meta: {
|
||||
arg: void
|
||||
requestId: string
|
||||
requestStatus: 'fulfilled'
|
||||
}
|
||||
}>(action)
|
||||
})
|
||||
}
|
||||
}
|
||||
// case 2: `createAsyncThunk` with `meta`
|
||||
{
|
||||
const thunk = createAsyncThunk<
|
||||
'ret',
|
||||
void,
|
||||
{
|
||||
pendingMeta: { startedTimeStamp: number }
|
||||
fulfilledMeta: {
|
||||
fulfilledTimeStamp: number
|
||||
baseQueryMeta: 'meta!'
|
||||
}
|
||||
rejectedMeta: {
|
||||
baseQueryMeta: 'meta!'
|
||||
}
|
||||
}
|
||||
>(
|
||||
'test',
|
||||
(_, api) => {
|
||||
return api.fulfillWithValue('ret' as const, {
|
||||
fulfilledTimeStamp: 5,
|
||||
baseQueryMeta: 'meta!',
|
||||
})
|
||||
},
|
||||
{
|
||||
getPendingMeta() {
|
||||
return { startedTimeStamp: 0 }
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
builder.addCase(thunk.pending, (_, action) => {
|
||||
expectType<{
|
||||
payload: undefined
|
||||
meta: {
|
||||
arg: void
|
||||
requestId: string
|
||||
requestStatus: 'pending'
|
||||
startedTimeStamp: number
|
||||
}
|
||||
}>(action)
|
||||
})
|
||||
|
||||
builder.addCase(thunk.rejected, (_, action) => {
|
||||
expectType<{
|
||||
payload: unknown
|
||||
error: SerializedError
|
||||
meta: {
|
||||
arg: void
|
||||
requestId: string
|
||||
requestStatus: 'rejected'
|
||||
aborted: boolean
|
||||
condition: boolean
|
||||
rejectedWithValue: boolean
|
||||
baseQueryMeta?: 'meta!'
|
||||
}
|
||||
}>(action)
|
||||
if (action.meta.rejectedWithValue) {
|
||||
expectType<'meta!'>(action.meta.baseQueryMeta)
|
||||
}
|
||||
})
|
||||
builder.addCase(thunk.fulfilled, (_, action) => {
|
||||
expectType<{
|
||||
payload: 'ret'
|
||||
meta: {
|
||||
arg: void
|
||||
requestId: string
|
||||
requestStatus: 'fulfilled'
|
||||
baseQueryMeta: 'meta!'
|
||||
}
|
||||
}>(action)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
460
node_modules/@reduxjs/toolkit/src/tests/matchers.test.ts
generated
vendored
Normal file
460
node_modules/@reduxjs/toolkit/src/tests/matchers.test.ts
generated
vendored
Normal file
@@ -0,0 +1,460 @@
|
||||
import type { ThunkAction, AnyAction } from '@reduxjs/toolkit'
|
||||
import {
|
||||
isAllOf,
|
||||
isAnyOf,
|
||||
isAsyncThunkAction,
|
||||
isFulfilled,
|
||||
isPending,
|
||||
isRejected,
|
||||
isRejectedWithValue,
|
||||
createAction,
|
||||
createAsyncThunk,
|
||||
createReducer,
|
||||
} from '@reduxjs/toolkit'
|
||||
|
||||
const thunk: ThunkAction<any, any, any, AnyAction> = () => {}
|
||||
|
||||
describe('isAnyOf', () => {
|
||||
it('returns true only if any matchers match (match function)', () => {
|
||||
const actionA = createAction<string>('a')
|
||||
const actionB = createAction<number>('b')
|
||||
|
||||
const trueAction = {
|
||||
type: 'a',
|
||||
payload: 'payload',
|
||||
}
|
||||
|
||||
expect(isAnyOf(actionA, actionB)(trueAction)).toEqual(true)
|
||||
|
||||
const falseAction = {
|
||||
type: 'c',
|
||||
payload: 'payload',
|
||||
}
|
||||
|
||||
expect(isAnyOf(actionA, actionB)(falseAction)).toEqual(false)
|
||||
})
|
||||
|
||||
it('returns true only if any type guards match', () => {
|
||||
const actionA = createAction<string>('a')
|
||||
const actionB = createAction<number>('b')
|
||||
|
||||
const isActionA = actionA.match
|
||||
const isActionB = actionB.match
|
||||
|
||||
const trueAction = {
|
||||
type: 'a',
|
||||
payload: 'payload',
|
||||
}
|
||||
|
||||
expect(isAnyOf(isActionA, isActionB)(trueAction)).toEqual(true)
|
||||
|
||||
const falseAction = {
|
||||
type: 'c',
|
||||
payload: 'payload',
|
||||
}
|
||||
|
||||
expect(isAnyOf(isActionA, isActionB)(falseAction)).toEqual(false)
|
||||
})
|
||||
|
||||
it('returns true only if any matchers match (thunk action creators)', () => {
|
||||
const thunkA = createAsyncThunk<string>('a', () => {
|
||||
return 'noop'
|
||||
})
|
||||
const thunkB = createAsyncThunk<number>('b', () => {
|
||||
return 0
|
||||
})
|
||||
|
||||
const action = thunkA.fulfilled('fakeRequestId', 'test')
|
||||
|
||||
expect(isAnyOf(thunkA.fulfilled, thunkB.fulfilled)(action)).toEqual(true)
|
||||
|
||||
expect(
|
||||
isAnyOf(thunkA.pending, thunkA.rejected, thunkB.fulfilled)(action)
|
||||
).toEqual(false)
|
||||
})
|
||||
|
||||
it('works with reducers', () => {
|
||||
const actionA = createAction<string>('a')
|
||||
const actionB = createAction<number>('b')
|
||||
|
||||
const trueAction = {
|
||||
type: 'a',
|
||||
payload: 'payload',
|
||||
}
|
||||
|
||||
const initialState = { value: false }
|
||||
|
||||
const reducer = createReducer(initialState, (builder) => {
|
||||
builder.addMatcher(isAnyOf(actionA, actionB), (state) => {
|
||||
return { ...state, value: true }
|
||||
})
|
||||
})
|
||||
|
||||
expect(reducer(initialState, trueAction)).toEqual({ value: true })
|
||||
|
||||
const falseAction = {
|
||||
type: 'c',
|
||||
payload: 'payload',
|
||||
}
|
||||
|
||||
expect(reducer(initialState, falseAction)).toEqual(initialState)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isAllOf', () => {
|
||||
it('returns true only if all matchers match', () => {
|
||||
const actionA = createAction<string>('a')
|
||||
|
||||
interface SpecialAction {
|
||||
payload: 'SPECIAL'
|
||||
}
|
||||
|
||||
const isActionSpecial = (action: any): action is SpecialAction => {
|
||||
return action.payload === 'SPECIAL'
|
||||
}
|
||||
|
||||
const trueAction = {
|
||||
type: 'a',
|
||||
payload: 'SPECIAL',
|
||||
}
|
||||
|
||||
expect(isAllOf(actionA, isActionSpecial)(trueAction)).toEqual(true)
|
||||
|
||||
const falseAction = {
|
||||
type: 'a',
|
||||
payload: 'ORDINARY',
|
||||
}
|
||||
|
||||
expect(isAllOf(actionA, isActionSpecial)(falseAction)).toEqual(false)
|
||||
|
||||
const thunkA = createAsyncThunk<string>('a', () => 'result')
|
||||
|
||||
const specialThunkAction = thunkA.fulfilled('SPECIAL', 'fakeRequestId')
|
||||
|
||||
expect(isAllOf(thunkA.fulfilled, isActionSpecial)(specialThunkAction)).toBe(
|
||||
true
|
||||
)
|
||||
|
||||
const ordinaryThunkAction = thunkA.fulfilled('ORDINARY', 'fakeRequestId')
|
||||
|
||||
expect(
|
||||
isAllOf(thunkA.fulfilled, isActionSpecial)(ordinaryThunkAction)
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isPending', () => {
|
||||
test('should return false for a regular action', () => {
|
||||
const action = createAction<string>('action/type')('testPayload')
|
||||
|
||||
expect(isPending()(action)).toBe(false)
|
||||
expect(isPending(action)).toBe(false)
|
||||
expect(isPending(thunk)).toBe(false)
|
||||
})
|
||||
|
||||
test('should return true only for pending async thunk actions', () => {
|
||||
const thunk = createAsyncThunk<string>('a', () => 'result')
|
||||
|
||||
const pendingAction = thunk.pending('fakeRequestId')
|
||||
expect(isPending()(pendingAction)).toBe(true)
|
||||
expect(isPending(pendingAction)).toBe(true)
|
||||
|
||||
const rejectedAction = thunk.rejected(
|
||||
new Error('rejected'),
|
||||
'fakeRequestId'
|
||||
)
|
||||
expect(isPending()(rejectedAction)).toBe(false)
|
||||
|
||||
const fulfilledAction = thunk.fulfilled('result', 'fakeRequestId')
|
||||
expect(isPending()(fulfilledAction)).toBe(false)
|
||||
})
|
||||
|
||||
test('should return true only for thunks provided as arguments', () => {
|
||||
const thunkA = createAsyncThunk<string>('a', () => 'result')
|
||||
const thunkB = createAsyncThunk<string>('b', () => 'result')
|
||||
const thunkC = createAsyncThunk<string>('c', () => 'result')
|
||||
|
||||
const matchAC = isPending(thunkA, thunkC)
|
||||
const matchB = isPending(thunkB)
|
||||
|
||||
function testPendingAction(
|
||||
thunk: typeof thunkA | typeof thunkB | typeof thunkC,
|
||||
expected: boolean
|
||||
) {
|
||||
const pendingAction = thunk.pending('fakeRequestId')
|
||||
expect(matchAC(pendingAction)).toBe(expected)
|
||||
expect(matchB(pendingAction)).toBe(!expected)
|
||||
|
||||
const rejectedAction = thunk.rejected(
|
||||
new Error('rejected'),
|
||||
'fakeRequestId'
|
||||
)
|
||||
expect(matchAC(rejectedAction)).toBe(false)
|
||||
|
||||
const fulfilledAction = thunk.fulfilled('result', 'fakeRequestId')
|
||||
expect(matchAC(fulfilledAction)).toBe(false)
|
||||
}
|
||||
|
||||
testPendingAction(thunkA, true)
|
||||
testPendingAction(thunkC, true)
|
||||
testPendingAction(thunkB, false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isRejected', () => {
|
||||
test('should return false for a regular action', () => {
|
||||
const action = createAction<string>('action/type')('testPayload')
|
||||
|
||||
expect(isRejected()(action)).toBe(false)
|
||||
expect(isRejected(action)).toBe(false)
|
||||
expect(isRejected(thunk)).toBe(false)
|
||||
})
|
||||
|
||||
test('should return true only for rejected async thunk actions', () => {
|
||||
const thunk = createAsyncThunk<string>('a', () => 'result')
|
||||
|
||||
const pendingAction = thunk.pending('fakeRequestId')
|
||||
expect(isRejected()(pendingAction)).toBe(false)
|
||||
|
||||
const rejectedAction = thunk.rejected(
|
||||
new Error('rejected'),
|
||||
'fakeRequestId'
|
||||
)
|
||||
expect(isRejected()(rejectedAction)).toBe(true)
|
||||
expect(isRejected(rejectedAction)).toBe(true)
|
||||
|
||||
const fulfilledAction = thunk.fulfilled('result', 'fakeRequestId')
|
||||
expect(isRejected()(fulfilledAction)).toBe(false)
|
||||
})
|
||||
|
||||
test('should return true only for thunks provided as arguments', () => {
|
||||
const thunkA = createAsyncThunk<string>('a', () => 'result')
|
||||
const thunkB = createAsyncThunk<string>('b', () => 'result')
|
||||
const thunkC = createAsyncThunk<string>('c', () => 'result')
|
||||
|
||||
const matchAC = isRejected(thunkA, thunkC)
|
||||
const matchB = isRejected(thunkB)
|
||||
|
||||
function testRejectedAction(
|
||||
thunk: typeof thunkA | typeof thunkB | typeof thunkC,
|
||||
expected: boolean
|
||||
) {
|
||||
const pendingAction = thunk.pending('fakeRequestId')
|
||||
expect(matchAC(pendingAction)).toBe(false)
|
||||
|
||||
const rejectedAction = thunk.rejected(
|
||||
new Error('rejected'),
|
||||
'fakeRequestId'
|
||||
)
|
||||
expect(matchAC(rejectedAction)).toBe(expected)
|
||||
expect(matchB(rejectedAction)).toBe(!expected)
|
||||
|
||||
const fulfilledAction = thunk.fulfilled('result', 'fakeRequestId')
|
||||
expect(matchAC(fulfilledAction)).toBe(false)
|
||||
}
|
||||
|
||||
testRejectedAction(thunkA, true)
|
||||
testRejectedAction(thunkC, true)
|
||||
testRejectedAction(thunkB, false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isRejectedWithValue', () => {
|
||||
test('should return false for a regular action', () => {
|
||||
const action = createAction<string>('action/type')('testPayload')
|
||||
|
||||
expect(isRejectedWithValue()(action)).toBe(false)
|
||||
expect(isRejectedWithValue(action)).toBe(false)
|
||||
expect(isRejectedWithValue(thunk)).toBe(false)
|
||||
})
|
||||
|
||||
test('should return true only for rejected-with-value async thunk actions', async () => {
|
||||
const thunk = createAsyncThunk<string>('a', (_, { rejectWithValue }) => {
|
||||
return rejectWithValue('rejectWithValue!')
|
||||
})
|
||||
|
||||
const pendingAction = thunk.pending('fakeRequestId')
|
||||
expect(isRejectedWithValue()(pendingAction)).toBe(false)
|
||||
|
||||
const rejectedAction = thunk.rejected(
|
||||
new Error('rejected'),
|
||||
'fakeRequestId'
|
||||
)
|
||||
expect(isRejectedWithValue()(rejectedAction)).toBe(false)
|
||||
|
||||
const getState = jest.fn(() => ({}))
|
||||
const dispatch = jest.fn((x: any) => x)
|
||||
const extra = {}
|
||||
|
||||
// note: doesn't throw because we don't unwrap it
|
||||
const rejectedWithValueAction = await thunk()(dispatch, getState, extra)
|
||||
|
||||
expect(isRejectedWithValue()(rejectedWithValueAction)).toBe(true)
|
||||
|
||||
const fulfilledAction = thunk.fulfilled('result', 'fakeRequestId')
|
||||
expect(isRejectedWithValue()(fulfilledAction)).toBe(false)
|
||||
})
|
||||
|
||||
test('should return true only for thunks provided as arguments', async () => {
|
||||
const payloadCreator = (_: any, { rejectWithValue }: any) => {
|
||||
return rejectWithValue('rejectWithValue!')
|
||||
}
|
||||
|
||||
const thunkA = createAsyncThunk<string>('a', payloadCreator)
|
||||
const thunkB = createAsyncThunk<string>('b', payloadCreator)
|
||||
const thunkC = createAsyncThunk<string>('c', payloadCreator)
|
||||
|
||||
const matchAC = isRejectedWithValue(thunkA, thunkC)
|
||||
const matchB = isRejectedWithValue(thunkB)
|
||||
|
||||
async function testRejectedAction(
|
||||
thunk: typeof thunkA | typeof thunkB | typeof thunkC,
|
||||
expected: boolean
|
||||
) {
|
||||
const pendingAction = thunk.pending('fakeRequestId')
|
||||
expect(matchAC(pendingAction)).toBe(false)
|
||||
|
||||
const rejectedAction = thunk.rejected(
|
||||
new Error('rejected'),
|
||||
'fakeRequestId'
|
||||
)
|
||||
// rejected-with-value is a narrower requirement than rejected
|
||||
expect(matchAC(rejectedAction)).toBe(false)
|
||||
|
||||
const getState = jest.fn(() => ({}))
|
||||
const dispatch = jest.fn((x: any) => x)
|
||||
const extra = {}
|
||||
|
||||
// note: doesn't throw because we don't unwrap it
|
||||
const rejectedWithValueAction = await thunk()(dispatch, getState, extra)
|
||||
|
||||
expect(matchAC(rejectedWithValueAction)).toBe(expected)
|
||||
expect(matchB(rejectedWithValueAction)).toBe(!expected)
|
||||
|
||||
const fulfilledAction = thunk.fulfilled('result', 'fakeRequestId')
|
||||
expect(matchAC(fulfilledAction)).toBe(false)
|
||||
}
|
||||
|
||||
await testRejectedAction(thunkA, true)
|
||||
await testRejectedAction(thunkC, true)
|
||||
await testRejectedAction(thunkB, false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isFulfilled', () => {
|
||||
test('should return false for a regular action', () => {
|
||||
const action = createAction<string>('action/type')('testPayload')
|
||||
|
||||
expect(isFulfilled()(action)).toBe(false)
|
||||
expect(isFulfilled(action)).toBe(false)
|
||||
expect(isFulfilled(thunk)).toBe(false)
|
||||
})
|
||||
|
||||
test('should return true only for fulfilled async thunk actions', () => {
|
||||
const thunk = createAsyncThunk<string>('a', () => 'result')
|
||||
|
||||
const pendingAction = thunk.pending('fakeRequestId')
|
||||
expect(isFulfilled()(pendingAction)).toBe(false)
|
||||
|
||||
const rejectedAction = thunk.rejected(
|
||||
new Error('rejected'),
|
||||
'fakeRequestId'
|
||||
)
|
||||
expect(isFulfilled()(rejectedAction)).toBe(false)
|
||||
|
||||
const fulfilledAction = thunk.fulfilled('result', 'fakeRequestId')
|
||||
expect(isFulfilled()(fulfilledAction)).toBe(true)
|
||||
expect(isFulfilled(fulfilledAction)).toBe(true)
|
||||
})
|
||||
|
||||
test('should return true only for thunks provided as arguments', () => {
|
||||
const thunkA = createAsyncThunk<string>('a', () => 'result')
|
||||
const thunkB = createAsyncThunk<string>('b', () => 'result')
|
||||
const thunkC = createAsyncThunk<string>('c', () => 'result')
|
||||
|
||||
const matchAC = isFulfilled(thunkA, thunkC)
|
||||
const matchB = isFulfilled(thunkB)
|
||||
|
||||
function testFulfilledAction(
|
||||
thunk: typeof thunkA | typeof thunkB | typeof thunkC,
|
||||
expected: boolean
|
||||
) {
|
||||
const pendingAction = thunk.pending('fakeRequestId')
|
||||
expect(matchAC(pendingAction)).toBe(false)
|
||||
|
||||
const rejectedAction = thunk.rejected(
|
||||
new Error('rejected'),
|
||||
'fakeRequestId'
|
||||
)
|
||||
expect(matchAC(rejectedAction)).toBe(false)
|
||||
|
||||
const fulfilledAction = thunk.fulfilled('result', 'fakeRequestId')
|
||||
expect(matchAC(fulfilledAction)).toBe(expected)
|
||||
expect(matchB(fulfilledAction)).toBe(!expected)
|
||||
}
|
||||
|
||||
testFulfilledAction(thunkA, true)
|
||||
testFulfilledAction(thunkC, true)
|
||||
testFulfilledAction(thunkB, false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isAsyncThunkAction', () => {
|
||||
test('should return false for a regular action', () => {
|
||||
const action = createAction<string>('action/type')('testPayload')
|
||||
|
||||
expect(isAsyncThunkAction()(action)).toBe(false)
|
||||
expect(isAsyncThunkAction(action)).toBe(false)
|
||||
expect(isAsyncThunkAction(thunk)).toBe(false)
|
||||
})
|
||||
|
||||
test('should return true for any async thunk action if no arguments were provided', () => {
|
||||
const thunk = createAsyncThunk<string>('a', () => 'result')
|
||||
const matcher = isAsyncThunkAction()
|
||||
|
||||
const pendingAction = thunk.pending('fakeRequestId')
|
||||
expect(matcher(pendingAction)).toBe(true)
|
||||
|
||||
const rejectedAction = thunk.rejected(
|
||||
new Error('rejected'),
|
||||
'fakeRequestId'
|
||||
)
|
||||
expect(matcher(rejectedAction)).toBe(true)
|
||||
|
||||
const fulfilledAction = thunk.fulfilled('result', 'fakeRequestId')
|
||||
expect(matcher(fulfilledAction)).toBe(true)
|
||||
})
|
||||
|
||||
test('should return true only for thunks provided as arguments', () => {
|
||||
const thunkA = createAsyncThunk<string>('a', () => 'result')
|
||||
const thunkB = createAsyncThunk<string>('b', () => 'result')
|
||||
const thunkC = createAsyncThunk<string>('c', () => 'result')
|
||||
|
||||
const matchAC = isAsyncThunkAction(thunkA, thunkC)
|
||||
const matchB = isAsyncThunkAction(thunkB)
|
||||
|
||||
function testAllActions(
|
||||
thunk: typeof thunkA | typeof thunkB | typeof thunkC,
|
||||
expected: boolean
|
||||
) {
|
||||
const pendingAction = thunk.pending('fakeRequestId')
|
||||
expect(matchAC(pendingAction)).toBe(expected)
|
||||
expect(matchB(pendingAction)).toBe(!expected)
|
||||
|
||||
const rejectedAction = thunk.rejected(
|
||||
new Error('rejected'),
|
||||
'fakeRequestId'
|
||||
)
|
||||
expect(matchAC(rejectedAction)).toBe(expected)
|
||||
expect(matchB(rejectedAction)).toBe(!expected)
|
||||
|
||||
const fulfilledAction = thunk.fulfilled('result', 'fakeRequestId')
|
||||
expect(matchAC(fulfilledAction)).toBe(expected)
|
||||
expect(matchB(fulfilledAction)).toBe(!expected)
|
||||
}
|
||||
|
||||
testAllActions(thunkA, true)
|
||||
testAllActions(thunkC, true)
|
||||
testAllActions(thunkB, false)
|
||||
})
|
||||
})
|
||||
331
node_modules/@reduxjs/toolkit/src/tests/matchers.typetest.ts
generated
vendored
Normal file
331
node_modules/@reduxjs/toolkit/src/tests/matchers.typetest.ts
generated
vendored
Normal file
@@ -0,0 +1,331 @@
|
||||
import { expectExactType, expectUnknown } from './helpers'
|
||||
import { IsUnknown } from '@internal/tsHelpers'
|
||||
import type { AnyAction } from 'redux'
|
||||
import type { SerializedError } from '../../src'
|
||||
import {
|
||||
createAction,
|
||||
createAsyncThunk,
|
||||
isAllOf,
|
||||
isAnyOf,
|
||||
isAsyncThunkAction,
|
||||
isFulfilled,
|
||||
isPending,
|
||||
isRejected,
|
||||
isRejectedWithValue,
|
||||
} from '../../src'
|
||||
|
||||
/* isAnyOf */
|
||||
|
||||
/*
|
||||
* Test: isAnyOf correctly narrows types when used with action creators
|
||||
*/
|
||||
function isAnyOfActionTest(action: AnyAction) {
|
||||
const actionA = createAction('a', () => {
|
||||
return {
|
||||
payload: {
|
||||
prop1: 1,
|
||||
prop3: 2,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const actionB = createAction('b', () => {
|
||||
return {
|
||||
payload: {
|
||||
prop1: 1,
|
||||
prop2: 2,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
if (isAnyOf(actionA, actionB)(action)) {
|
||||
return {
|
||||
prop1: action.payload.prop1,
|
||||
// @ts-expect-error
|
||||
prop2: action.payload.prop2,
|
||||
// @ts-expect-error
|
||||
prop3: action.payload.prop3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: isAnyOf correctly narrows types when used with async thunks
|
||||
*/
|
||||
function isAnyOfThunkTest(action: AnyAction) {
|
||||
const asyncThunk1 = createAsyncThunk<{ prop1: number; prop3: number }>(
|
||||
'asyncThunk1',
|
||||
|
||||
async () => {
|
||||
return {
|
||||
prop1: 1,
|
||||
prop3: 3,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const asyncThunk2 = createAsyncThunk<{ prop1: number; prop2: number }>(
|
||||
'asyncThunk2',
|
||||
|
||||
async () => {
|
||||
return {
|
||||
prop1: 1,
|
||||
prop2: 2,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (isAnyOf(asyncThunk1.fulfilled, asyncThunk2.fulfilled)(action)) {
|
||||
return {
|
||||
prop1: action.payload.prop1,
|
||||
// @ts-expect-error
|
||||
prop2: action.payload.prop2,
|
||||
// @ts-expect-error
|
||||
prop3: action.payload.prop3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: isAnyOf correctly narrows types when used with type guards
|
||||
*/
|
||||
function isAnyOfTypeGuardTest(action: AnyAction) {
|
||||
interface ActionA {
|
||||
type: 'a'
|
||||
payload: {
|
||||
prop1: 1
|
||||
prop3: 2
|
||||
}
|
||||
}
|
||||
|
||||
interface ActionB {
|
||||
type: 'b'
|
||||
payload: {
|
||||
prop1: 1
|
||||
prop2: 2
|
||||
}
|
||||
}
|
||||
|
||||
const guardA = (v: any): v is ActionA => {
|
||||
return v.type === 'a'
|
||||
}
|
||||
|
||||
const guardB = (v: any): v is ActionB => {
|
||||
return v.type === 'b'
|
||||
}
|
||||
|
||||
if (isAnyOf(guardA, guardB)(action)) {
|
||||
return {
|
||||
prop1: action.payload.prop1,
|
||||
// @ts-expect-error
|
||||
prop2: action.payload.prop2,
|
||||
// @ts-expect-error
|
||||
prop3: action.payload.prop3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* isAllOf */
|
||||
|
||||
interface SpecialAction {
|
||||
payload: {
|
||||
special: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const isSpecialAction = (v: any): v is SpecialAction => {
|
||||
return v.meta.isSpecial
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: isAllOf correctly narrows types when used with action creators
|
||||
* and type guards
|
||||
*/
|
||||
function isAllOfActionTest(action: AnyAction) {
|
||||
const actionA = createAction('a', () => {
|
||||
return {
|
||||
payload: {
|
||||
prop1: 1,
|
||||
prop3: 2,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
if (isAllOf(actionA, isSpecialAction)(action)) {
|
||||
return {
|
||||
prop1: action.payload.prop1,
|
||||
// @ts-expect-error
|
||||
prop2: action.payload.prop2,
|
||||
prop3: action.payload.prop3,
|
||||
special: action.payload.special,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: isAllOf correctly narrows types when used with async thunks
|
||||
* and type guards
|
||||
*/
|
||||
function isAllOfThunkTest(action: AnyAction) {
|
||||
const asyncThunk1 = createAsyncThunk<{ prop1: number; prop3: number }>(
|
||||
'asyncThunk1',
|
||||
|
||||
async () => {
|
||||
return {
|
||||
prop1: 1,
|
||||
prop3: 3,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (isAllOf(asyncThunk1.fulfilled, isSpecialAction)(action)) {
|
||||
return {
|
||||
prop1: action.payload.prop1,
|
||||
// @ts-expect-error
|
||||
prop2: action.payload.prop2,
|
||||
prop3: action.payload.prop3,
|
||||
special: action.payload.special,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: isAnyOf correctly narrows types when used with type guards
|
||||
*/
|
||||
function isAllOfTypeGuardTest(action: AnyAction) {
|
||||
interface ActionA {
|
||||
type: 'a'
|
||||
payload: {
|
||||
prop1: 1
|
||||
prop3: 2
|
||||
}
|
||||
}
|
||||
|
||||
const guardA = (v: any): v is ActionA => {
|
||||
return v.type === 'a'
|
||||
}
|
||||
|
||||
if (isAllOf(guardA, isSpecialAction)(action)) {
|
||||
return {
|
||||
prop1: action.payload.prop1,
|
||||
// @ts-expect-error
|
||||
prop2: action.payload.prop2,
|
||||
prop3: action.payload.prop3,
|
||||
special: action.payload.special,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: isPending correctly narrows types
|
||||
*/
|
||||
function isPendingTest(action: AnyAction) {
|
||||
if (isPending(action)) {
|
||||
expectExactType<undefined>(action.payload)
|
||||
// @ts-expect-error
|
||||
action.error
|
||||
}
|
||||
|
||||
const thunk = createAsyncThunk<string>('a', () => 'result')
|
||||
|
||||
if (isPending(thunk)(action)) {
|
||||
expectExactType<undefined>(action.payload)
|
||||
// @ts-expect-error
|
||||
action.error
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: isRejected correctly narrows types
|
||||
*/
|
||||
function isRejectedTest(action: AnyAction) {
|
||||
if (isRejected(action)) {
|
||||
// might be there if rejected with payload
|
||||
expectUnknown(action.payload)
|
||||
expectExactType<SerializedError>(action.error)
|
||||
}
|
||||
|
||||
const thunk = createAsyncThunk<string>('a', () => 'result')
|
||||
|
||||
if (isRejected(thunk)(action)) {
|
||||
// might be there if rejected with payload
|
||||
expectUnknown(action.payload)
|
||||
expectExactType<SerializedError>(action.error)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: isFulfilled correctly narrows types
|
||||
*/
|
||||
function isFulfilledTest(action: AnyAction) {
|
||||
if (isFulfilled(action)) {
|
||||
expectUnknown(action.payload)
|
||||
// @ts-expect-error
|
||||
action.error
|
||||
}
|
||||
|
||||
const thunk = createAsyncThunk<string>('a', () => 'result')
|
||||
if (isFulfilled(thunk)(action)) {
|
||||
expectExactType('' as string)(action.payload)
|
||||
// @ts-expect-error
|
||||
action.error
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: isAsyncThunkAction correctly narrows types
|
||||
*/
|
||||
function isAsyncThunkActionTest(action: AnyAction) {
|
||||
if (isAsyncThunkAction(action)) {
|
||||
expectUnknown(action.payload)
|
||||
// do not expect an error property because pending/fulfilled lack it
|
||||
// @ts-expect-error
|
||||
action.error
|
||||
}
|
||||
|
||||
const thunk = createAsyncThunk<string>('a', () => 'result')
|
||||
if (isAsyncThunkAction(thunk)(action)) {
|
||||
// we should expect the payload to be available, but of unknown type because the action may be pending/rejected
|
||||
expectUnknown(action.payload)
|
||||
// do not expect an error property because pending/fulfilled lack it
|
||||
// @ts-expect-error
|
||||
action.error
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Test: isRejectedWithValue correctly narrows types
|
||||
*/
|
||||
function isRejectedWithValueTest(action: AnyAction) {
|
||||
if (isRejectedWithValue(action)) {
|
||||
expectUnknown(action.payload)
|
||||
expectExactType<SerializedError>(action.error)
|
||||
}
|
||||
|
||||
const thunk = createAsyncThunk<
|
||||
string,
|
||||
void,
|
||||
{ rejectValue: { message: string } }
|
||||
>('a', () => 'result')
|
||||
if (isRejectedWithValue(thunk)(action)) {
|
||||
expectExactType({ message: '' as string })(action.payload)
|
||||
expectExactType<SerializedError>(action.error)
|
||||
}
|
||||
}
|
||||
|
||||
function matchersAcceptSpreadArguments() {
|
||||
const thunk1 = createAsyncThunk('a', () => 'a')
|
||||
const thunk2 = createAsyncThunk('b', () => 'b')
|
||||
const interestingThunks = [thunk1, thunk2]
|
||||
const interestingPendingThunks = interestingThunks.map(
|
||||
(thunk) => thunk.pending
|
||||
)
|
||||
const interestingFulfilledThunks = interestingThunks.map(
|
||||
(thunk) => thunk.fulfilled
|
||||
)
|
||||
|
||||
const isLoading = isAnyOf(...interestingPendingThunks)
|
||||
const isNotLoading = isAnyOf(...interestingFulfilledThunks)
|
||||
|
||||
const isAllLoading = isAllOf(...interestingPendingThunks)
|
||||
}
|
||||
635
node_modules/@reduxjs/toolkit/src/tests/serializableStateInvariantMiddleware.test.ts
generated
vendored
Normal file
635
node_modules/@reduxjs/toolkit/src/tests/serializableStateInvariantMiddleware.test.ts
generated
vendored
Normal file
@@ -0,0 +1,635 @@
|
||||
import {
|
||||
mockConsole,
|
||||
createConsole,
|
||||
getLog,
|
||||
} from 'console-testing-library/pure'
|
||||
import type { AnyAction, Reducer } from '@reduxjs/toolkit'
|
||||
import {
|
||||
createNextState,
|
||||
configureStore,
|
||||
createSerializableStateInvariantMiddleware,
|
||||
findNonSerializableValue,
|
||||
isPlain,
|
||||
} from '@reduxjs/toolkit'
|
||||
import { isNestedFrozen } from '@internal/serializableStateInvariantMiddleware'
|
||||
|
||||
// Mocking console
|
||||
let restore = () => {}
|
||||
beforeEach(() => {
|
||||
restore = mockConsole(createConsole())
|
||||
})
|
||||
afterEach(() => restore())
|
||||
|
||||
describe('findNonSerializableValue', () => {
|
||||
it('Should return false if no matching values are found', () => {
|
||||
const obj = {
|
||||
a: 42,
|
||||
b: {
|
||||
b1: 'test',
|
||||
},
|
||||
c: [99, { d: 123 }],
|
||||
}
|
||||
|
||||
const result = findNonSerializableValue(obj)
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('Should return a keypath and the value if it finds a non-serializable value', () => {
|
||||
function testFunction() {}
|
||||
|
||||
const obj = {
|
||||
a: 42,
|
||||
b: {
|
||||
b1: testFunction,
|
||||
},
|
||||
c: [99, { d: 123 }],
|
||||
}
|
||||
|
||||
const result = findNonSerializableValue(obj)
|
||||
|
||||
expect(result).toEqual({ keyPath: 'b.b1', value: testFunction })
|
||||
})
|
||||
|
||||
it('Should return the first non-serializable value it finds', () => {
|
||||
const map = new Map()
|
||||
const symbol = Symbol.for('testSymbol')
|
||||
|
||||
const obj = {
|
||||
a: 42,
|
||||
b: {
|
||||
b1: 1,
|
||||
},
|
||||
c: [99, { d: 123 }, map, symbol, 'test'],
|
||||
d: symbol,
|
||||
}
|
||||
|
||||
const result = findNonSerializableValue(obj)
|
||||
|
||||
expect(result).toEqual({ keyPath: 'c.2', value: map })
|
||||
})
|
||||
|
||||
it('Should return a specific value if the root object is non-serializable', () => {
|
||||
const value = new Map()
|
||||
const result = findNonSerializableValue(value)
|
||||
|
||||
expect(result).toEqual({ keyPath: '<root>', value })
|
||||
})
|
||||
|
||||
it('Should accept null as a valid value', () => {
|
||||
const obj = {
|
||||
a: 42,
|
||||
b: {
|
||||
b1: 1,
|
||||
},
|
||||
c: null,
|
||||
}
|
||||
|
||||
const result = findNonSerializableValue(obj)
|
||||
|
||||
expect(result).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('serializableStateInvariantMiddleware', () => {
|
||||
it('Should log an error when a non-serializable action is dispatched', () => {
|
||||
const reducer: Reducer = (state = 0, _action) => state + 1
|
||||
|
||||
const serializableStateInvariantMiddleware =
|
||||
createSerializableStateInvariantMiddleware()
|
||||
|
||||
const store = configureStore({
|
||||
reducer,
|
||||
middleware: [serializableStateInvariantMiddleware],
|
||||
})
|
||||
|
||||
const type = Symbol.for('SOME_CONSTANT')
|
||||
const dispatchedAction = { type }
|
||||
|
||||
store.dispatch(dispatchedAction)
|
||||
|
||||
expect(getLog().log).toMatchInlineSnapshot(`
|
||||
"A non-serializable value was detected in an action, in the path: \`type\`. Value: Symbol(SOME_CONSTANT)
|
||||
Take a look at the logic that dispatched this action: Object {
|
||||
\\"type\\": Symbol(SOME_CONSTANT),
|
||||
}
|
||||
(See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)
|
||||
(To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)"
|
||||
`)
|
||||
})
|
||||
|
||||
it('Should log an error when a non-serializable value is in state', () => {
|
||||
const ACTION_TYPE = 'TEST_ACTION'
|
||||
|
||||
const initialState = {
|
||||
a: 0,
|
||||
}
|
||||
|
||||
const badValue = new Map()
|
||||
|
||||
const reducer: Reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case ACTION_TYPE: {
|
||||
return {
|
||||
a: badValue,
|
||||
}
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
const serializableStateInvariantMiddleware =
|
||||
createSerializableStateInvariantMiddleware()
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
testSlice: reducer,
|
||||
},
|
||||
middleware: [serializableStateInvariantMiddleware],
|
||||
})
|
||||
|
||||
store.dispatch({ type: ACTION_TYPE })
|
||||
|
||||
expect(getLog().log).toMatchInlineSnapshot(`
|
||||
"A non-serializable value was detected in the state, in the path: \`testSlice.a\`. Value: Map {}
|
||||
Take a look at the reducer(s) handling this action type: TEST_ACTION.
|
||||
(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
|
||||
`)
|
||||
})
|
||||
|
||||
describe('consumer tolerated structures', () => {
|
||||
const nonSerializableValue = new Map()
|
||||
|
||||
const nestedSerializableObjectWithBadValue = {
|
||||
isSerializable: true,
|
||||
entries: (): [string, any][] => [
|
||||
['good-string', 'Good!'],
|
||||
['good-number', 1337],
|
||||
['bad-map-instance', nonSerializableValue],
|
||||
],
|
||||
}
|
||||
|
||||
const serializableObject = {
|
||||
isSerializable: true,
|
||||
entries: (): [string, any][] => [
|
||||
['first', 1],
|
||||
['second', 'B!'],
|
||||
['third', nestedSerializableObjectWithBadValue],
|
||||
],
|
||||
}
|
||||
|
||||
it('Should log an error when a non-serializable value is nested in state', () => {
|
||||
const ACTION_TYPE = 'TEST_ACTION'
|
||||
|
||||
const initialState = {
|
||||
a: 0,
|
||||
}
|
||||
|
||||
const reducer: Reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case ACTION_TYPE: {
|
||||
return {
|
||||
a: serializableObject,
|
||||
}
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
// use default options
|
||||
const serializableStateInvariantMiddleware =
|
||||
createSerializableStateInvariantMiddleware()
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
testSlice: reducer,
|
||||
},
|
||||
middleware: [serializableStateInvariantMiddleware],
|
||||
})
|
||||
|
||||
store.dispatch({ type: ACTION_TYPE })
|
||||
|
||||
// since default options are used, the `entries` function in `serializableObject` will cause the error
|
||||
expect(getLog().log).toMatchInlineSnapshot(`
|
||||
"A non-serializable value was detected in the state, in the path: \`testSlice.a.entries\`. Value: [Function entries]
|
||||
Take a look at the reducer(s) handling this action type: TEST_ACTION.
|
||||
(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
|
||||
`)
|
||||
})
|
||||
|
||||
it('Should use consumer supplied isSerializable and getEntries options to tolerate certain structures', () => {
|
||||
const ACTION_TYPE = 'TEST_ACTION'
|
||||
|
||||
const initialState = {
|
||||
a: 0,
|
||||
}
|
||||
|
||||
const isSerializable = (val: any): boolean =>
|
||||
val.isSerializable || isPlain(val)
|
||||
const getEntries = (val: any): [string, any][] =>
|
||||
val.isSerializable ? val.entries() : Object.entries(val)
|
||||
|
||||
const reducer: Reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case ACTION_TYPE: {
|
||||
return {
|
||||
a: serializableObject,
|
||||
}
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
const serializableStateInvariantMiddleware =
|
||||
createSerializableStateInvariantMiddleware({
|
||||
isSerializable,
|
||||
getEntries,
|
||||
})
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
testSlice: reducer,
|
||||
},
|
||||
middleware: [serializableStateInvariantMiddleware],
|
||||
})
|
||||
|
||||
store.dispatch({ type: ACTION_TYPE })
|
||||
|
||||
// error reported is from a nested class instance, rather than the `entries` function `serializableObject`
|
||||
expect(getLog().log).toMatchInlineSnapshot(`
|
||||
"A non-serializable value was detected in the state, in the path: \`testSlice.a.third.bad-map-instance\`. Value: Map {}
|
||||
Take a look at the reducer(s) handling this action type: TEST_ACTION.
|
||||
(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
|
||||
`)
|
||||
})
|
||||
})
|
||||
|
||||
it('Should use the supplied isSerializable function to determine serializability', () => {
|
||||
const ACTION_TYPE = 'TEST_ACTION'
|
||||
|
||||
const initialState = {
|
||||
a: 0,
|
||||
}
|
||||
|
||||
const badValue = new Map()
|
||||
|
||||
const reducer: Reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case ACTION_TYPE: {
|
||||
return {
|
||||
a: badValue,
|
||||
}
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
const serializableStateInvariantMiddleware =
|
||||
createSerializableStateInvariantMiddleware({
|
||||
isSerializable: () => true,
|
||||
})
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
testSlice: reducer,
|
||||
},
|
||||
middleware: [serializableStateInvariantMiddleware],
|
||||
})
|
||||
|
||||
store.dispatch({ type: ACTION_TYPE })
|
||||
|
||||
// Supplied 'isSerializable' considers all values serializable, hence
|
||||
// no error logging is expected:
|
||||
expect(getLog().log).toBe('')
|
||||
})
|
||||
|
||||
it('should not check serializability for ignored action types', () => {
|
||||
let numTimesCalled = 0
|
||||
|
||||
const serializableStateMiddleware =
|
||||
createSerializableStateInvariantMiddleware({
|
||||
isSerializable: () => {
|
||||
numTimesCalled++
|
||||
return true
|
||||
},
|
||||
ignoredActions: ['IGNORE_ME'],
|
||||
})
|
||||
|
||||
const store = configureStore({
|
||||
reducer: () => ({}),
|
||||
middleware: [serializableStateMiddleware],
|
||||
})
|
||||
|
||||
expect(numTimesCalled).toBe(0)
|
||||
|
||||
store.dispatch({ type: 'IGNORE_ME' })
|
||||
|
||||
// The state check only calls `isSerializable` once
|
||||
expect(numTimesCalled).toBe(1)
|
||||
|
||||
store.dispatch({ type: 'ANY_OTHER_ACTION' })
|
||||
|
||||
// Action checks call `isSerializable` 2+ times when enabled
|
||||
expect(numTimesCalled).toBeGreaterThanOrEqual(3)
|
||||
})
|
||||
|
||||
describe('ignored action paths', () => {
|
||||
function reducer() {
|
||||
return 0
|
||||
}
|
||||
const nonSerializableValue = new Map()
|
||||
|
||||
it('default value: meta.arg', () => {
|
||||
configureStore({
|
||||
reducer,
|
||||
middleware: [createSerializableStateInvariantMiddleware()],
|
||||
}).dispatch({ type: 'test', meta: { arg: nonSerializableValue } })
|
||||
|
||||
expect(getLog().log).toMatchInlineSnapshot(`""`)
|
||||
})
|
||||
|
||||
it('default value can be overridden', () => {
|
||||
configureStore({
|
||||
reducer,
|
||||
middleware: [
|
||||
createSerializableStateInvariantMiddleware({
|
||||
ignoredActionPaths: [],
|
||||
}),
|
||||
],
|
||||
}).dispatch({ type: 'test', meta: { arg: nonSerializableValue } })
|
||||
|
||||
expect(getLog().log).toMatchInlineSnapshot(`
|
||||
"A non-serializable value was detected in an action, in the path: \`meta.arg\`. Value: Map {}
|
||||
Take a look at the logic that dispatched this action: Object {
|
||||
\\"meta\\": Object {
|
||||
\\"arg\\": Map {},
|
||||
},
|
||||
\\"type\\": \\"test\\",
|
||||
}
|
||||
(See https://redux.js.org/faq/actions#why-should-type-be-a-string-or-at-least-serializable-why-should-my-action-types-be-constants)
|
||||
(To allow non-serializable values see: https://redux-toolkit.js.org/usage/usage-guide#working-with-non-serializable-data)"
|
||||
`)
|
||||
})
|
||||
|
||||
it('can specify (multiple) different values', () => {
|
||||
configureStore({
|
||||
reducer,
|
||||
middleware: [
|
||||
createSerializableStateInvariantMiddleware({
|
||||
ignoredActionPaths: ['payload', 'meta.arg'],
|
||||
}),
|
||||
],
|
||||
}).dispatch({
|
||||
type: 'test',
|
||||
payload: { arg: nonSerializableValue },
|
||||
meta: { arg: nonSerializableValue },
|
||||
})
|
||||
|
||||
expect(getLog().log).toMatchInlineSnapshot(`""`)
|
||||
})
|
||||
|
||||
it('can specify regexp', () => {
|
||||
configureStore({
|
||||
reducer,
|
||||
middleware: [
|
||||
createSerializableStateInvariantMiddleware({
|
||||
ignoredActionPaths: [/^payload\..*$/],
|
||||
}),
|
||||
],
|
||||
}).dispatch({
|
||||
type: 'test',
|
||||
payload: { arg: nonSerializableValue },
|
||||
})
|
||||
|
||||
expect(getLog().log).toMatchInlineSnapshot(`""`)
|
||||
})
|
||||
})
|
||||
|
||||
it('allows ignoring actions entirely', () => {
|
||||
let numTimesCalled = 0
|
||||
|
||||
const serializableStateMiddleware =
|
||||
createSerializableStateInvariantMiddleware({
|
||||
isSerializable: () => {
|
||||
numTimesCalled++
|
||||
return true
|
||||
},
|
||||
ignoreActions: true,
|
||||
})
|
||||
|
||||
const store = configureStore({
|
||||
reducer: () => ({}),
|
||||
middleware: [serializableStateMiddleware],
|
||||
})
|
||||
|
||||
expect(numTimesCalled).toBe(0)
|
||||
|
||||
store.dispatch({ type: 'THIS_DOESNT_MATTER' })
|
||||
|
||||
// `isSerializable` is called once for a state check
|
||||
expect(numTimesCalled).toBe(1)
|
||||
|
||||
store.dispatch({ type: 'THIS_DOESNT_MATTER_AGAIN' })
|
||||
|
||||
expect(numTimesCalled).toBe(2)
|
||||
})
|
||||
|
||||
it('should not check serializability for ignored slice names', () => {
|
||||
const ACTION_TYPE = 'TEST_ACTION'
|
||||
|
||||
const initialState = {
|
||||
a: 0,
|
||||
}
|
||||
|
||||
const badValue = new Map()
|
||||
|
||||
const reducer: Reducer = (state = initialState, action) => {
|
||||
switch (action.type) {
|
||||
case ACTION_TYPE: {
|
||||
return {
|
||||
a: badValue,
|
||||
b: {
|
||||
c: badValue,
|
||||
d: badValue,
|
||||
},
|
||||
e: { f: badValue },
|
||||
g: {
|
||||
h: badValue,
|
||||
i: badValue,
|
||||
},
|
||||
}
|
||||
}
|
||||
default:
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
const serializableStateInvariantMiddleware =
|
||||
createSerializableStateInvariantMiddleware({
|
||||
ignoredPaths: [
|
||||
// Test for ignoring a single value
|
||||
'testSlice.a',
|
||||
// Test for ignoring a single nested value
|
||||
'testSlice.b.c',
|
||||
// Test for ignoring an object and its children
|
||||
'testSlice.e',
|
||||
// Test for ignoring based on RegExp
|
||||
/^testSlice\.g\..*$/,
|
||||
],
|
||||
})
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
testSlice: reducer,
|
||||
},
|
||||
middleware: [serializableStateInvariantMiddleware],
|
||||
})
|
||||
|
||||
store.dispatch({ type: ACTION_TYPE })
|
||||
|
||||
// testSlice.b.d was not covered in ignoredPaths, so will still log the error
|
||||
expect(getLog().log).toMatchInlineSnapshot(`
|
||||
"A non-serializable value was detected in the state, in the path: \`testSlice.b.d\`. Value: Map {}
|
||||
Take a look at the reducer(s) handling this action type: TEST_ACTION.
|
||||
(See https://redux.js.org/faq/organizing-state#can-i-put-functions-promises-or-other-non-serializable-items-in-my-store-state)"
|
||||
`)
|
||||
})
|
||||
|
||||
it('allows ignoring state entirely', () => {
|
||||
const badValue = new Map()
|
||||
let numTimesCalled = 0
|
||||
const reducer = () => badValue
|
||||
const store = configureStore({
|
||||
reducer,
|
||||
middleware: [
|
||||
createSerializableStateInvariantMiddleware({
|
||||
isSerializable: () => {
|
||||
numTimesCalled++
|
||||
return true
|
||||
},
|
||||
ignoreState: true,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
expect(numTimesCalled).toBe(0)
|
||||
|
||||
store.dispatch({ type: 'test' })
|
||||
|
||||
expect(getLog().log).toMatchInlineSnapshot(`""`)
|
||||
|
||||
// Should be called twice for the action - there is an initial check for early returns, then a second and potentially 3rd for nested properties
|
||||
expect(numTimesCalled).toBe(2)
|
||||
})
|
||||
|
||||
it('never calls isSerializable if both ignoreState and ignoreActions are true', () => {
|
||||
const badValue = new Map()
|
||||
let numTimesCalled = 0
|
||||
const reducer = () => badValue
|
||||
const store = configureStore({
|
||||
reducer,
|
||||
middleware: [
|
||||
createSerializableStateInvariantMiddleware({
|
||||
isSerializable: () => {
|
||||
numTimesCalled++
|
||||
return true
|
||||
},
|
||||
ignoreState: true,
|
||||
ignoreActions: true,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
expect(numTimesCalled).toBe(0)
|
||||
|
||||
store.dispatch({ type: 'TEST', payload: new Date() })
|
||||
store.dispatch({ type: 'OTHER_THING' })
|
||||
|
||||
expect(numTimesCalled).toBe(0)
|
||||
})
|
||||
|
||||
it('Should print a warning if execution takes too long', () => {
|
||||
const reducer: Reducer = (state = 42, action) => {
|
||||
return state
|
||||
}
|
||||
|
||||
const serializableStateInvariantMiddleware =
|
||||
createSerializableStateInvariantMiddleware({ warnAfter: 4 })
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
testSlice: reducer,
|
||||
},
|
||||
middleware: [serializableStateInvariantMiddleware],
|
||||
})
|
||||
|
||||
store.dispatch({
|
||||
type: 'SOME_ACTION',
|
||||
payload: new Array(10000).fill({ value: 'more' }),
|
||||
})
|
||||
expect(getLog().log).toMatch(
|
||||
/^SerializableStateInvariantMiddleware took \d*ms, which is more than the warning threshold of 4ms./
|
||||
)
|
||||
})
|
||||
|
||||
it('Should not print a warning if "reducer" takes too long', () => {
|
||||
const reducer: Reducer = (state = 42, action) => {
|
||||
const started = Date.now()
|
||||
while (Date.now() - started < 8) {}
|
||||
return state
|
||||
}
|
||||
|
||||
const serializableStateInvariantMiddleware =
|
||||
createSerializableStateInvariantMiddleware({ warnAfter: 4 })
|
||||
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
testSlice: reducer,
|
||||
},
|
||||
middleware: [serializableStateInvariantMiddleware],
|
||||
})
|
||||
|
||||
store.dispatch({ type: 'SOME_ACTION' })
|
||||
expect(getLog().log).toMatch('')
|
||||
})
|
||||
|
||||
it('Should cache its results', () => {
|
||||
let numPlainChecks = 0
|
||||
const countPlainChecks = (x: any) => {
|
||||
numPlainChecks++
|
||||
return isPlain(x)
|
||||
}
|
||||
|
||||
const serializableStateInvariantMiddleware =
|
||||
createSerializableStateInvariantMiddleware({
|
||||
isSerializable: countPlainChecks,
|
||||
})
|
||||
|
||||
const store = configureStore({
|
||||
reducer: (state = [], action) => {
|
||||
if (action.type === 'SET_STATE') return action.payload
|
||||
return state
|
||||
},
|
||||
middleware: [serializableStateInvariantMiddleware],
|
||||
})
|
||||
|
||||
const state = createNextState([], () =>
|
||||
new Array(50).fill(0).map((x, i) => ({ i }))
|
||||
)
|
||||
expect(isNestedFrozen(state)).toBe(true)
|
||||
|
||||
store.dispatch({
|
||||
type: 'SET_STATE',
|
||||
payload: state,
|
||||
})
|
||||
expect(numPlainChecks).toBeGreaterThan(state.length)
|
||||
|
||||
numPlainChecks = 0
|
||||
store.dispatch({ type: 'NOOP' })
|
||||
expect(numPlainChecks).toBeLessThan(10)
|
||||
})
|
||||
})
|
||||
3
node_modules/@reduxjs/toolkit/src/tests/tsconfig.json
generated
vendored
Normal file
3
node_modules/@reduxjs/toolkit/src/tests/tsconfig.json
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig.test.json"
|
||||
}
|
||||
6
node_modules/@reduxjs/toolkit/src/tests/tsconfig.typetests.json
generated
vendored
Normal file
6
node_modules/@reduxjs/toolkit/src/tests/tsconfig.typetests.json
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.test.json",
|
||||
"compilerOptions": {
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user