diff --git a/README.md b/README.md index ef84897d9..03373f01f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# react-d3-graph · [![Build Status](https://travis-ci.org/danielcaldas/react-d3-graph.svg?branch=master)](https://travis-ci.org/danielcaldas/react-d3-graph) [![npm version](https://img.shields.io/badge/npm-v0.4.0-blue.svg)](https://www.npmjs.com/package/react-d3-graph) [![npm stats](https://img.shields.io/badge/downloads-1k+-brightgreen.svg)](https://npm-stat.com/charts.html?package=react-d3-graph&from=2017-04-25&to=2017-11-24) [![probot enabled](https://img.shields.io/badge/probot:stale-enabled-yellow.svg)](https://probot.github.io/) +# react-d3-graph · [![Build Status](https://travis-ci.org/danielcaldas/react-d3-graph.svg?branch=master)](https://travis-ci.org/danielcaldas/react-d3-graph) [![npm version](https://img.shields.io/badge/npm-v0.4.0-blue.svg)](https://www.npmjs.com/package/react-d3-graph) [![npm stats](https://img.shields.io/badge/downloads-1k-brightgreen.svg)](https://npm-stat.com/charts.html?package=react-d3-graph&from=2017-04-25&to=2017-11-24) [![probot enabled](https://img.shields.io/badge/probot:stale-enabled-yellow.svg)](https://probot.github.io/) [:book:](https://danielcaldas.github.io/react-d3-graph/docs/index.html) ### *Interactive and configurable graphs with react and d3 effortlessly* diff --git a/package.json b/package.json index 543114048..edd0891d4 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "lint:test": "node_modules/eslint/bin/eslint.js --config=.eslintrc.test.config.js \"test/**/*.test.js\"", "test": "jest --verbose --coverage", "test:clean": "jest --no-cache --updateSnapshot --verbose --coverage", - "test:watch": "jest --verbose --watchAll" + "test:watch": "jest --verbose --coverage --watchAll" }, "dependencies": { "d3": "4.10.2", @@ -45,7 +45,7 @@ "eslint-plugin-promise": "3.5.0", "eslint-plugin-standard": "2.1.1", "html-webpack-plugin": "2.30.1", - "jest": "21.1.0", + "jest": "21.3.0-beta.8", "npm-run-all": "4.1.1", "react-addons-test-utils": "15.6.0", "react-dom": "15.6.1", diff --git a/src/components/graph/helper.jsx b/src/components/graph/helper.jsx index 7b9e1c6cf..f54c00eee 100644 --- a/src/components/graph/helper.jsx +++ b/src/components/graph/helper.jsx @@ -154,34 +154,6 @@ function _buildNodeLinks(nodeId, nodes, links, config, linkCallbacks, highlighte return linksComponents; } -/** - * Get the correct node opacity in order to properly make decisions based on context such as currently highlighted node. - * @param {Object} node - the node object for whom we will generate properties. - * @param {string} highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}. - * @param {Object} highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}. - * @param {Object} config - same as {@link #buildGraph|config in buildGraph}. - * @returns {number} the opacity value for the given node. - * @memberof Graph/helper - */ -function _getNodeOpacity(node, highlightedNode, highlightedLink, config) { - const highlight = node.highlighted - || node.id === (highlightedLink && highlightedLink.source) - || node.id === (highlightedLink && highlightedLink.target); - const someNodeHighlighted = !!(highlightedNode - || highlightedLink && highlightedLink.source && highlightedLink.target); - let opacity; - - if (someNodeHighlighted && config.highlightDegree === 0) { - opacity = highlight ? config.node.opacity : config.highlightOpacity; - } else if (someNodeHighlighted) { - opacity = highlight ? config.node.opacity : config.highlightOpacity; - } else { - opacity = config.node.opacity; - } - - return opacity; -} - /** * Build some Node properties based on given parameters. * @param {Object} node - the node object for whom we will generate properties. @@ -239,6 +211,119 @@ function _buildNodeProps(node, config, nodeCallbacks, highlightedNode, highlight }; } +/** + * Get the correct node opacity in order to properly make decisions based on context such as currently highlighted node. + * @param {Object} node - the node object for whom we will generate properties. + * @param {string} highlightedNode - same as {@link #buildGraph|highlightedNode in buildGraph}. + * @param {Object} highlightedLink - same as {@link #buildGraph|highlightedLink in buildGraph}. + * @param {Object} config - same as {@link #buildGraph|config in buildGraph}. + * @returns {number} the opacity value for the given node. + * @memberof Graph/helper + */ +function _getNodeOpacity(node, highlightedNode, highlightedLink, config) { + const highlight = node.highlighted + || node.id === (highlightedLink && highlightedLink.source) + || node.id === (highlightedLink && highlightedLink.target); + const someNodeHighlighted = !!(highlightedNode + || highlightedLink && highlightedLink.source && highlightedLink.target); + let opacity; + + if (someNodeHighlighted && config.highlightDegree === 0) { + opacity = highlight ? config.node.opacity : config.highlightOpacity; + } else if (someNodeHighlighted) { + opacity = highlight ? config.node.opacity : config.highlightOpacity; + } else { + opacity = config.node.opacity; + } + + return opacity; +} + +/** + * Receives a matrix of the graph with the links source and target as concrete node instances and it transforms it + * in a lightweight matrix containing only links with source and target being strings representative of some node id + * and the respective link value (if non existent will default to 1). + * @param {Object[]} graphLinks - an array of all graph links but all the links contain the source and target nodes + * objects. + * @returns {Object.} an object containing a matrix of connections of the graph, for each nodeId, + * there is an object that maps adjacent nodes ids (string) and their values (number). + * @memberof Graph/helper + */ +function _initializeLinks(graphLinks) { + return graphLinks.reduce((links, l) => { + const source = l.source.id || l.source; + const target = l.target.id || l.target; + + if (!links[source]) { + links[source] = {}; + } + + if (!links[target]) { + links[target] = {}; + } + + // @TODO: If the graph is directed this should be adapted + links[source][target] = links[target][source] = l.value || 1; + + return links; + }, {}); +} + +/** + * Method that initialize graph nodes provided by rd3g consumer and adds additional default mandatory properties + * that are optional for the user. Also it generates an index mapping, this maps nodes ids the their index in the array + * of nodes. This is needed because d3 callbacks such as node click and link click return the index of the node. + * @param {Object[]} graphNodes - the array of nodes provided by the rd3g consumer. + * @returns {Object} returns the nodes ready to be used within rd3g with additional properties such as x, y + * and highlighted values. + * @memberof Graph/helper + */ +function _initializeNodes(graphNodes) { + let nodes = {}; + const n = graphNodes.length; + + for (let i=0; i < n; i++) { + const node = graphNodes[i]; + + node.highlighted = false; + + if (!node.hasOwnProperty('x')) { node['x'] = 0; } + if (!node.hasOwnProperty('y')) { node['y'] = 0; } + + nodes[node.id.toString()] = node; + } + + return nodes; +} + +/** + * Some integrity validations on links and nodes structure. If some validation fails the function will + * throw an error. + * @param {Object} data - Same as {@link #initializeGraphState|data in initializeGraphState}. + * @memberof Graph/helper + * @throws can throw the following error msg: + * INSUFFICIENT_DATA - msg if no nodes are provided + * INVALID_LINKS - if links point to nonexistent nodes + */ +function _validateGraphData(data) { + if (!data.nodes || !data.nodes.length) { + utils.throwErr('Graph', ERRORS.INSUFFICIENT_DATA); + } + + const n = data.links.length; + + for (let i=0; i < n; i++) { + const l = data.links[i]; + + if (!data.nodes.find(n => n.id === l.source)) { + utils.throwErr('Graph', `${ERRORS.INVALID_LINKS} - "${l.source}" is not a valid source node id`); + } + if (!data.nodes.find(n => n.id === l.target)) { + utils.throwErr('Graph', `${ERRORS.INVALID_LINKS} - "${l.target}" is not a valid target node id`); + } + } +} + /** * Method that actually is exported an consumed by Graph component in order to build all Nodes and Link * components. @@ -305,8 +390,9 @@ function buildGraph(nodes, nodeCallbacks, links, linkCallbacks, config, highligh /** * Create d3 forceSimulation to be applied on the graph.
- * https://github.com/d3/d3-force#forceSimulation
- * https://github.com/d3/d3-force#simulation_force
+ * {@link https://github.com/d3/d3-force#forceSimulation|d3-force#forceSimulation}
+ * {@link https://github.com/d3/d3-force#simulation_force|d3-force#simulation_force}
+ * Wtf is a force? {@link https://github.com/d3/d3-force#forces| here} * @param {number} width - the width of the container area of the graph. * @param {number} height - the height of the container area of the graph. * @returns {Object} returns the simulation instance to be consumed. @@ -335,7 +421,7 @@ function createForceSimulation(width, height) { function initializeGraphState({data, id, config}, state) { let graph; - validateGraphData(data); + _validateGraphData(data); if (state && state.nodes && state.links) { // absorb existent positioning @@ -353,8 +439,8 @@ function initializeGraphState({data, id, config}, state) { graph.links = data.links.map(l => Object.assign({}, l)); let newConfig = Object.assign({}, utils.merge(DEFAULT_CONFIG, config || {})); - let nodes = initializeNodes(graph.nodes); - let links = initializeLinks(graph.links); // matrix of graph connections + let nodes = _initializeNodes(graph.nodes); + let links = _initializeLinks(graph.links); // matrix of graph connections const {nodes: d3Nodes, links: d3Links} = graph; const formatedId = id.replace(/ /g, '_'); const simulation = createForceSimulation(newConfig.width, newConfig.height); @@ -374,95 +460,8 @@ function initializeGraphState({data, id, config}, state) { }; } -/** - * Receives a matrix of the graph with the links source and target as concrete node instances and it transforms it - * in a lightweight matrix containing only links with source and target being strings representative of some node id - * and the respective link value (if non existent will default to 1). - * @param {Object[]} graphLinks - an array of all graph links but all the links contain the source and target nodes - * objects. - * @returns {Object.} an object containing a matrix of connections of the graph, for each nodeId, - * there is an object that maps adjacent nodes ids (string) and their values (number). - * @memberof Graph/helper - */ -function initializeLinks(graphLinks) { - return graphLinks.reduce((links, l) => { - const source = l.source.id || l.source; - const target = l.target.id || l.target; - - if (!links[source]) { - links[source] = {}; - } - - if (!links[target]) { - links[target] = {}; - } - - // @TODO: If the graph is directed this should be adapted - links[source][target] = links[target][source] = l.value || 1; - - return links; - }, {}); -} - -/** - * Method that initialize graph nodes provided by rd3g consumer and adds additional default mandatory properties - * that are optional for the user. Also it generates an index mapping, this maps nodes ids the their index in the array - * of nodes. This is needed because d3 callbacks such as node click and link click return the index of the node. - * @param {Object[]} graphNodes - the array of nodes provided by the rd3g consumer. - * @returns {Object} returns the nodes ready to be used within rd3g with additional properties such as x, y - * and highlighted values. - * @memberof Graph/helper - */ -function initializeNodes(graphNodes) { - let nodes = {}; - const n = graphNodes.length; - - for (let i=0; i < n; i++) { - const node = graphNodes[i]; - - node.highlighted = false; - - if (!node.hasOwnProperty('x')) { node['x'] = 0; } - if (!node.hasOwnProperty('y')) { node['y'] = 0; } - - nodes[node.id.toString()] = node; - } - - return nodes; -} - -/** - * Some integrity validations on links and nodes structure. If some validation fails the function will - * throw an error. - * @param {Object} data - Same as {@link #initializeGraphState|data in initializeGraphState}. - * @memberof Graph/helper - * @throws can throw the following error msg: - * INSUFFICIENT_DATA - msg if no nodes are provided - * INVALID_LINKS - if links point to nonexistent nodes - */ -function validateGraphData(data) { - if (!data.nodes || !data.nodes.length) { - utils.throwErr('Graph', ERRORS.INSUFFICIENT_DATA); - } - - const n = data.links.length; - - for (let i=0; i < n; i++) { - const l = data.links[i]; - - if (!data.nodes.find(n => n.id === l.source)) { - utils.throwErr('Graph', `${ERRORS.INVALID_LINKS} - ${l.source} is not a valid node id`); - } - if (!data.nodes.find(n => n.id === l.target)) { - utils.throwErr('Graph', `${ERRORS.INVALID_LINKS} - ${l.target} is not a valid node id`); - } - } -} - export default { buildGraph, createForceSimulation, - initializeGraphState, - initializeLinks, - initializeNodes + initializeGraphState }; diff --git a/src/components/graph/index.jsx b/src/components/graph/index.jsx index 5b7a2a92d..02d09e476 100644 --- a/src/components/graph/index.jsx +++ b/src/components/graph/index.jsx @@ -95,6 +95,27 @@ const D3_CONST = { * onMouseOutLink={onMouseOutLink}/> */ export default class Graph extends React.Component { + /** + * Sets d3 tick function and configures other d3 stuff such as forces and drag events. + */ + _graphForcesConfig() { + this.state.simulation.nodes(this.state.d3Nodes).on('tick', this._tick); + + const forceLink = d3ForceLink(this.state.d3Links) + .id(l => l.id) + .distance(D3_CONST.LINK_IDEAL_DISTANCE) + .strength(D3_CONST.FORCE_LINK_STRENGTH); + + this.state.simulation.force(CONST.LINK_CLASS_NAME, forceLink); + + const customNodeDrag = d3Drag() + .on('start', this._onDragStart) + .on('drag', this._onDragMove) + .on('end', this._onDragEnd); + + d3Select(`#${this.state.id}-${CONST.GRAPH_WRAPPER_ID}`).selectAll('.node').call(customNodeDrag); + } + /** * Handles d3 drag 'end' event. */ @@ -263,27 +284,6 @@ export default class Graph extends React.Component { */ restartSimulation = () => !this.state.config.staticGraph && this.state.simulation.restart(); - /** - * Sets d3 tick function and configures other d3 stuff such as forces and drag events. - */ - _graphForcesConfig() { - this.state.simulation.nodes(this.state.d3Nodes).on('tick', this._tick); - - const forceLink = d3ForceLink(this.state.d3Links) - .id(l => l.id) - .distance(D3_CONST.LINK_IDEAL_DISTANCE) - .strength(D3_CONST.FORCE_LINK_STRENGTH); - - this.state.simulation.force(CONST.LINK_CLASS_NAME, forceLink); - - const customNodeDrag = d3Drag() - .on('start', this._onDragStart) - .on('drag', this._onDragMove) - .on('end', this._onDragEnd); - - d3Select(`#${this.state.id}-${CONST.GRAPH_WRAPPER_ID}`).selectAll('.node').call(customNodeDrag); - } - constructor(props) { super(props); diff --git a/src/err.js b/src/err.js index ae916faa6..1bf9d881e 100644 --- a/src/err.js +++ b/src/err.js @@ -1,9 +1,7 @@ +/*eslint max-len: ["error", 200]*/ export default { GRAPH_NO_ID_PROP: "id prop not defined! id property is mandatory and it should be unique.", - STATIC_GRAPH_DATA_UPDATE: "a static graph cannot receive new data (nodes or links).\ - Make sure config.staticGraph is set to true if you want to update graph data", - INVALID_LINKS: "you provided a invalid links data structure.\ - Links source and target attributes must point to an existent node", - INSUFFICIENT_DATA: "you have not provided enough data for react-d3-graph to render something.\ - You need to provide at least one node" + STATIC_GRAPH_DATA_UPDATE: "a static graph cannot receive new data (nodes or links). Make sure config.staticGraph is set to true if you want to update graph data", + INVALID_LINKS: "you provided a invalid links data structure. Links source and target attributes must point to an existent node", + INSUFFICIENT_DATA: "you have not provided enough data for react-d3-graph to render something. You need to provide at least one node" }; diff --git a/src/utils.js b/src/utils.js index bceac669e..875bb20f4 100644 --- a/src/utils.js +++ b/src/utils.js @@ -82,6 +82,10 @@ function isObjectEmpty(o) { function merge(o1={}, o2={}, _depth=0) { let o = {}; + if (Object.keys(o1 || {}).length === 0) { + return (o2 && !isObjectEmpty(o2)) ? o2 : {}; + } + for (let k of Object.keys(o1)) { const nestedO = !!(o2[k] && typeof o2[k] === 'object' && typeof o1[k] === 'object' && _depth < MAX_DEPTH); diff --git a/test/component/graph/graph.helper.test.js b/test/component/graph/graph.helper.test.js new file mode 100644 index 000000000..f196324d4 --- /dev/null +++ b/test/component/graph/graph.helper.test.js @@ -0,0 +1,311 @@ +import graphHelper from '../../../src/components/graph/helper'; + +jest.mock('../../../src/utils'); +import utils from '../../../src/utils'; + +jest.mock('d3-force'); +import { + forceX as d3ForceX, + forceY as d3ForceY, + forceSimulation as d3ForceSimulation, + forceManyBody as d3ForceManyBody +} from 'd3-force'; + +describe('Graph Helper', () => { + describe('#createForceSimulation', () => { + test('should properly create d3 simulation object', () => { + const fr = 10; + const forceStub = jest.fn(); + + d3ForceX.mockImplementation(() => { + return { + strength: () => fr + }; + }); + d3ForceY.mockImplementation(() => { + return { + strength: () => fr + }; + }); + d3ForceManyBody.mockImplementation(() => { + return { + strength: () => fr + }; + }); + forceStub.mockImplementation(() => { + return { + force: forceStub + }; + }); + d3ForceSimulation.mockImplementation(() => { + return { + force: forceStub + }; + }); + + graphHelper.createForceSimulation(1000, 1000); + + expect(d3ForceX).toHaveBeenCalledWith(500); + expect(d3ForceY).toHaveBeenCalledWith(500); + expect(d3ForceSimulation).toHaveBeenCalledWith(); + expect(forceStub).toHaveBeenCalledTimes(3); + expect(forceStub).toHaveBeenCalledWith('charge', fr); + expect(forceStub).toHaveBeenCalledWith('x', fr); + expect(forceStub).toHaveBeenLastCalledWith('y', fr); + }); + }); + + describe('#initializeGraphState', () => { + describe('when valid graph data is provided', () => { + beforeEach(() => { + utils.merge.mockImplementation(() => { + return { + config: 'config' + }; + }); + }); + + describe('and received state was already initialized', () => { + test('should create graph structure absorbing stored nodes behavior in state obj', () => { + const data = { + nodes: [{id: 'A'}, {id: 'B'}, {id: 'C'}], + links: [{source: 'A', target: 'B'}, {source: 'C', target: 'A'}] + }; + const state = { + nodes: { + A: { x: 20, y: 40 }, + B: { x: 40, y: 60 } + }, + links: 'links', + nodeIndexMapping: 'nodeIndexMapping' + }; + + const newState = graphHelper.initializeGraphState({data, id: 'id', config: {}}, state); + + expect(newState.d3Nodes).toEqual( + [{ + highlighted: false, + id: 'A', + x: 20, + y: 40 + }, { + highlighted: false, + id: 'B', + x: 40, + y: 60 + }, { + highlighted: false, + id: 'C', + x: 0, + y: 0 + }] + ); + expect(newState.d3Links).toEqual( + [{ + source: 'A', + target: 'B' + }, { + source: 'C', + target: 'A' + }] + ); + }); + }); + + describe('and received state is empty', () => { + test('should create new graph structure with nodes and links', () => { + const data = { + nodes: [{id: 'A'}, {id: 'B'}, {id: 'C'}], + links: [{source: 'A', target: 'B'}, {source: 'C', target: 'A'}] + }; + const state = {}; + + const newState = graphHelper.initializeGraphState({data, id: 'id', config: {}}, state); + + expect(newState.d3Nodes).toEqual( + [{ + highlighted: false, + id: 'A', + x: 0, + y: 0 + }, { + highlighted: false, + id: 'B', + x: 0, + y: 0 + }, { + highlighted: false, + id: 'C', + x: 0, + y: 0 + }] + ); + expect(newState.d3Links).toEqual( + [{ + source: 'A', + target: 'B' + }, { + source: 'C', + target: 'A' + }] + ); + }); + }); + + test('should return proper state object for given inputs', () => { + const forceStub = jest.fn(); + + forceStub.mockImplementation(() => { + return { + force: forceStub + }; + }); + + d3ForceSimulation.mockImplementation(() => { + return { + force: forceStub + }; + }); + + const data = { + nodes: [{id: 'A'}, {id: 'B'}, {id: 'C'}], + links: [{source: 'A', target: 'B'}, {source: 'C', target: 'A'}] + }; + const state = { + nodes: { + A: { x: 20, y: 40 }, + B: { x: 40, y: 60 } + }, + links: 'links', + nodeIndexMapping: 'nodeIndexMapping' + }; + + const newState = graphHelper.initializeGraphState({data, id: 'id', config: undefined}, state); + + expect(newState).toEqual( + { + config: { + config: "config" + }, + configUpdated: false, + d3Links: [{ + source: "A", + target: "B" + }, { + source: "C", + target: "A" + }], + d3Nodes: [{ + highlighted: false, + id: "A", + x: 20, + y: 40 + }, { + highlighted: false, + id: "B", + x: 40, + y: 60 + }, { + highlighted: false, + id: "C", + x: 0, + y: 0 + }], + highlightedNode: "", + id: "id", + links: { + A: { + B: 1, + C: 1 + }, + B: { + A: 1 + }, + C: { + A: 1 + } + }, + newGraphElements: false, + nodes: { + A: { + highlighted: false, + id: "A", + x: 20, + y: 40 + }, + B: { + highlighted: false, + id: "B", + x: 40, + y: 60 + }, + C: { + highlighted: false, + id: "C", + x: 0, + y: 0 + } + }, + simulation: { + force: forceStub + }, + transform: 1 + } + ); + }); + }); + + describe('when invalid graph data is provided', () => { + describe('when no nodes are provided', () => { + test('should throw INSUFFICIENT_DATA error', () => { + const data = { nodes: [], links: [] }; + + graphHelper.initializeGraphState({ + data, + id: 'id', + config: 'config' + }, 'state'); + + expect(utils.throwErr).toHaveBeenCalledWith('Graph', 'you have not provided enough data' + + ' for react-d3-graph to render something. You need to provide at least one node'); + }); + }); + + describe('when invalid link structure is found', () => { + afterEach(() => { + utils.throwErr.mockReset(); + }); + + describe('when link source references nonexistent node', () => { + test('should throw INVALID_LINKS error', () => { + const data = { nodes: [{id: 'A'}], links: [{source: 'B', target: 'A'}] }; + + graphHelper.initializeGraphState({ + data, + id: 'id', + config: 'config' + }, 'state'); + + expect(utils.throwErr).toHaveBeenCalledWith('Graph', 'you provided a invalid links data' + + ' structure. Links source and target attributes must point to an existent node - "B" is not a valid source node id'); + }); + }); + + describe('when link target references nonexistent node', () => { + test('should throw INVALID_LINKS error', () => { + const data = { nodes: [{id: 'A'}], links: [{source: 'A', target: 'B'}] }; + + graphHelper.initializeGraphState({ + data, + id: 'id', + config: 'config' + }, 'state'); + + expect(utils.throwErr).toHaveBeenCalledWith('Graph', 'you provided a invalid links data' + + ' structure. Links source and target attributes must point to an existent node - "B" is not a valid target node id'); + }); + }); + }); + }); + }); +}); diff --git a/test/graph.mock.js b/test/component/graph/graph.mock.js similarity index 100% rename from test/graph.mock.js rename to test/component/graph/graph.mock.js diff --git a/test/Graph.test.js b/test/component/graph/graph.test.js similarity index 95% rename from test/Graph.test.js rename to test/component/graph/graph.test.js index d1052fead..835b626d7 100644 --- a/test/Graph.test.js +++ b/test/component/graph/graph.test.js @@ -1,7 +1,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; -import Graph from '../src/components/graph'; +import Graph from '../../../src/components/graph'; import graphMock from './graph.mock.js'; describe('Graph Component', () => { @@ -36,10 +36,6 @@ describe('Graph Component', () => { that.tree = that.graph.toJSON(); }); - test('should be properly rendered', () => { - expect(that.tree).toMatchSnapshot(); - }); - describe('when onMouseOverNode is called', () => { const nodeOffset = 1; const nodeAdjOffset = 2; diff --git a/test/Link.test.js b/test/component/link/link.test.js similarity index 80% rename from test/Link.test.js rename to test/component/link/link.test.js index 1803e8e3c..cd2ef7c83 100644 --- a/test/Link.test.js +++ b/test/component/link/link.test.js @@ -1,7 +1,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; -import Link from '../src/components/link'; +import Link from '../../../src/components/link'; describe('Link Component', () => { let that = {}; @@ -16,10 +16,6 @@ describe('Link Component', () => { that.tree = that.link.toJSON(); }); - test('should be properly rendered', () => { - expect(that.tree).toMatchSnapshot(); - }); - test('should call callback function when onClick is performed', () => { that.tree.props.onClick(); expect(that.callbackMock).toBeCalled(); diff --git a/test/Node.test.js b/test/component/node/node.test.js similarity index 90% rename from test/Node.test.js rename to test/component/node/node.test.js index 58ef0a170..6bba20931 100644 --- a/test/Node.test.js +++ b/test/component/node/node.test.js @@ -1,7 +1,7 @@ import React from 'react'; import renderer from 'react-test-renderer'; -import Node from '../src/components/node'; +import Node from '../../../src/components/node'; describe('Node Component', () => { let that = {}; @@ -27,10 +27,6 @@ describe('Node Component', () => { that.tree = that.node.toJSON(); }); - test('should be properly rendered', () => { - expect(that.tree).toMatchSnapshot(); - }); - test('should call callback function when onClick is called', () => { that.tree.children[0].props.onClick(); expect(that.clickCallback).toBeCalled(); diff --git a/test/__snapshots__/Graph.test.js.snap b/test/snapshot/graph/__snapshots__/graph.snapshot.test.js.snap similarity index 99% rename from test/__snapshots__/Graph.test.js.snap rename to test/snapshot/graph/__snapshots__/graph.snapshot.test.js.snap index 6d4dda273..dea32ae88 100644 --- a/test/__snapshots__/Graph.test.js.snap +++ b/test/snapshot/graph/__snapshots__/graph.snapshot.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Graph Component should be properly rendered 1`] = ` +exports[`Snapshot - Graph Component should match snapshot 1`] = `
diff --git a/test/snapshot/graph/graph.mock.js b/test/snapshot/graph/graph.mock.js new file mode 100644 index 000000000..725eebdc9 --- /dev/null +++ b/test/snapshot/graph/graph.mock.js @@ -0,0 +1,196 @@ +export default { + nodes: [{ + id: 'Harry', + x: '40', + y: '200' + }, { + id: 'Sally', + x: '140', + y: '20' + }, { + id: 'Mario', + x: '20', + y: '110' + }, { + id: 'Sarah', + x: '30', + y: '184' + }, { + id: 'Alice', + x: '305', + y: '745' + }, { + id: 'Eveie', + x: '200', + y: '300' + }, { + id: 'Peter', + x: '120', + y: '270' + }, { + id: 'James', + x: '190', + y: '609' + }, { + id: 'Carol', + x: '101', + y: '201' + }, { + id: 'Nicky', + x: '2', + y: '200' + }, { + id: 'Bobby', + x: '404', + y: '404' + }, { + id: 'Frank', + x: '14', + y: '30' + }, { + id: 'Lynne', + x: '14', + y: '250' + }, { + id: 'Roger', + x: '14', + y: '259' + }, { + id: 'Maddy', + x: '409', + y: '500' + }, { + id: 'Sonny', + x: '520', + y: '123' + }, { + id: 'Johan', + x: '14', + y: '20' + }, { + id: 'Henry', + x: '20', + y: '1' + }, { + id: 'Mikey', + x: '90', + y: '90' + }, { + id: 'Elric', + x: '90', + y: '656' + }], + links: [{ + source: 'Harry', + target: 'Sally', + value: '1.2' + }, { + source: 'Harry', + target: 'Mario', + value: '1.3' + }, { + source: 'Sarah', + target: 'Alice', + value: '0.2' + }, { + source: 'Eveie', + target: 'Alice', + value: '0.5' + }, { + source: 'Peter', + target: 'Alice', + value: '1.6' + }, { + source: 'Mario', + target: 'Alice', + value: '0.4' + }, { + source: 'James', + target: 'Alice', + value: '0.6' + }, { + source: 'Harry', + target: 'Carol', + value: '0.7' + }, { + source: 'Harry', + target: 'Nicky', + value: '0.8' + }, { + source: 'Bobby', + target: 'Frank', + value: '0.8' + }, { + source: 'Alice', + target: 'Mario', + value: '0.7' + }, { + source: 'Harry', + target: 'Lynne', + value: '0.5' + }, { + source: 'Sarah', + target: 'James', + value: '1.9' + }, { + source: 'Roger', + target: 'James', + value: '1.1' + }, { + source: 'Maddy', + target: 'James', + value: '0.3' + }, { + source: 'Sonny', + target: 'Roger', + value: '0.5' + }, { + source: 'James', + target: 'Roger', + value: '1.5' + }, { + source: 'Alice', + target: 'Peter', + value: '1.1' + }, { + source: 'Johan', + target: 'Peter', + value: '1.6' + }, { + source: 'Alice', + target: 'Eveie', + value: '0.5' + }, { + source: 'Harry', + target: 'Eveie', + value: '0.1' + }, { + source: 'Eveie', + target: 'Harry', + value: '2.0' + }, { + source: 'Henry', + target: 'Mikey', + value: '0.4' + }, { + source: 'Elric', + target: 'Mikey', + value: '0.6' + }, { + source: 'James', + target: 'Sarah', + value: '1.5' + }, { + source: 'Alice', + target: 'Sarah', + value: '0.6' + }, { + source: 'James', + target: 'Maddy', + value: '0.5' + }, { + source: 'Peter', + target: 'Johan', + value: '0.7' + }] +}; diff --git a/test/snapshot/graph/graph.snapshot.test.js b/test/snapshot/graph/graph.snapshot.test.js new file mode 100644 index 000000000..459bd0318 --- /dev/null +++ b/test/snapshot/graph/graph.snapshot.test.js @@ -0,0 +1,42 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; + +import Graph from '../../../src/components/graph'; +import graphMock from './graph.mock.js'; + +describe('Snapshot - Graph Component', () => { + let that = {}; + + that.nodeColor = 'red'; + that.highlightColor = 'blue'; + that.svgSize = 600; + that.highlightOpacity = 0.1; + that.config = { + height: that.svgSize, + width: that.svgSize, + nodeHighlightBehavior: true, + highlightOpacity: that.highlightOpacity, + staticGraph: true, + node: { + color: that.nodeColor, + highlightColor: that.highlightColor, + size: 100 + }, + link: { + highlightColor: that.highlightColor + } + }; + that.mouseOverNodeCallback = jest.fn(); + + beforeEach(() => { + that.graph = renderer.create( + + ); + + that.tree = that.graph.toJSON(); + }); + + test('should match snapshot', () => { + expect(that.tree).toMatchSnapshot(); + }); +}); diff --git a/test/__snapshots__/Link.test.js.snap b/test/snapshot/link/__snapshots__/link.snapshot.test.js.snap similarity index 81% rename from test/__snapshots__/Link.test.js.snap rename to test/snapshot/link/__snapshots__/link.snapshot.test.js.snap index 3ebad8b5e..d8486e5d3 100644 --- a/test/__snapshots__/Link.test.js.snap +++ b/test/snapshot/link/__snapshots__/link.snapshot.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Link Component should be properly rendered 1`] = ` +exports[`Snapshot - Link Component should match snapshot 1`] = ` { + let that = {}; + + beforeEach(() => { + that.callbackMock = jest.fn(); + + that.link = renderer.create( + + ); + + that.tree = that.link.toJSON(); + }); + + test('should match snapshot', () => { + expect(that.tree).toMatchSnapshot(); + }); +}); diff --git a/test/__snapshots__/Node.test.js.snap b/test/snapshot/node/__snapshots__/node.snapshot.test.js.snap similarity index 90% rename from test/__snapshots__/Node.test.js.snap rename to test/snapshot/node/__snapshots__/node.snapshot.test.js.snap index fe74ec5de..27b48b0ea 100644 --- a/test/__snapshots__/Node.test.js.snap +++ b/test/snapshot/node/__snapshots__/node.snapshot.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Node Component should be properly rendered 1`] = ` +exports[`Snapshot - Node Component should match snapshot 1`] = ` { + let that = {}; + + beforeEach(() => { + that.clickCallback = jest.fn(); + that.mouseOverCallback = jest.fn(); + that.mouseOutCallback = jest.fn(); + + that.node = renderer.create( + + ); + + that.tree = that.node.toJSON(); + }); + + test('should match snapshot', () => { + expect(that.tree).toMatchSnapshot(); + }); +}); diff --git a/test/utils.test.js b/test/utils.test.js index fdd41c8e2..113d249de 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -1,7 +1,7 @@ import utils from '../src/utils'; describe('Utils', () => { - describe('merge', () => { + describe('#merge', () => { let that = {}; beforeEach(() => { @@ -34,19 +34,30 @@ describe('Utils', () => { expect(r.b.c.e).toEqual('test'); expect(r.b.c.f).toEqual(12); }); + + test('should merge properly: o1 is null', () => { + const r = utils.merge(null, that.o); + + expect(r.a).toEqual(1); + expect(r.b.c.d).toEqual([1, 2, {m: 'test'}]); + expect(r.b.c.e).toEqual('test'); + expect(r.b.c.f).toEqual(12); + }); }); - test('isObjectEmpty', () => { - expect(utils.isObjectEmpty({ a: 1, b: {}})).toEqual(false); - expect(utils.isObjectEmpty({ a: 1 })).toEqual(false); - expect(utils.isObjectEmpty(null)).toEqual(false); - expect(utils.isObjectEmpty(undefined)).toEqual(false); - expect(utils.isObjectEmpty(0)).toEqual(false); - expect(utils.isObjectEmpty('test')).toEqual(false); - expect(utils.isObjectEmpty({})).toEqual(true); + describe('#isObjectEmpty', () => { + test('should properly check whether the object is empty or not', () => { + expect(utils.isObjectEmpty({ a: 1, b: {}})).toEqual(false); + expect(utils.isObjectEmpty({ a: 1 })).toEqual(false); + expect(utils.isObjectEmpty(null)).toEqual(false); + expect(utils.isObjectEmpty(undefined)).toEqual(false); + expect(utils.isObjectEmpty(0)).toEqual(false); + expect(utils.isObjectEmpty('test')).toEqual(false); + expect(utils.isObjectEmpty({})).toEqual(true); + }); }); - describe('isDeepEqual', () => { + describe('#isDeepEqual', () => { let that = {}; beforeEach(() => { @@ -89,6 +100,13 @@ describe('Utils', () => { }; }); + test('should return true if o1 and o2 references are the same', () => { + const o1 = {}; + const o2 = o1; + + expect(utils.isDeepEqual(o1, o2)).toEqual(true); + }); + test('should return true if no modifications are performed', () => { expect(utils.isDeepEqual(that.o1, that.o2)).toEqual(true); }); @@ -161,4 +179,17 @@ describe('Utils', () => { expect(utils.isDeepEqual(that.o1, that.o2)).toEqual(false); }); }); + + describe('#throwErr', () => { + test('should throw error', () => { + const c = 'some component'; + const msg = 'err message'; + + try { + utils.throwErr(c, msg); + } catch (err) { + expect(err.message).toEqual('react-d3-graph :: some component :: err message'); + } + }); + }); });