Skip to content

Commit

Permalink
feat: 馃幐 added TreeState
Browse files Browse the repository at this point in the history
added TreeState, an immutable strucuture that represents the tree state
  • Loading branch information
diogofcunha committed Jan 27, 2019
1 parent a941029 commit 33933d0
Show file tree
Hide file tree
Showing 4 changed files with 228 additions and 0 deletions.
11 changes: 11 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,15 @@ interface Selectors {
updateNode: (node: FlattenedNode, state: {[stateKey: string]: any}) => NodeAction;
}

interface State {
flattenedTree: Array<number | string>[];
tree: Node[];
}

export interface TreeState {
getNodeAt: (state: State, index: number) => Node;
getTree: (state: State) => Node[];
createFromTree: (tree: Node[]) => State;
}

export const selectors: Selectors;
71 changes: 71 additions & 0 deletions src/state/TreeState.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {getFlattenedTreePaths} from '../selectors/getFlattenedTree';
import {getNodeFromPath} from '../selectors/nodes';

class State {
flattenedTree = null;
tree = null;

constructor(tree, flattenedTree) {
this.tree = tree;
this.flattenedTree = flattenedTree || getFlattenedTreePaths(tree);
}
}

export const validateState = state => {
if (!(state instanceof State)) {
throw new Error(`Expected a State instance but got ${typeof state}`);
}
};

/**
* Immutable structure that represents the TreeState.
*/
export default class TreeState {
/**
* Given a state, finds a node at a certain row index.
* @param {State} state - The current state
* @param {number} index - The visible row index
* @return {State} An internal state representation
*/
static getNodeAt = (state, index) => {
validateState(state);

const rowPath = state.flattenedTree[index];

if (!rowPath) {
throw Error(
`Tried to get node at row "${index}" but got nothing, the tree are ${state.flattenedTree.length} visible rows`,
);
}

return getNodeFromPath(rowPath, state.tree);
};

/**
* Given a state, gets the tree
* @param {State} state - The current state
* @return {Node[]} The tree
*/
static getTree = state => {
validateState(state);

return state.tree;
};

/**
* Creates an instance of state.
* @param {Node[]} tree - The original tree
* @return {State} An internal state representation
*/
static createFromTree = tree => {
if (!tree) {
throw Error('A falsy tree was supplied in tree creation');
}

if (!Array.isArray(tree)) {
throw Error('An invalid tree was supplied in creation');
}

return new State(tree);
};
}
64 changes: 64 additions & 0 deletions src/state/__tests__/TreeState.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import TreeState from '../TreeState';
import {Nodes} from '../../../testData/sampleTree';

describe('TreeState', () => {
describe('createFromTree', () => {
test('should fail when falsy value is supplied', () => {
expect(() => TreeState.createFromTree()).toThrowError('A falsy tree was supplied in tree creation');
expect(() => TreeState.createFromTree('')).toThrowError('A falsy tree was supplied in tree creation');
expect(() => TreeState.createFromTree(0)).toThrowError('A falsy tree was supplied in tree creation');
expect(() => TreeState.createFromTree(false)).toThrowError('A falsy tree was supplied in tree creation');
});

test('should fail when an invalid value is supplied', () => {
expect(() => TreeState.createFromTree({})).toThrowError('An invalid tree was supplied in creation');
expect(() => TreeState.createFromTree('tree')).toThrowError('An invalid tree was supplied in creation');
expect(() => TreeState.createFromTree(1234)).toThrowError('An invalid tree was supplied in creation');
expect(() => TreeState.createFromTree(true)).toThrowError('An invalid tree was supplied in creation');
});

test('should create state when a valid tree is supplied', () => {
const {tree, flattenedTree} = TreeState.createFromTree(Nodes);

expect(tree).toEqual(Nodes);
expect(flattenedTree).toMatchSnapshot();
});
});

describe('getNodeAt', () => {
test('should get a for an existing rowId', () => {
expect(TreeState.getNodeAt(TreeState.createFromTree(Nodes), 0)).toBe(Nodes[0]);
expect(TreeState.getNodeAt(TreeState.createFromTree(Nodes), 1)).toMatchSnapshot('2nd row');
expect(TreeState.getNodeAt(TreeState.createFromTree(Nodes), 2)).toMatchSnapshot('3rd row');
expect(TreeState.getNodeAt(TreeState.createFromTree(Nodes), 6)).toMatchSnapshot('7th row');
});

test('should fail with a custom error when supplied rowId does not exist', () => {
expect(() => TreeState.getNodeAt(TreeState.createFromTree(Nodes), 25)).toThrowErrorMatchingSnapshot();
});

test('should fail for when invalid state is supplied', () => {
expect(() => TreeState.getNodeAt('state', 0)).toThrowError('Expected a State instance but got string');
expect(() => TreeState.getNodeAt(1225, 0)).toThrowError('Expected a State instance but got number');
expect(() => TreeState.getNodeAt([], 0)).toThrowError('Expected a State instance but got object');
expect(() => TreeState.getNodeAt({}, 0)).toThrowError('Expected a State instance but got object');
expect(() => TreeState.getNodeAt(true, 0)).toThrowError('Expected a State instance but got boolean');
expect(() => TreeState.getNodeAt(() => {}, 0)).toThrowError('Expected a State instance but got function');
});
});

describe('getTree', () => {
test('should get a tree', () => {
expect(TreeState.getTree(TreeState.createFromTree(Nodes))).toEqual(Nodes);
});

test('should fail for when invalid state is supplied', () => {
expect(() => TreeState.getTree('state')).toThrowError('Expected a State instance but got string');
expect(() => TreeState.getTree(1225)).toThrowError('Expected a State instance but got number');
expect(() => TreeState.getTree([])).toThrowError('Expected a State instance but got object');
expect(() => TreeState.getTree({})).toThrowError('Expected a State instance but got object');
expect(() => TreeState.getTree(true)).toThrowError('Expected a State instance but got boolean');
expect(() => TreeState.getTree(() => {})).toThrowError('Expected a State instance but got function');
});
});
});
82 changes: 82 additions & 0 deletions src/state/__tests__/__snapshots__/TreeState.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`TreeState createFromTree should create state when a valid tree is supplied 1`] = `
Array [
Array [
0,
],
Array [
0,
2,
],
Array [
0,
2,
3,
],
Array [
0,
2,
4,
],
Array [
0,
5,
],
Array [
1,
],
Array [
"z",
],
]
`;

exports[`TreeState getNodeAt should fail with a custom error when supplied rowId does not exist 1`] = `"Tried to get node at row \\"25\\" but got nothing, the tree are 7 visible rows"`;

exports[`TreeState getNodeAt should get a for an existing rowId: 2nd row 1`] = `
Object {
"children": Array [
Object {
"id": 3,
"name": "Leaf 3",
"state": Object {
"deletable": true,
"favorite": true,
},
},
Object {
"id": 4,
"name": "Leaf 4",
},
],
"id": 2,
"name": "Leaf 2",
"state": Object {
"deletable": true,
"expanded": true,
},
}
`;

exports[`TreeState getNodeAt should get a for an existing rowId: 3rd row 1`] = `
Object {
"id": 3,
"name": "Leaf 3",
"state": Object {
"deletable": true,
"favorite": true,
},
}
`;

exports[`TreeState getNodeAt should get a for an existing rowId: 7th row 1`] = `
Object {
"id": "z",
"name": "Leaf z",
"state": Object {
"deletable": true,
"favorite": true,
},
}
`;

0 comments on commit 33933d0

Please sign in to comment.