-
Notifications
You must be signed in to change notification settings - Fork 95
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
added TreeStateModifiers, a set of functions that help modify state
- Loading branch information
1 parent
d578c06
commit d467c8f
Showing
6 changed files
with
544 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import {getFlattenedTreePaths, doesChangeAffectFlattenedTree, isNodeExpanded} from '../selectors/getFlattenedTree'; | ||
import TreeState, {validateState, State} from './TreeState'; | ||
import {replaceNodeFromTree} from '../selectors/nodes'; | ||
|
||
/** | ||
* @callback setNode | ||
* @param {Node} node - current node value | ||
* @return {Node} The updated node | ||
*/ | ||
|
||
/** | ||
* Set of Tree State Modifiers | ||
*/ | ||
export default class TreeStateModifiers { | ||
/** | ||
* Given a state, finds a node at a certain row index. | ||
* @param {State} state - The current state | ||
* @param {number} index - The visible row index | ||
* @param {setNode} setNode - A function to update the node | ||
* @return {State} An internal state representation | ||
*/ | ||
static editNodeAt = (state, index, setNode) => { | ||
validateState(state); | ||
|
||
const node = TreeState.getNodeAt(state, index); | ||
const updatedNode = setNode(node); | ||
const flattenedTree = [...state.flattenedTree]; | ||
const flattenedNodeMap = flattenedTree[index]; | ||
const parents = flattenedNodeMap.slice(0, flattenedNodeMap.length - 1); | ||
|
||
if (doesChangeAffectFlattenedTree(node, updatedNode)) { | ||
const numberOfVisibleDescendants = TreeState.getNumberOfVisibleDescendants(state, index); | ||
|
||
if (isNodeExpanded(updatedNode)) { | ||
const updatedNodeSubTree = getFlattenedTreePaths([updatedNode], parents); | ||
|
||
flattenedTree.splice(index + 1, 0, ...updatedNodeSubTree.slice(1)); | ||
} else { | ||
flattenedTree.splice(index + 1, numberOfVisibleDescendants); | ||
} | ||
} | ||
|
||
const tree = replaceNodeFromTree(state.tree, {...updatedNode, parents}); | ||
|
||
return new State(tree, flattenedTree); | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,255 @@ | ||
import deepFreeze from 'deep-freeze'; | ||
import {diff} from 'deep-diff'; | ||
|
||
import TreeStateModifiers from '../TreeStateModifiers'; | ||
import {Nodes} from '../../../testData/sampleTree'; | ||
import TreeState from '../TreeState'; | ||
|
||
describe('TreeStateModifiers', () => { | ||
const noop = () => {}; | ||
|
||
describe('editNodeAt', () => { | ||
test('should fail when invalid state is supplied', () => { | ||
expect(() => TreeStateModifiers.editNodeAt('state', 0, noop)).toThrowError( | ||
'Expected a State instance but got string', | ||
); | ||
expect(() => TreeStateModifiers.editNodeAt(1225, 0, noop)).toThrowError( | ||
'Expected a State instance but got number', | ||
); | ||
expect(() => TreeStateModifiers.editNodeAt([], 0, noop)).toThrowError('Expected a State instance but got object'); | ||
expect(() => TreeStateModifiers.editNodeAt({}, 0, noop)).toThrowError('Expected a State instance but got object'); | ||
expect(() => TreeStateModifiers.editNodeAt(true, 0, noop)).toThrowError( | ||
'Expected a State instance but got boolean', | ||
); | ||
expect(() => TreeStateModifiers.editNodeAt(() => {}, 0, noop)).toThrowError( | ||
'Expected a State instance but got function', | ||
); | ||
}); | ||
|
||
test('should fail with descriptive error when node at index does not exist', () => { | ||
expect(() => | ||
TreeStateModifiers.editNodeAt(TreeState.createFromTree(Nodes), 20, noop), | ||
).toThrowErrorMatchingSnapshot(); | ||
}); | ||
|
||
describe('flattened tree', () => { | ||
test('should collapse a node in a root node', () => { | ||
const state = TreeState.createFromTree(Nodes); | ||
|
||
deepFreeze(state); | ||
|
||
const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 0, n => ({ | ||
...n, | ||
state: {...n.state, expanded: false}, | ||
})); | ||
|
||
expect(flattenedTree).toMatchSnapshot(); | ||
}); | ||
|
||
test('should collapse a node in a children node', () => { | ||
const state = TreeState.createFromTree(Nodes); | ||
|
||
deepFreeze(state); | ||
|
||
const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 1, n => ({ | ||
...n, | ||
state: {...n.state, expanded: false}, | ||
})); | ||
|
||
expect(flattenedTree).toMatchSnapshot(); | ||
}); | ||
|
||
test('should expand a node in a root node', () => { | ||
const state = TreeState.createFromTree(Nodes); | ||
|
||
deepFreeze(state); | ||
|
||
const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 5, n => ({ | ||
...n, | ||
state: {...n.state, expanded: true}, | ||
})); | ||
|
||
expect(flattenedTree).toMatchSnapshot(); | ||
}); | ||
|
||
test('should expand a node in a children node', () => { | ||
const state = TreeState.createFromTree(Nodes); | ||
|
||
deepFreeze(state); | ||
|
||
const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 2, n => ({ | ||
...n, | ||
state: {...n.state, expanded: true}, | ||
})); | ||
|
||
expect(flattenedTree).toMatchSnapshot(); | ||
}); | ||
|
||
test('should not change for updates that do not change state', () => { | ||
const state = TreeState.createFromTree(Nodes); | ||
|
||
deepFreeze(state); | ||
|
||
const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 2, n => ({ | ||
...n, | ||
name: 'node', | ||
})); | ||
|
||
expect(flattenedTree).toEqual(state.flattenedTree); | ||
}); | ||
|
||
test('should not change for updates that change state but not expansion', () => { | ||
const state = TreeState.createFromTree(Nodes); | ||
|
||
deepFreeze(state); | ||
|
||
const {flattenedTree} = TreeStateModifiers.editNodeAt(state, 2, n => ({ | ||
...n, | ||
state: {...n.state, favorite: true}, | ||
})); | ||
|
||
expect(flattenedTree).toEqual(state.flattenedTree); | ||
|
||
const {flattenedTree: flattenedTree2} = TreeStateModifiers.editNodeAt(state, 0, n => ({ | ||
...n, | ||
state: {...n.state, deletable: true}, | ||
})); | ||
|
||
expect(flattenedTree2).toEqual(state.flattenedTree); | ||
|
||
const {flattenedTree: flattenedTree3} = TreeStateModifiers.editNodeAt(state, 0, n => ({ | ||
...n, | ||
state: {...n.state, randomKey: true}, | ||
})); | ||
|
||
expect(flattenedTree3).toEqual(state.flattenedTree); | ||
}); | ||
}); | ||
|
||
describe('tree', () => { | ||
test('should update a node in the root and keep the rest intact', () => { | ||
const state = TreeState.createFromTree(Nodes); | ||
|
||
deepFreeze(state); | ||
|
||
const updatedName = 'Edit node 1'; | ||
|
||
// Change 'Leaf 1' | ||
const {tree} = TreeStateModifiers.editNodeAt(state, 0, n => ({ | ||
...n, | ||
name: updatedName, | ||
})); | ||
|
||
const changes = diff(state.tree, tree); | ||
|
||
expect(changes.length).toBe(1); | ||
expect(changes[0]).toMatchSnapshot(); | ||
}); | ||
|
||
test('should update a child node and keep the rest intact', () => { | ||
const state = TreeState.createFromTree(Nodes); | ||
|
||
deepFreeze(state); | ||
|
||
const updatedName = 'Edited node'; | ||
|
||
// Change 'Leaf 3' | ||
const {tree} = TreeStateModifiers.editNodeAt(state, 2, n => ({ | ||
...n, | ||
name: updatedName, | ||
})); | ||
|
||
const changes = diff(state.tree, tree); | ||
|
||
expect(changes.length).toBe(1); | ||
expect(changes[0]).toMatchSnapshot(); | ||
}); | ||
|
||
test('should update a node state in the root and keep the rest intact', () => { | ||
const state = TreeState.createFromTree(Nodes); | ||
|
||
deepFreeze(state); | ||
|
||
// Expand 'Leaf 6' | ||
const {tree} = TreeStateModifiers.editNodeAt(state, 5, n => ({ | ||
...n, | ||
state: {expanded: true}, | ||
})); | ||
|
||
const changes = diff(state.tree, tree); | ||
|
||
expect(changes).toMatchSnapshot(); | ||
}); | ||
|
||
test('should update a child node state and keep the rest intact', () => { | ||
const state = TreeState.createFromTree(Nodes); | ||
|
||
deepFreeze(state); | ||
|
||
// Collapse 'Leaf 2' | ||
const {tree} = TreeStateModifiers.editNodeAt(state, 1, n => ({ | ||
...n, | ||
state: {...n.state, expanded: false}, | ||
})); | ||
|
||
const changes = diff(state.tree, tree); | ||
|
||
expect(changes.length).toBe(1); | ||
expect(changes[0]).toMatchSnapshot(); | ||
}); | ||
|
||
test('should create state for a child node and keep the rest intact', () => { | ||
const state = TreeState.createFromTree(Nodes); | ||
|
||
deepFreeze(state); | ||
|
||
// Favorite 'Leaf 5' | ||
const {tree} = TreeStateModifiers.editNodeAt(state, 4, n => ({ | ||
...n, | ||
state: {...n.state, expanded: true}, | ||
})); | ||
|
||
const changes = diff(state.tree, tree); | ||
|
||
expect(changes.length).toBe(1); | ||
expect(changes[0]).toMatchSnapshot(); | ||
}); | ||
|
||
test('should delete state for a root node and keep the rest intact', () => { | ||
const state = TreeState.createFromTree(Nodes); | ||
|
||
deepFreeze(state); | ||
|
||
// Clear state for 'Leaf 6' | ||
const {tree} = TreeStateModifiers.editNodeAt(state, 5, n => { | ||
return Object.keys(n) | ||
.filter(k => k !== 'state') | ||
.reduce((node, k) => ({...node, [k]: n[k]}), {}); | ||
}); | ||
|
||
const changes = diff(state.tree, tree); | ||
|
||
expect(changes.length).toBe(1); | ||
expect(changes[0]).toMatchSnapshot(); | ||
}); | ||
|
||
test('should delete state for a child node and keep the rest intact', () => { | ||
const state = TreeState.createFromTree(Nodes); | ||
|
||
deepFreeze(state); | ||
|
||
// Clear state for 'Leaf 3' | ||
const {tree} = TreeStateModifiers.editNodeAt(state, 2, n => { | ||
return Object.keys(n) | ||
.filter(k => k !== 'state') | ||
.reduce((node, k) => ({...node, [k]: n[k]}), {}); | ||
}); | ||
|
||
const changes = diff(state.tree, tree); | ||
|
||
expect(changes.length).toBe(1); | ||
expect(changes[0]).toMatchSnapshot(); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.