@cbinsights/react-d3-graph
Version:
React component to build interactive and configurable graphs with d3 effortlessly
522 lines (478 loc) • 18.8 kB
JavaScript
import * as graphHelper from '../../../src/components/graph/graph.helper';
import config from '../../../src/components/graph/graph.config';
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', () => {
beforeAll(() => {
utils.isDeepEqual = jest.fn();
utils.isObjectEmpty = jest.fn();
utils.merge = jest.fn();
utils.throwErr = jest.fn();
});
describe('#buildLinkProps', () => {
let that = {};
beforeAll(() => {
const linkConfig = Object.assign({}, config.link);
that = {
config: { link: linkConfig },
link: { source: 'source', target: 'target' }
};
});
describe('when building props for a link', () => {
describe('and no custom color is set', () => {
test('should return default color defined in link config', () => {
const props = graphHelper.buildLinkProps(
that.link,
{},
{},
that.config,
[],
undefined,
undefined,
1
);
expect(props.stroke).toEqual(that.config.link.color);
});
});
describe('and custom color is set to green', () => {
test('should return green color in the props', () => {
const props = graphHelper.buildLinkProps(
{ ...that.link, color: 'green' },
{},
{},
that.config,
[],
undefined,
undefined,
1
);
expect(props.stroke).toEqual('green');
});
});
});
});
describe('#buildNodeProps', () => {
let that = {};
beforeEach(() => {
const nodeConfig = Object.assign({}, config.node, { svg: 'file.svg' });
const linkConfig = Object.assign({}, config.link);
that = {
config: { node: nodeConfig, link: linkConfig },
node: {
id: 'id',
x: 1,
y: 2,
color: 'green',
highlighted: false,
symbolType: undefined
}
};
});
describe('when node to build is the highlightedNode', () => {
test('should return node props with proper highlight values', () => {
that.node.highlighted = true;
Object.assign(that.config.node, {
highlightColor: 'red',
highlightFontSize: 12,
highlightFontWeight: 'bold',
highlightStrokeColor: 'yellow',
highlightStrokeWidth: 2
});
const props = graphHelper.buildNodeProps(that.node, that.config, undefined, 'id', undefined, 1);
expect(props).toEqual({
className: 'node',
cursor: 'pointer',
cx: 1,
cy: 2,
dx: 15.5,
fill: 'red',
fontSize: 12,
fontWeight: 'bold',
fontColor: 'black',
id: 'id',
label: 'id',
onClickNode: undefined,
onMouseOut: undefined,
onMouseOverNode: undefined,
opacity: 1,
renderLabel: true,
size: 200,
stroke: 'yellow',
strokeWidth: 2,
svg: 'file.svg',
type: 'circle'
});
});
});
describe('when node to build is the highlightedLink target (or source)', () => {
describe('and highlight degree is 0', () => {
test('should properly build node props ()', () => {
that.config.highlightDegree = 0;
const props = graphHelper.buildNodeProps(
that.node,
that.config,
undefined,
{
source: 'some other id',
target: 'id'
},
undefined,
1
);
expect(props).toEqual({
className: 'node',
cursor: 'pointer',
cx: 1,
cy: 2,
dx: 11.5,
fill: 'green',
fontSize: 8,
fontWeight: 'normal',
fontColor: 'black',
id: 'id',
label: 'id',
onClickNode: undefined,
onMouseOut: undefined,
onMouseOverNode: undefined,
opacity: undefined,
renderLabel: true,
size: 200,
stroke: 'none',
strokeWidth: 1.5,
svg: 'file.svg',
type: 'circle'
});
});
});
describe('and highlight degree is bigger then 0', () => {
test('should properly build node props', () => {
that.config.highlightDegree = 2;
const props = graphHelper.buildNodeProps(
that.node,
that.config,
undefined,
{
source: 'some other id',
target: 'id'
},
undefined,
1
);
expect(props).toEqual({
className: 'node',
cursor: 'pointer',
cx: 1,
cy: 2,
dx: 11.5,
fill: 'green',
fontSize: 8,
fontWeight: 'normal',
fontColor: 'black',
id: 'id',
label: 'id',
onClickNode: undefined,
onMouseOut: undefined,
onMouseOverNode: undefined,
opacity: undefined,
renderLabel: true,
size: 200,
stroke: 'none',
strokeWidth: 1.5,
svg: 'file.svg',
type: 'circle'
});
});
});
});
});
describe('#initializeGraphState', () => {
describe('when valid graph data is provided', () => {
beforeEach(() => {
const fr = 10;
const forceStub = jest.fn();
d3ForceX.mockImplementation(() => ({ strength: () => fr }));
d3ForceY.mockImplementation(() => ({ strength: () => fr }));
d3ForceManyBody.mockImplementation(() => ({ strength: () => fr }));
forceStub.mockImplementation(() => ({ force: forceStub }));
d3ForceSimulation.mockImplementation(() => ({ force: forceStub }));
utils.merge.mockImplementation(() => ({ 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,
nodesInputSnapshot: [
{
id: 'A'
},
{
id: 'B'
},
{
id: 'C'
}
],
linksInputSnapshot: [
{
source: 'A',
target: 'B'
},
{
source: 'C',
target: 'A'
}
]
});
});
});
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'
);
});
});
});
});
});
});