UNPKG

lucid-ui

Version:

A UI component library from AppNexus.

347 lines (305 loc) 8.64 kB
jest.mock('./logger'); import assert from 'assert'; import sinon from 'sinon'; import { isFunction, upperCase } from 'lodash'; import { cleanArgs, thunk, getReduxPrimitives } from './redux'; describe('redux utils', () => { describe('#cleanArgs', () => { it('it should remove the last element if it has an `event` property', () => { assert.deepEqual(cleanArgs(['foo', 'bar', { event: 'event' }]), [ 'foo', 'bar', ]); assert.deepEqual(cleanArgs(['foo', 'bar', { event: null }]), [ 'foo', 'bar', ]); }); it('it should not remove the last element if it has no `event` property', () => { assert.deepEqual(cleanArgs(['foo', 'bar']), ['foo', 'bar']); }); }); describe('#thunk', () => { it('should set `isThunk` property on the input function to `true`', () => { assert(thunk(function () {}).isThunk, 'must have `isThunk`'); }); }); describe('#getReduxPrimitives', () => { const reducers = { foo: { onChange: (_state: any, payload: any) => { return { value: payload }; }, bar: { onChange: (_state: any, payload: any) => ({ value: payload }), }, }, }; const initialState = { foo: { value: 'foo', bar: { value: null, }, }, }; describe('reducer', () => { const { reducer } = getReduxPrimitives({ reducers, initialState }); it('should return initialState on unmatched type', () => { const action = { type: 'UNKNOWN' }; assert.deepEqual( reducer(initialState, action), initialState, 'must deep equal initialState' ); }); it('should correctly apply state change', () => { const action = { type: 'foo.onChange', payload: 'bar' }; const nextState: any = reducer(initialState, action); assert.equal(nextState.foo.value, 'bar', 'must equal action payload'); }); describe('nested reducer', () => { it('should correctly apply state change', () => { const action = { type: 'foo.bar.onChange', payload: 'baz' }; const nextState: any = reducer(initialState, action); assert.equal( nextState.foo.bar.value, 'baz', 'must equal action payload' ); }); }); describe('with rootPath', () => { const { reducer: reducerWithRootPath } = getReduxPrimitives({ reducers, initialState, rootPath: ['root'], }); it('should correctly apply state change', () => { const action = { type: 'root.foo.onChange', payload: 'bar' }; const nextState: any = reducerWithRootPath(initialState, action); assert.equal(nextState.foo.value, 'bar', 'must equal action payload'); }); }); describe('thunks', () => { it('should not include thunk paths in reducer', () => { const action = { type: 'root.foo.asyncOperation' }; const nextState: any = reducer(initialState, action); assert.deepEqual( initialState, nextState, 'must deep equal initialState' ); }); }); }); describe('connectors', () => { describe('selector/mapStateToProps', () => { const selectors = { foo: { uppercase: ({ value }: any) => upperCase(value), }, }; const { connectors: [mapStateToProps], } = getReduxPrimitives({ reducers, initialState, selectors, }); it('should apply selector', () => { const viewState = mapStateToProps(initialState, null, null); assert.equal(viewState.foo.uppercase, 'FOO', 'must equal "FOO"'); }); it('should pass state through unharmed if selectors are undefined', () => { const { connectors: [mapStateToProps], } = getReduxPrimitives({ reducers, initialState, }); const viewState = mapStateToProps(initialState, null, null); assert.deepEqual(viewState, initialState); }); describe('rootPath', () => { const { connectors: [mapStateToProps], } = getReduxPrimitives({ reducers, initialState, selectors, rootPath: ['root'], }); it('should apply selector', () => { const viewState = mapStateToProps( { root: initialState }, null, null ); assert.equal(viewState.foo.uppercase, 'FOO', 'must equal "FOO"'); }); }); describe('rootSelector', () => { const { connectors: [mapStateToProps], } = getReduxPrimitives({ reducers, initialState, selectors, rootSelector: (state) => ({ ...state, computed: state.foo.uppercase + state.foo.value, }), }); it('should apply rootSelector', () => { const viewState = mapStateToProps(initialState, null, null); assert.equal(viewState.computed, 'FOOfoo', 'must equal "FOOfoo"'); }); }); }); describe('dispatchTree/mapDispatchToProps', () => { describe('synchronous dispatch', () => { const rootState = { qux: { quux: { foo: { value: 'foo', bar: { value: null, }, }, }, }, }; const initialState = rootState.qux.quux; const { connectors } = getReduxPrimitives({ reducers, initialState, rootPath: ['qux', 'quux'], }); const mockDispatch: any = sinon.spy((action) => action); const mapDispatchToProps = connectors[1]; const dispatchTree = mapDispatchToProps(mockDispatch, null, null); beforeEach(() => { mockDispatch.reset(); }); it('should dispatch the correct action', () => { dispatchTree.foo.onChange('bar', 'baz'); const dispatchedAction = mockDispatch.getCall(0).args[0]; assert.deepEqual( dispatchedAction, { type: 'qux.quux.foo.onChange', payload: 'bar', meta: ['baz'], }, 'must include path as type, first param as payload, and subsequent params as meta' ); }); }); describe('thunks', () => { const thunkSpy: any = sinon.spy(); const reducers = { foo: { onChange: (state: any, payload: any) => { return { value: payload }; }, bar: { onChange: (state: any, payload: any) => ({ value: payload }), }, asyncOperation: thunk( (payload) => (dispatchTree: { onChange: (arg0: any) => any }) => dispatchTree.onChange(payload) ), thunkSpy: thunk(() => thunkSpy), }, }; const rootState = { qux: { quux: { foo: { value: 'foo', bar: { value: null, }, }, }, }, }; const initialState = rootState.qux.quux; const { connectors } = getReduxPrimitives({ reducers, initialState, rootPath: ['qux', 'quux'], }); const extraArgs = ['rest1', 'rest2']; const mapDispatchToProps = connectors[1]; const mockGetState = sinon.spy(() => rootState); const mockDispatch: any = sinon.spy((action) => isFunction(action) ? action(mockDispatch, mockGetState, ...extraArgs) : action ); const dispatchTree = mapDispatchToProps(mockDispatch, null, null); beforeEach(() => { thunkSpy.reset(); dispatchTree.foo.asyncOperation('qux'); }); it('should dispatch a thunk', () => { const dispatchedThunk = mockDispatch.getCall(0).args[0]; assert(isFunction(dispatchedThunk), 'must be a function'); }); it('should dispatch the correct action', () => { const dispatchedAction = mockDispatch.getCall(1).args[0]; assert.deepEqual( dispatchedAction, { type: 'qux.quux.foo.onChange', payload: 'qux', meta: [], }, 'must include path as type and param on payload' ); }); it('should call the thunk with the correct arguments', () => { dispatchTree.foo.thunkSpy(); const { args: [ localDispatchTree, getLocalState, dispatch, getState, ...rest ], } = thunkSpy.getCall(0); assert.equal( dispatchTree.foo, localDispatchTree, 'must be called with local dispatchTree' ); assert.equal( getLocalState(), rootState.qux.quux.foo, 'must be called with getLocalState' ); assert.equal( dispatch, mockDispatch, 'must be called with redux.dispatch' ); assert.equal( getState, mockGetState, 'must be called with redux.getState' ); assert.deepEqual( rest, extraArgs, 'must pass through extra arguments' ); }); }); }); }); }); });