Feature(map): Filter challenges on Map

closes #9346
This commit is contained in:
Berkeley Martinez
2016-07-14 17:13:48 -07:00
parent ff64349e9a
commit 0995e3bba6
12 changed files with 708 additions and 79 deletions

View File

@ -7,16 +7,21 @@ import { Panel } from 'react-bootstrap';
import Challenge from './Challenge.jsx';
import { toggleThisPanel } from '../../redux/actions';
import {
makePanelOpenSelector,
makePanelHiddenSelector
} from '../../redux/selectors';
const dispatchActions = { toggleThisPanel };
const mapStateToProps = createSelector(
const makeMapStateToProps = () => createSelector(
(_, props) => props.dashedName,
(state, props) => state.entities.block[props.dashedName],
state => state.entities.challenge,
(state, props) => state.challengesApp.mapUi[props.dashedName],
(dashedName, block, challengeMap, isOpen) => {
makePanelOpenSelector(),
makePanelHiddenSelector(),
(dashedName, block, isOpen, isHidden) => {
return {
isOpen,
isHidden,
dashedName,
title: block.title,
time: block.time,
@ -35,6 +40,7 @@ export class Block extends PureComponent {
dashedName: PropTypes.string,
time: PropTypes.string,
isOpen: PropTypes.bool,
isHidden: PropTypes.bool,
challenges: PropTypes.array,
toggleThisPanel: PropTypes.func
};
@ -79,8 +85,12 @@ export class Block extends PureComponent {
time,
dashedName,
isOpen,
isHidden,
challenges
} = this.props;
if (isHidden) {
return null;
}
return (
<Panel
bsClass='map-accordion-panel-nested'
@ -98,4 +108,4 @@ export class Block extends PureComponent {
}
}
export default connect(mapStateToProps, dispatchActions)(Block);
export default connect(makeMapStateToProps, dispatchActions)(Block);

View File

@ -7,16 +7,19 @@ import classnames from 'classnames';
import debug from 'debug';
import { updateCurrentChallenge } from '../../redux/actions';
import { makePanelHiddenSelector } from '../../redux/selectors';
const bindableActions = { updateCurrentChallenge };
const mapStateToProps = createSelector(
const makeMapStateToProps = () => createSelector(
(_, props) => props.dashedName,
state => state.entities.challenge,
(dashedName, challengeMap) => {
makePanelHiddenSelector(),
(dashedName, challengeMap, isHidden) => {
const challenge = challengeMap[dashedName] || {};
return {
dashedName,
challenge,
isHidden,
title: challenge.title,
block: challenge.block,
isLocked: challenge.isLocked,
@ -27,6 +30,7 @@ const mapStateToProps = createSelector(
};
}
);
export class Challenge extends PureComponent {
constructor(...args) {
super(...args);
@ -39,6 +43,7 @@ export class Challenge extends PureComponent {
isLocked: PropTypes.bool,
isRequired: PropTypes.bool,
isCompleted: PropTypes.bool,
isHidden: PropTypes.bool,
challenge: PropTypes.object,
updateCurrentChallenge: PropTypes.func
};
@ -95,9 +100,13 @@ export class Challenge extends PureComponent {
isCompleted,
isComingSoon,
isDev,
isHidden,
challenge,
updateCurrentChallenge
} = this.props;
if (isHidden) {
return null;
}
const challengeClassName = classnames({
'text-primary': true,
'padded-ionic-icon': true,
@ -133,4 +142,4 @@ export class Challenge extends PureComponent {
}
}
export default connect(mapStateToProps, bindableActions)(Challenge);
export default connect(makeMapStateToProps, bindableActions)(Challenge);

View File

@ -27,6 +27,7 @@ export class Header extends PureComponent {
constructor(...props) {
super(...props);
this.handleKeyDown = this.handleKeyDown.bind(this);
this.handleClearButton = this.handleClearButton.bind(this);
}
static displayName = 'MapHeader';
static propTypes = {
@ -44,18 +45,22 @@ export class Header extends PureComponent {
}
}
renderSearchAddon(filter, clearFilter) {
handleClearButton(e) {
e.preventDefault();
this.props.clearFilter();
}
renderSearchAddon(filter) {
if (!filter) {
return searchIcon;
}
return <span onClick={ clearFilter }>{ clearIcon }</span>;
return <span onClick={this.handleClearButton }>{ clearIcon }</span>;
}
render() {
const {
filter,
updateFilter,
clearFilter,
collapseAll,
expandAll,
isAllCollapsed
@ -100,7 +105,7 @@ export class Header extends PureComponent {
value={ filter }
/>
<InputGroup.Addon>
{ this.renderSearchAddon(filter, clearFilter) }
{ this.renderSearchAddon(filter) }
</InputGroup.Addon>
</InputGroup>
</Row>

View File

@ -7,20 +7,35 @@ import { Panel } from 'react-bootstrap';
import Block from './Block.jsx';
import { toggleThisPanel } from '../../redux/actions';
import {
makePanelOpenSelector,
makePanelHiddenSelector
} from '../../redux/selectors';
const dispatchActions = { toggleThisPanel };
const mapStateToProps = createSelector(
(_, props) => props.dashedName,
(state, props) => state.entities.superBlock[props.dashedName],
(state, props) => state.challengesApp.mapUi[props.dashedName],
(dashedName, superBlock, isOpen) => ({
isOpen,
dashedName,
title: superBlock.title,
blocks: superBlock.blocks,
message: superBlock.message
})
);
// make selectors unique to each component
// see
// reactjs/reselect
// sharing-selectors-with-props-across-multiple-components
const makeMapStateToProps = () => {
const panelOpenSelector = makePanelOpenSelector();
const panelHiddenSelector = makePanelHiddenSelector();
return createSelector(
(_, props) => props.dashedName,
(state, props) => state.entities.superBlock[props.dashedName],
panelOpenSelector,
panelHiddenSelector,
(dashedName, superBlock, isOpen, isHidden) => ({
isOpen,
isHidden,
dashedName,
title: superBlock.title,
blocks: superBlock.blocks,
message: superBlock.message
})
);
};
export class SuperBlock extends PureComponent {
constructor(...props) {
super(...props);
@ -32,6 +47,7 @@ export class SuperBlock extends PureComponent {
dashedName: PropTypes.string,
blocks: PropTypes.array,
isOpen: PropTypes.bool,
isHidden: PropTypes.bool,
message: PropTypes.string,
toggleThisPanl: PropTypes.func
};
@ -82,8 +98,12 @@ export class SuperBlock extends PureComponent {
dashedName,
blocks,
message,
isOpen
isOpen,
isHidden
} = this.props;
if (isHidden) {
return null;
}
return (
<Panel
bsClass='map-accordion-panel'
@ -107,6 +127,6 @@ export class SuperBlock extends PureComponent {
}
export default connect(
mapStateToProps,
makeMapStateToProps,
dispatchActions
)(SuperBlock);

View File

@ -43,27 +43,7 @@ export const updateFilter = createAction(
e => e.target.value
);
function createMapKey(map, key) {
map[key] = true;
return map;
}
export const initMap = createAction(
types.initMap,
(
{ superBlock: superBlockMap },
superBlocks
) => {
if (!superBlocks || !superBlockMap) {
return {};
}
const blocks = superBlocks
.map(superBlock => superBlockMap[superBlock].blocks)
.reduce((blocks, block) => blocks.concat(block));
return superBlocks
.concat(blocks)
.reduce(createMapKey, {});
}
);
export const initMap = createAction(types.initMap);
export const toggleThisPanel = createAction(types.toggleThisPanel);
export const collapseAll = createAction(types.collapseAll);
export const expandAll = createAction(types.expandAll);

View File

@ -7,6 +7,7 @@ import {
updateCurrentChallenge,
initMap
} from './actions';
import { createMapUi } from '../utils';
import {
delayedRedirect,
createErrorObservable
@ -72,7 +73,7 @@ export default function fetchChallengesSaga(action$, getState, { services }) {
createNameIdMap(entities),
result
),
initMap(entities, result),
initMap(createMapUi(entities, result)),
);
})
.catch(createErrorObservable);

View File

@ -4,6 +4,7 @@ import nextChallengeSaga from './next-challenge-saga';
import answerSaga from './answer-saga';
import resetChallengeSaga from './reset-challenge-saga';
import bugSaga from './bug-saga';
import mapUiSaga from './map-ui-saga';
export * as actions from './actions';
export reducer from './reducer';
@ -17,5 +18,6 @@ export const sagas = [
nextChallengeSaga,
answerSaga,
resetChallengeSaga,
bugSaga
bugSaga,
mapUiSaga
];

View File

@ -0,0 +1,34 @@
import types from './types';
import { initMap } from './actions';
import { unfilterMapUi, applyFilterToMap } from '../utils';
export default function mapUiSaga(actions$, getState) {
return actions$
.filter(({ type }) => (
type === types.updateFilter ||
type === types.clearFilter
))
.debounce(250)
.map(({ payload: filter = '' }) => filter)
.distinctUntilChanged()
.map(filter => {
const { challengesApp: { mapUi = {} } } = getState();
let newMapUi;
if (filter.length <= 3) {
newMapUi = unfilterMapUi(mapUi);
} else {
const regexString = filter
// replace spaces with any key to match dashes
.replace(/ /g, '.')
// makes search more fuzzy (thanks @xRahul)
.split('')
.join('.*');
const filterRegex = new RegExp(regexString, 'i');
newMapUi = applyFilterToMap(mapUi, filterRegex);
}
if (!newMapUi || newMapUi === mapUi) {
return null;
}
return initMap(newMapUi);
});
}

View File

@ -8,7 +8,10 @@ import {
buildSeed,
createTests,
getPreFile,
getFileKey
getFileKey,
toggleThisPanel,
collapseAllPanels,
expandAllPanels
} from '../utils';
const initialUiState = {
@ -180,7 +183,7 @@ const mainReducer = handleActions(
mouse: [ 0, 0 ]
}),
[types.videoCompleted]: (state, { payload: userAnswer } ) => ({
[types.videoCompleted]: (state, { payload: userAnswer }) => ({
...state,
isCorrect: true,
isPressed: false,
@ -238,35 +241,37 @@ const filesReducer = handleActions(
);
// {
// // show
// [(super)BlockName]: { open: Boolean }
// // do not show
// [(super)BlockName]: null
// children: [...{
// name: (superBlock: String),
// isOpen: Boolean,
// isHidden: Boolean,
// children: [...{
// name: (blockName: String),
// isOpen: Boolean,
// isHidden: Boolean,
// children: [...{
// name: (challengeName: String),
// isHidden: Boolean
// }]
// }]
// }
// }
const mapReducer = handleActions(
{
[types.initMap]: (state, { payload }) => ({
...state,
...payload
}),
[types.toggleThisPanel]: (state, { payload }) => ({
...state,
[payload]: !state[payload]
}),
[types.collapseAll]: state => ({
...Object.keys(state).reduce((newState, key) => {
newState[key] = false;
return newState;
}, {}),
isAllCollapsed: true
}),
[types.expandAll]: state => ({
...Object.keys(state).reduce((newState, key) => {
newState[key] = true;
return newState;
}, {}),
isAllCollapsed: false
})
[types.initMap]: (state, { payload }) => payload,
[types.toggleThisPanel]: (state, { payload: name }) => {
return toggleThisPanel(state, name);
},
[types.collapseAll]: state => {
const newState = collapseAllPanels(state);
newState.isAllCollapsed = true;
return newState;
},
[types.expandAll]: state => {
const newState = expandAllPanels(state);
newState.isAllCollapsed = false;
return newState;
}
},
initialState.mapUi
);

View File

@ -1,5 +1,6 @@
import * as challengeTypes from '../../../utils/challengeTypes';
import { createSelector } from 'reselect';
import * as challengeTypes from '../../../utils/challengeTypes';
import { getNode } from '../utils';
const viewTypes = {
[ challengeTypes.html]: 'classic',
@ -52,3 +53,21 @@ export const challengeSelector = createSelector(
};
}
);
export const makePanelOpenSelector = () => createSelector(
state => state.challengesApp.mapUi,
(_, props) => props.dashedName,
(mapUi, name) => {
const node = getNode(mapUi, name);
return node ? node.isOpen : true;
}
);
export const makePanelHiddenSelector = () => createSelector(
state => state.challengesApp.mapUi,
(_, props) => props.dashedName,
(mapUi, name) => {
const node = getNode(mapUi, name);
return node ? node.isHidden : false;
}
);

View File

@ -284,3 +284,224 @@ export function getMouse(e, [dx, dy]) {
return [pageX - dx, pageY - dy];
}
const emptyProtector = {
blocks: [],
challenges: []
};
// protect against malformed data
function protect(block) {
// if no block or block has no challenges or blocks
// use protector
if (!block || !(block.challenges || block.blocks)) {
return emptyProtector;
}
return block;
}
// interface Node {
// isHidden: Boolean,
// children: Void|[ ...Node ],
// isOpen?: Boolean
// }
//
// interface MapUi
// {
// children: [...{
// name: (superBlock: String),
// isOpen: Boolean,
// isHidden: Boolean,
// children: [...{
// name: (blockName: String),
// isOpen: Boolean,
// isHidden: Boolean,
// children: [...{
// name: (challengeName: String),
// isHidden: Boolean
// }]
// }]
// }]
// }
export function createMapUi(
{ superBlock: superBlockMap, block: blockMap } = {},
superBlocks
) {
if (!superBlocks || !superBlockMap || !blockMap) {
return {};
}
return {
children: superBlocks.map(superBlock => {
return {
name: superBlock,
isOpen: true,
isHidden: false,
children: protect(superBlockMap[superBlock]).blocks.map(block => {
return {
name: block,
isOpen: true,
isHidden: false,
children: protect(blockMap[block]).challenges.map(challenge => {
return {
name: challenge,
isHidden: false,
children: null
};
})
};
})
};
})
};
}
// synchronise
// traverseMapUi(
// tree: MapUi|Node,
// update: ((MapUi|Node) => MapUi|Node)
// ) => MapUi|Node
export function traverseMapUi(tree, update) {
let childrenChanged;
if (!Array.isArray(tree.children)) {
return update(tree);
}
const newChildren = tree.children.map(node => {
const newNode = traverseMapUi(node, update);
if (!childrenChanged && newNode !== node) {
childrenChanged = true;
}
return newNode;
});
if (childrenChanged) {
tree = {
...tree,
children: newChildren
};
}
return update(tree);
}
// synchronise
// getNode(tree: MapUi, name: String) => MapUi
export function getNode(tree, name) {
let node;
traverseMapUi(tree, thisNode => {
if (thisNode.name === name) {
node = thisNode;
}
return thisNode;
});
return node;
}
// synchronise
// updateSingelNode(
// tree: MapUi,
// name: String,
// update(MapUi|Node) => MapUi|Node
// ) => MapUi
export function updateSingleNode(tree, name, update) {
return traverseMapUi(tree, node => {
if (name !== node.name) {
return node;
}
return update(node);
});
}
// synchronise
// toggleThisPanel(tree: MapUi, name: String) => MapUi
export function toggleThisPanel(tree, name) {
return updateSingleNode(tree, name, node => {
return {
...node,
isOpen: !node.isOpen
};
});
}
// toggleAllPanels(tree: MapUi, isOpen: Boolean = false ) => MapUi
export function toggleAllPanels(tree, isOpen = false) {
return traverseMapUi(tree, node => {
if (!Array.isArray(node.children) || node.isOpen === isOpen) {
return node;
}
return {
...node,
isOpen
};
});
}
// collapseAllPanels(tree: MapUi) => MapUi
export function collapseAllPanels(tree) {
return toggleAllPanels(tree);
}
// expandAllPanels(tree: MapUi) => MapUi
export function expandAllPanels(tree) {
return toggleAllPanels(tree, true);
}
// applyFilterToMap(tree: MapUi, filterRegex: RegExp) => MapUi
export function applyFilterToMap(tree, filterRegex) {
return traverseMapUi(
tree,
node => {
// no children indicates a challenge node
// if leaf (challenge) then test if regex is a match
if (!Array.isArray(node.children)) {
// does challenge name meet filter criteria?
if (filterRegex.test(node.name)) {
// is challenge currently hidden?
if (node.isHidden) {
// unhide challenge, it matches
return {
...node,
isHidden: false
};
}
} else if (!node.isHidden) {
return {
...node,
isHidden: true
};
}
return node;
}
// if not leaf node (challenge) then
// test to see if all its children are hidden
if (node.children.every(node => node.isHidden)) {
if (node.isHidden) {
return node;
}
return {
...node,
isHidden: true
};
} else if (node.isHidden) {
return {
...node,
isHidden: false
};
}
// nothing has changed
return node;
}
);
}
// unfilterMapUi(tree: MapUi) => MapUi
export function unfilterMapUi(tree) {
return traverseMapUi(
tree,
node => {
if (!node.isHidden) {
return node;
}
return {
...node,
isHidden: false
};
}
);
}

View File

@ -1,8 +1,18 @@
import test from 'tape';
import sinon from 'sinon';
import {
getNextChallenge,
getFirstChallengeOfNextBlock,
getFirstChallengeOfNextSuperBlock
getFirstChallengeOfNextSuperBlock,
createMapUi,
traverseMapUi,
getNode,
updateSingleNode,
toggleThisPanel,
expandAllPanels,
collapseAllPanels,
applyFilterToMap,
unfilterMapUi
} from './utils.js';
@ -774,4 +784,317 @@ test('common/app/routes/challenges/utils', function(t) {
);
});
});
t.test('createMapUi', t => {
t.plan(3);
t.test('should return an `{}` when proper args not supplied', t => {
t.plan(3);
t.equal(
Object.keys(createMapUi()).length,
0
);
t.equal(
Object.keys(createMapUi({}, [])).length,
0
);
t.equal(
Object.keys(createMapUi({ superBlock: {} }, [])).length,
0
);
});
t.test('should return a map tree', t => {
const expected = {
children: [{
name: 'superBlockA',
children: [{
name: 'blockA',
children: [{
name: 'challengeA'
}]
}]
}]
};
const actual = createMapUi({
superBlock: {
superBlockA: {
blocks: [
'blockA'
]
}
},
block: {
blockA: {
challenges: [
'challengeA'
]
}
}
}, ['superBlockA']);
t.plan(3);
t.equal(actual.children[0].name, expected.children[0].name);
t.equal(
actual.children[0].children[0].name,
expected.children[0].children[0].name
);
t.equal(
actual.children[0].children[0].children[0].name,
expected.children[0].children[0].children[0].name
);
});
t.test('should protect against malformed data', t => {
t.plan(2);
t.equal(
createMapUi({
superBlock: {},
block: {
blockA: {
challenges: [
'challengeA'
]
}
}
}, ['superBlockA']).children[0].children.length,
0
);
t.equal(
createMapUi({
superBlock: {
superBlockA: {
blocks: [
'blockA'
]
}
},
block: {}
}, ['superBlockA']).children[0].children[0].children.length,
0
);
});
});
t.test('traverseMapUi', t => {
t.test('should return tree', t => {
t.plan(2);
const expectedTree = {};
const actaulTree = traverseMapUi(expectedTree, tree => {
t.equal(tree, expectedTree);
return tree;
});
t.equal(actaulTree, expectedTree);
});
t.test('should hit every node', t => {
t.plan(4);
const expected = { children: [{ children: [{}] }] };
const spy = sinon.spy(t => t);
spy.withArgs(expected);
spy.withArgs(expected.children[0]);
spy.withArgs(expected.children[0].children[0]);
traverseMapUi(expected, spy);
t.equal(spy.callCount, 3);
t.ok(spy.withArgs(expected).calledOnce, 'foo');
t.ok(spy.withArgs(expected.children[0]).calledOnce, 'bar');
t.ok(spy.withArgs(expected.children[0].children[0]).calledOnce, 'baz');
});
t.test('should create new object when children change', t => {
t.plan(9);
const expected = { children: [{ bar: true }, {}] };
const actual = traverseMapUi(expected, node => ({ ...node, foo: true }));
t.notEqual(actual, expected);
t.notEqual(actual.children, expected.children);
t.notEqual(actual.children[0], expected.children[0]);
t.notEqual(actual.children[1], expected.children[1]);
t.equal(actual.children[0].bar, expected.children[0].bar);
t.notOk(expected.children[0].foo);
t.notOk(expected.children[1].foo);
t.true(actual.children[0].foo);
t.true(actual.children[1].foo);
});
});
t.test('getNode', t => {
t.test('should return node', t => {
t.plan(1);
const expected = { name: 'foo' };
const tree = { children: [{ name: 'notfoo' }, expected ] };
const actual = getNode(tree, 'foo');
t.equal(expected, actual);
});
t.test('should returned undefined if not found', t => {
t.plan(1);
const tree = {
children: [ { name: 'foo' }, { children: [ { name: 'bar' } ] } ]
};
const actual = getNode(tree, 'baz');
t.notOk(actual);
});
});
t.test('updateSingleNode', t => {
t.test('should update single node', t => {
const expected = { name: 'foo' };
const untouched = { name: 'notFoo' };
const actual = updateSingleNode(
{ children: [ untouched, expected ] },
'foo',
node => ({ ...node, tag: true })
);
t.plan(4);
t.ok(actual.children[1].tag);
t.equal(actual.children[1].name, expected.name);
t.notEqual(actual.children[1], expected);
t.equal(actual.children[0], untouched);
});
});
t.test('toggleThisPanel', t => {
t.test('should update single node', t => {
const expected = { name: 'foo', isOpen: true };
const actual = toggleThisPanel(
{ children: [ { name: 'foo', isOpen: false }] },
'foo'
);
t.plan(1);
t.deepLooseEqual(actual.children[0], expected);
});
});
t.test('toggleAllPanels', t => {
t.test('should add `isOpen: true` to every node without children', t => {
const expected = {
isOpen: true,
children: [{
isOpen: true,
children: [{}, {}]
}]
};
const actual = expandAllPanels({ children: [{ children: [{}, {}] }] });
t.plan(1);
t.deepLooseEqual(actual, expected);
});
t.test('should add `isOpen: false` to every node without children', t => {
const leaf = {};
const expected = {
isOpen: false,
children: [{
isOpen: false,
children: [{}, leaf]
}]
};
const actual = collapseAllPanels(
{ isOpen: true, children: [{ children: [{}, leaf]}]},
);
t.plan(2);
t.deepLooseEqual(actual, expected);
t.equal(actual.children[0].children[1], leaf);
});
});
t.test('applyFilterToMap', t => {
t.test('should not touch child that is already hidden', t => {
t.plan(1);
const expected = { name: 'bar', isHidden: true };
const actual = applyFilterToMap(
expected,
/foo/
);
t.equal(actual, expected);
});
t.test('should update child that is hidden', t => {
t.plan(1);
const expected = { name: 'bar', isHidden: false };
const input = { name: 'bar', isHidden: true };
const actual = applyFilterToMap(input, /bar/);
t.deepLooseEqual(actual, expected);
});
t.test('should unhide child that matches filter regex', t => {
t.plan(1);
const expected = { name: 'foo' };
const actual = applyFilterToMap({ name: 'foo' }, /foo/);
t.deepLooseEqual(actual, expected);
});
t.test('should hide child that does not match filter', t => {
t.plan(1);
const expected = { name: 'bar', isHidden: true };
const actual = applyFilterToMap({ name: 'bar' }, /foo/);
t.deepLooseEqual(actual, expected);
});
t.test('should not touch node that is already hidden', t => {
t.plan(1);
const expected = {
name: 'bar',
isHidden: true,
children: [
{ name: 'baz', isHidden: true },
{ name: 'baz2', isHidden: true }
]
};
const actual = applyFilterToMap(expected, /foo/);
t.equal(actual, expected);
});
t.test('should not touch node that is unhidden', t => {
t.plan(1);
const expected = {
name: 'bar',
isHidden: false,
children: [
{ name: 'baz', isHidden: true },
{ name: 'foo', isHidden: false }
]
};
const actual = applyFilterToMap(expected, /foo/);
t.equal(actual, expected);
});
t.test('should hide node if all children are hidden', t => {
t.plan(1);
const input = {
name: 'bar',
isHidden: false,
children: [
{ name: 'baz' },
{ name: 'baz2', isHidden: false }
]
};
const expected = {
name: 'bar',
isHidden: true,
children: [
{ name: 'baz', isHidden: true },
{ name: 'baz2', isHidden: true }
]
};
const actual = applyFilterToMap(input, /foo/);
t.deepLooseEqual(actual, expected);
});
t.test('should unhide node some children unhidden', t => {
t.plan(1);
const input = {
name: 'bar',
isHidden: true,
children: [
{ name: 'baz', isHidden: true },
{ name: 'foo', isHidden: false }
]
};
const expected = {
name: 'bar',
isHidden: false,
children: [
{ name: 'baz', isHidden: true },
{ name: 'foo', isHidden: false }
]
};
const actual = applyFilterToMap(input, /foo/);
t.deepLooseEqual(actual, expected);
});
});
t.test('unfilterMapUi', t => {
t.test('should not touch node that is already hidden', t => {
const expected = { isHidden: false };
const actual = unfilterMapUi(expected);
t.plan(1);
t.equal(actual, expected);
});
t.test('should update node that is not hidden', t => {
const expected = { isHidden: false };
const input = { isHidden: true };
const actual = unfilterMapUi(input);
t.plan(2);
t.notEqual(actual, input);
t.deepLooseEqual(actual, expected);
});
});
});