This commit is contained in:
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])
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user