redux-pathspace
Version:
> Quickly & easily create path-based namespaces to add actions that map to reducers
348 lines (277 loc) • 17.5 kB
JavaScript
/* eslint global-require: 0 */
const tape = require('tape');
const { createPathspace } = require('../dist/redux-pathspace');
function isFunction(f) {
return [typeof f, 'function'];
}
tape('redux-pathspace', (t) => {
t.test('properly exports api methods', (tt) => {
const { createNamespace, createReducer, setStore, mapNamespaces } = createPathspace();
tt.equal(...isFunction(createNamespace), 'exports a `createNamespace` function');
tt.equal(...isFunction(createReducer), 'exports a `createReducer` function');
tt.equal(...isFunction(setStore), 'exports a `setStore` function');
tt.equal(...isFunction(mapNamespaces), 'exports a `mapNamespaces` function');
tt.equal(...isFunction(createPathspace), 'exports a `mapNamespaces` function');
tt.end();
});
t.test('mapNamespaces', (tt) => {
const { mapNamespaces, createReducer } = createPathspace();
const {
mapNamespaces: mapNamespacesArray, createReducer: createArrReducer,
} = createPathspace();
const {
mapNamespaces: mapNamespacesString, createReducer: createStringReducer,
} = createPathspace();
const { mapNamespaces: mapNamespacesThrow } = createPathspace();
const { createStore } = require('redux');
const initialState = {
foo: {
bar: [1, 2, 3, 4],
baz: {
zab: undefined,
},
bing: [{
name: 'hello',
}],
bang: [[{
boom: 'zing',
zoom: [{
zing: 'nested',
}],
}]],
},
};
const initialStateArray = [{
foo: 'bar',
}];
const initialStateString = 'fooBar';
const mapped = mapNamespaces(initialState);
const mappedArray = mapNamespacesArray(initialStateArray);
const mappedString = mapNamespacesString(initialStateString);
tt.equal(...isFunction(mapped.foo.examine), 'properly maps namespaces');
tt.equal(...isFunction(mapped.foo.bar.examine), 'properly maps namespaces');
tt.equal(...isFunction(mapped.foo.baz.examine), 'properly maps namespaces');
tt.equal(...isFunction(mapped.foo.baz.zab.examine), 'properly maps namespaces');
tt.equal(...isFunction(mapped.foo.bar), 'creates a function for array paths');
tt.deepEqual(mapped.foo.bar.examine(initialState), [1, 2, 3, 4], 'properly handles array indexes');
tt.equal(mapped.foo.bar(2).examine(initialState), 3, 'properly handles array indexes');
tt.equal(mapped.foo.bing(0).name.examine(initialState), 'hello', 'properly maps array item shapes');
tt.deepEqual(mapped.foo.bing(0).examine(initialState), { name: 'hello' }, 'properly maps array item shapes');
tt.equal(mapped.foo.bang(0)(0).boom.examine(initialState), 'zing', 'properly handles nested array shapes');
tt.deepEqual(mapped.foo.bang(0).examine(initialState), initialState.foo.bang[0], 'properly handles nested array shapes');
const zoom = mapped.foo.bang(0)(0).zoom(0);
tt.deepEqual(zoom.examine(initialState), initialState.foo.bang[0][0].zoom[0], 'properly handles nested array shapes');
tt.equal(zoom.zing.examine(initialState), 'nested', 'properly handles nested array shapes');
const secondIndexActionCreator = mapped.foo.bar(2).mapActionToReducer('FOO');
const superLongActionCreator = zoom.zing.mapActionToReducer('SET');
const randomIndexActionCreator = mapped.foo.bar(42).mapActionToReducer('FOO');
tt.deepEqual(secondIndexActionCreator('foo'), { type: 'foo.bar[2]:FOO', payload: 'foo', meta: {} }, 'properly handles actions');
tt.throws(() => mapped.foo.bar(2).mapActionToReducer('FOO'), 'throws when duplicate actions created');
tt.deepEqual(superLongActionCreator('foo'), { type: 'foo.bang[0][0].zoom[0].zing:SET', payload: 'foo', meta: {} }, 'properly handles deeply nested namespace actions');
const store = createStore(createReducer(initialState), initialState);
store.dispatch(superLongActionCreator('super long!'));
store.dispatch(randomIndexActionCreator('hello'));
tt.equal(zoom.zing.examine(store.getState()), 'super long!', 'properly reduces actions to the store');
tt.equal(mapped.foo.bar(42).examine(store.getState()), 'hello', 'properly reduces actions to the store');
tt.equal(mapped.foo.bar(37).examine(store.getState()), undefined, 'properly reduces actions to the store');
tt.equal(...isFunction(mappedArray.examine), 'properly maps arrays');
tt.equal(...isFunction(mappedArray(0).examine), 'properly maps arrays');
tt.equal(...isFunction(mappedArray(0).foo.examine), 'properly maps arrays');
tt.deepEqual(mappedArray.examine(initialStateArray), initialStateArray, 'properly maps arrays');
tt.deepEqual(mappedArray(0).examine(initialStateArray), initialStateArray[0], 'properly maps arrays');
tt.equal(mappedArray(0).foo.examine(initialStateArray), initialStateArray[0].foo, 'properly maps arrays');
tt.deepEqual(mappedArray.mapActionToReducer('SET')('foo'), { type: 'SET', payload: 'foo', meta: {} }, 'properly creats actions for mapped arrays');
tt.deepEqual(mappedArray(0).mapActionToReducer('SET')('foo'), { type: '[0]:SET', payload: 'foo', meta: {} }, 'properly creats actions for mapped arrays');
tt.deepEqual(mappedArray(0).foo.mapActionToReducer('SET')('foo'), { type: '[0].foo:SET', payload: 'foo', meta: {} }, 'properly creats actions for mapped arrays');
const arrayStore = createStore(createArrReducer(initialStateArray), initialStateArray);
const mappedArrFooActionCreator = mappedArray(0).foo.mapActionToReducer('YO');
arrayStore.dispatch(mappedArrFooActionCreator('yo!'));
tt.equal(mappedArray(0).foo.examine(arrayStore.getState()), 'yo!', 'properly reduces state for mapped arrays');
tt.equal(...isFunction(mappedString.examine), 'properly maps strings');
tt.equal(...isFunction(mappedString(0).examine), 'properly maps strings');
tt.equal(...isFunction(mappedString(1).examine), 'properly maps strings');
tt.equal(...isFunction(mappedString(2).examine), 'properly maps strings');
tt.equal(mappedString.examine(initialStateString), initialStateString, 'properly maps strings');
tt.equal(mappedString(0).examine(initialStateString), initialStateString[0], 'properly maps strings');
tt.equal(mappedString(1).examine(initialStateString), initialStateString[1], 'properly maps strings');
tt.equal(mappedString(2).examine(initialStateString), initialStateString[1], 'properly maps strings');
tt.deepEqual(mappedString.mapActionToReducer('SET')('foo'), { type: 'SET', payload: 'foo', meta: {} }, 'properly creats actions for mapped strings');
tt.deepEqual(mappedString(0).mapActionToReducer('SET')('foo'), { type: '[0]:SET', payload: 'foo', meta: {} }, 'properly creats actions for mapped strings');
tt.deepEqual(mappedString(1).mapActionToReducer('SET')('foo'), { type: '[1]:SET', payload: 'foo', meta: {} }, 'properly creats actions for mapped strings');
const stringStore = createStore(createStringReducer(initialStateString), initialStateString);
const mappedStringActionCreator = mappedString(3).mapActionToReducer('SET');
stringStore.dispatch(mappedStringActionCreator('T'));
tt.equal(mappedString(3).examine(stringStore.getState()), 'T', 'properly reduces state for mapped arrays');
tt.equal(mappedString.examine(stringStore.getState()), 'fooTar', 'properly reduces state for mapped arrays');
tt.throws(() => mapNamespacesThrow(0), 'throws when called with a non-object/array/string');
tt.throws(() => mapNamespacesThrow(() => {}), 'throws when called with a non-object/array/string');
tt.end();
});
t.test('setStore', (tt) => {
const { createNamespace, createReducer, setStore } = createPathspace();
const { createStore } = require('redux');
const initialState = { foo: 'bar', baz: 'zab' };
const foo = createNamespace('foo');
const baz = createNamespace('baz');
const bazActionCreator = baz.mapActionToReducer('SET', () => 'changed');
const fooActionCreator = foo.mapActionToReducer('CHANGE_BAZ_TOO')
.withSideEffect(({ dispatch }, ax) => () => { dispatch(ax.bazActionCreator()); return 'hello'; });
const actionCreators = { bazActionCreator, fooActionCreator };
const store = setStore(createStore(createReducer(initialState), initialState), actionCreators);
store.dispatch(fooActionCreator());
tt.equal(store.getState().baz, 'changed', 'properly sets store and action creators so dispatch/ation creators can be passed to side effects');
tt.end();
});
t.test('createNamespace', (createNamespaceTest) => {
let mock = createPathspace();
createNamespaceTest.doesNotThrow(() => mock.createNamespace('foo'), 'accepts a string');
createNamespaceTest.doesNotThrow(() => mock.createNamespace('foo.bar.baz'), 'accepts a stringed path representation');
createNamespaceTest.doesNotThrow(() => mock.createNamespace(['foo', 2]), 'accepts an array of strings or numbers');
createNamespaceTest.doesNotThrow(() => mock.createNamespace(0), 'accepts a number');
createNamespaceTest.throws(() => mock.createNamespace({}), Error, 'throws when passed an object');
createNamespaceTest.throws(() => mock.createNamespace(['foo', 'bar', 'baz', {}]), Error, 'throws when passed an array that does not consist of only strings or numbers');
createNamespaceTest.throws(() => mock.createNamespace('foo'), Error, 'throws when passed an existing path');
createNamespaceTest.throws(() => mock.createNamespace(['foo.bar.baz', 1]), Error, 'throws when using dot notation for path index in array');
const state = {
x: {
y: 'z',
},
};
function yReducer(slice) {
if (slice !== 'z') throw new Error();
}
const rootReducer = mock.createReducer(state);
const x = mock.createNamespace('x');
const y = mock.createNamespace('y', x);
const yActionCreator = y.mapActionToReducer('FOO', yReducer);
createNamespaceTest.doesNotThrow(() => rootReducer(state, yActionCreator()), 'should properly compose lenses');
createNamespaceTest.test('namespace', (namespaceTest) => {
mock = createPathspace();
const namespace = mock.createNamespace('x');
namespaceTest.equal(4, Object.keys(namespace).length, 'returns an object with 4 properties');
namespaceTest.equal(...isFunction(namespace.mapActionToReducer), 'returns a `mapActionToReducer` function');
namespaceTest.equal(...isFunction(namespace.examine), 'returns a `examine` function');
namespaceTest.equal(...isFunction(namespace.examine), 'provides a function');
namespaceTest.test('examine', (examineTest) => {
const fooState = {
m: 'foo',
};
const xPath = mock.createNamespace('m');
const xView = xPath.examine(fooState);
examineTest.equal(xView, 'foo', 'should properly examine path');
examineTest.end();
});
namespaceTest.test('mapActionToReducer', (mapActionToReducerTest) => {
mock = createPathspace();
const ns = mock.createNamespace('x');
mapActionToReducerTest.doesNotThrow(() => ns.mapActionToReducer('foo'), 'accepts a string');
mapActionToReducerTest.doesNotThrow(() => ns.mapActionToReducer('bar', () => {}), 'accepts an object with an optional reducer');
mapActionToReducerTest.doesNotThrow(() => ns.mapActionToReducer('baz', () => {}, {}), 'accepts an object with an optional meta property');
mapActionToReducerTest.equal(...isFunction(ns.mapActionToReducer('x')), 'returns a function');
mapActionToReducerTest.throws(() => ns.mapActionToReducer('foo'), Error, 'throws when supplied an existing actionType for the given namespace');
mapActionToReducerTest.throws(() => ns.mapActionToReducer('alpha', 0), Error, 'throws when optional reducer property is not a function');
mapActionToReducerTest.throws(() => ns.mapActionToReducer('omega', () => {}, []), Error, 'throws when optional meta property is not a plain object');
mapActionToReducerTest.test('actionCreator', (actionCreatorTest) => {
mock = createPathspace();
let actionCreator = mock.createNamespace('xPath').mapActionToReducer('FOO');
const action = actionCreator('fooBar');
actionCreatorTest.equal(action.type, 'xPath:FOO', 'returns a prefixed action.type');
actionCreatorTest.isEquivalent(action.meta, {}, 'returns a default meta object');
actionCreatorTest.equal(action.payload, 'fooBar', 'returns the supplied action.payload data');
actionCreatorTest.end();
actionCreatorTest.test('withSideEffect', (withSideEffectTest) => {
mock = createPathspace();
actionCreator = mock.createNamespace('y').mapActionToReducer('FOO');
withSideEffectTest.throws(() => actionCreator.withSideEffect(0), Error, 'throws when optional side-effecet is not a function');
actionCreator.withSideEffect(() => () => 'foo');
withSideEffectTest.equal(actionCreator().payload, 'foo', 'properly adds side effect');
withSideEffectTest.end();
});
actionCreatorTest.test('withPipeline', (withPipelineTest) => {
mock = createPathspace();
const { createStore } = require('redux');
const xState = { foo: 'foo', bar: 'bar', zab: 'zab' };
const store = createStore(mock.createReducer(xState), xState);
const nsFoo = mock.createNamespace('foo');
const nsBar = mock.createNamespace('bar');
const nsZab = mock.createNamespace('zab');
function helloWorld(x, y, z) {
return 'hello world';
}
function byeWorld(x, y, z) {
return 'bye world';
}
actionCreator = nsFoo.mapActionToReducer('hi', helloWorld).withPipeline(nsBar.wrapReducer(byeWorld));
const zabActionCreator = nsZab.mapActionToReducer('ok');
const barActionCreator = nsBar.mapActionToReducer('ok', () => 'im bar').withPipeline(zabActionCreator('yep'));
withPipelineTest.ok(typeof actionCreator.withPipeline === 'function', 'has a withPipeline method');
store.dispatch(actionCreator());
withPipelineTest.deepEqual(store.getState(), { foo: 'hello world', bar: 'bye world', zab: 'zab' }, 'properly runs basic pipeline');
store.dispatch(barActionCreator());
withPipelineTest.deepEqual(store.getState(), { foo: 'hello world', bar: 'im bar', zab: 'yep' }, 'properly runs basic pipeline');
withPipelineTest.end();
});
});
mapActionToReducerTest.end();
});
namespaceTest.test('lens', (lensTest) => {
mock = createPathspace();
const view = require('ramda/src/view');
const rState = { r: 'foo' };
const rPath = mock.createNamespace('r');
const rLens = rPath.lens;
lensTest.equal(view(rLens, rState), 'foo', 'getLens should properly provide lens');
lensTest.end();
});
namespaceTest.end();
});
createNamespaceTest.end();
});
t.test('createReducer', (tt) => {
const { createNamespace, createReducer } = createPathspace();
tt.doesNotThrow(() => createReducer(0), 'accepts any type');
tt.equal(...isFunction(createReducer()), 'returns a function');
tt.test('redux-pathspace -> createReducer -> reducer', (ttt) => {
const is = 'foo';
const reducer = createReducer(is);
ttt.equal(reducer(undefined, { type: 'bar' }), 'foo', 'returns: initial state when state is undefined');
const initialState = {
foo: {
bar: {
baz: [{ id: 1, name: 'x' }, { id: 2, name: 'y' }],
zab: 'hello',
},
},
indexPath: {
arr: ['hi'],
},
};
function pathReducerA(slice, payload, state) {
if (JSON.stringify(initialState.foo.bar) !== JSON.stringify(slice)) throw new Error();
if (JSON.stringify(initialState) !== JSON.stringify(state)) throw new Error();
if (payload !== 'foo') throw new Error();
return initialState;
}
function pathReducerB(slice) {
if (!slice.id) throw new Error();
if (slice.id !== 1) throw new Error();
const newSlice = { ...slice, id: 'foo' };
return newSlice;
}
const rootReducer = createReducer(initialState);
const actionCreator = createNamespace('foo.bar').mapActionToReducer('FOO', pathReducerA);
const indexPath = createNamespace('indexPath');
const hiPath = createNamespace(0, createNamespace('arr', indexPath));
const indexAction = hiPath.mapActionToReducer('FOO');
const actionB = createNamespace(['foo', 'bar', 'baz', 0]).mapActionToReducer('FOO', pathReducerB);
ttt.doesNotThrow(() => rootReducer(initialState, actionCreator('foo')), 'reducer: passes slice as first argument payload as second and full state as last argument');
ttt.doesNotThrow(() => rootReducer(initialState, actionB()), 'reducer: handles array-index paths');
ttt.doesNotThrow(() => rootReducer(initialState, indexAction()), 'properly handles index paths');
const newState = rootReducer(initialState, actionB());
ttt.equal(newState.foo.bar.baz[0].id, 'foo', 'root reducer properly returns modified state');
ttt.end();
});
tt.end();
});
t.end();
});