lucid-ui
Version:
A UI component library from AppNexus.
347 lines (305 loc) • 8.64 kB
text/typescript
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'
);
});
});
});
});
});
});