bdn-pocket
Version:
pocket tools for managing redux and redux-saga
323 lines (277 loc) • 6.87 kB
JavaScript
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)
})