UNPKG

@reduxjs/toolkit

Version:

The official, opinionated, batteries-included toolset for efficient Redux development

355 lines (280 loc) 9.24 kB
import { Reducer } from 'redux' import { configureStore } from './configureStore' import { createSerializableStateInvariantMiddleware, findNonSerializableValue, isPlain } from './serializableStateInvariantMiddleware' 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', () => { beforeEach(() => { console.error = jest.fn() }) 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(console.error).toHaveBeenCalled() const [ message, keyPath, value, action ] = (console.error as jest.Mock).mock.calls[0] expect(message).toContain('detected in an action, in the path: `%s`') expect(keyPath).toBe('type') expect(value).toBe(type) expect(action).toBe(dispatchedAction) }) 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(console.error).toHaveBeenCalled() const [ message, keyPath, value, actionType ] = (console.error as jest.Mock).mock.calls[0] expect(message).toContain('detected in the state, in the path: `%s`') expect(keyPath).toBe('testSlice.a') expect(value).toBe(badValue) expect(actionType).toBe(ACTION_TYPE) }) 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 }) expect(console.error).toHaveBeenCalled() const [ message, keyPath, value, actionType ] = (console.error as jest.Mock).mock.calls[0] // since default options are used, the `entries` function in `serializableObject` will cause the error expect(message).toContain('detected in the state, in the path: `%s`') expect(keyPath).toBe('testSlice.a.entries') expect(value).toBe(serializableObject.entries) expect(actionType).toBe(ACTION_TYPE) }) 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 }) expect(console.error).toHaveBeenCalled() const [ message, keyPath, value, actionType ] = (console.error as jest.Mock).mock.calls[0] // error reported is from a nested class instance, rather than the `entries` function `serializableObject` expect(message).toContain('detected in the state, in the path: `%s`') expect(keyPath).toBe('testSlice.a.third.bad-map-instance') expect(value).toBe(nonSerializableValue) expect(actionType).toBe(ACTION_TYPE) }) }) 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(console.error).not.toHaveBeenCalled() }) 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' }) expect(numTimesCalled).toBe(0) store.dispatch({ type: 'ANY_OTHER_ACTION' }) expect(numTimesCalled).toBeGreaterThan(0) }) })