UNPKG

bdn-pocket

Version:

pocket tools for managing redux and redux-saga

323 lines (277 loc) 6.87 kB
import R from 'ramda' import test from 'ava' import is from '@stamp/is' import Selector from '../src/selector/selector' const state = { entities: { user: { me: { name: 'arnaud' } }, comments: { one: { owner: 'me', text: 'hello', } } }, users: { result: ['me'] }, comments: { me: { result: ['one'] } } } test('Selector - simple select', t => { const comments = Selector .selectors({ comPag: state => state.comments.me.result, comEnt: state => state.entities.comments, }) .create({ reducer({ comPag, comEnt }) { return comPag.map( comId => comEnt[comId] ) } }) t.deepEqual( comments(state, { user: 'me' }), [state.entities.comments.one], 'same as reselect.createStructuredSelector with more semantics' ) }) test('Selector - no props', t => { const comments = Selector .selectors({ comPag: state => state.comments.me.result, comEnt: state => state.entities.comments, }) .create({ reducer({ comPag, comEnt }) { return comPag.map( comId => comEnt[comId] ) } }) const s = comments(state) t.true( typeof s === 'function', 'return a partial selector waiting for props (even empty)' ) t.deepEqual( s({}), [state.entities.comments.one], 'selector return selection with empty object as argument' ) t.deepEqual( s(), [state.entities.comments.one], 'selector return selection with no argument' ) t.deepEqual( s({coucou: 'hello'}), [state.entities.comments.one], 'selector return selection with object argument but not empty' ) }) test('Selector with props', t => { const comments = Selector .propTypes('user') .selectors({ comPag: state => state.comments, comEnt: state => state.entities.comments, }) .create({ reducer({ comPag, comEnt }, { user }) { const userComments = comPag[user] return userComments.result.map( comId => comEnt[comId] ) } }) t.deepEqual( comments(state, { user: 'me' }), [state.entities.comments.one], 'allow reselection with props as arguments' ) }) test('Selector memoization', t => { let reducerCallCount = 0 const comments = Selector .propTypes('user') .selectors({ comPag: state => state.comments, comEnt: state => state.entities.comments, }) .create({ reducer({ comPag, comEnt }, { user }) { reducerCallCount++ const userComments = comPag[user] return userComments ? userComments.result.map( comId => comEnt[comId] ) : [] } }) let res1 = comments(state, { user: 'me' }) // nb call: 1 t.true( res1 === comments(state, { user: 'me' }), 'last call is memoized and return same ref with same state (ref comparison) & same props (deepEq comparison)' ) t.true( res1 !== comments(state, { user: 'not me' }), // nb call: 2 'new props value invalidate memoization' ) // restart memo res1 = comments(state, { user: 'me' }) // nb call: 3 // change state outside of comments state shape selectors let newState = R.evolve({ entities: { users: { me: R.merge(R.__, { name: 'paz'} ) } } }, state) t.true( res1 === comments(newState, { user: 'me' }), // nb call: 3 'if sub state of selector remain unchanged will return same result as previous' ) // change sub state in commets state selectors newState = R.evolve({ entities: { comments: { one: R.merge(R.__, { text: 'blabla'} ) } } }, newState) t.true( res1 !== comments(newState, { user: 'me' }), // nb call: 4 'if sub state of selector change it will return different result' ) t.is( reducerCallCount, 4, 'reducer is called 4 times in this scenario' ) }) test('Selector partial', t => { const comments = Selector .propTypes('user') .selectors({ comPag: R.path(['comments']), comEnt: R.path(['entities', 'comments']), }) .create({ reducer({ comPag, comEnt }, { user }) { return { res: 'my result' } } }) const fn1 = comments(state) t.true( is.isFunction(fn1), 'Selector return a partial selector if props arg is missing' ) t.is( fn1({ user: 'me' }), comments(state, { user: 'me' }), 'Partial selector use same memoization that full args selector' ) t.is( fn1, comments(state), 'Partial selector is memoized once - same state' ) t.not( fn1, comments({}), 'Partial selector is memoized once - state changed' ) }) test('Selector as subselector', t => { let comRedCalled = false let userRedCalled = false const comments = Selector .propTypes('user') .selectors({ comEnt: R.path(['entities', 'comments']), }) .create({ reducer() { comRedCalled = true return ['comment', 'result'] } }) const user = Selector .propTypes('user') .selectors({ userEnt: R.path(['entities', 'user']), commentSel: comments }) .create({ reducer({ commentSel }, { user }) { userRedCalled = true return { user: ['user', 'result'], comments: commentSel({ user }), } } }) function compareCalled(pComRedCalled, pUserRedCalled ) { const c = { comRedCalled, userRedCalled } comRedCalled = false userRedCalled = false return R.equals( c, { comRedCalled: pComRedCalled, userRedCalled: pUserRedCalled } ) } function memoCheck(state, step) { user(state, { user: 'me'} ) t.true( compareCalled(false, false), `On others calls reducers are called if props and state remain the same - check ${step}` ) } let newState = state user(newState) t.true( compareCalled(false, false), 'Reducers are not called if props not sent' ) user(newState, { user: 'me'} ) t.true( compareCalled(true, true), 'On first call with props, reducers are called' ) memoCheck(newState, 1) newState = R.evolve({ entities: { comments: { one: R.merge(R.__, {text: 'youhou'}) } } }, state) user(newState, { user: 'me' } ) t.true( compareCalled(true, true), 'if state of sub selector is changed, sub reducer and parent reducer are called' ) memoCheck(newState, 2) // update state slice of user selector newState = R.assocPath( ['entities', 'user', 'notme'], { name: 'gab' }, newState ) user(newState, { user: 'me'} ) t.true( compareCalled(false, true), 'if only state of parent selector is changed, sub reducer is not called' ) memoCheck(newState, 3) })