Files
weave-scope/client/app/scripts/reducers/__tests__/root-test.js
2019-01-09 13:59:15 +00:00

735 lines
24 KiB
JavaScript

import { is, fromJS } from 'immutable';
import { TABLE_VIEW_MODE } from '../../constants/naming';
import { constructEdgeId } from '../../utils/layouter-utils';
import { highlightedEdgeIdsSelector } from '../../selectors/graph-view/decorators';
// Root reducer test suite using Jasmine matchers
describe('RootReducer', () => {
const ActionTypes = require('../../constants/action-types').default;
const reducer = require('../root').default;
const { initialState } = require('../root');
const topologyUtils = require('../../utils/topology-utils');
const topologySelectors = require('../../selectors/topology');
// TODO maybe extract those to topology-utils tests?
const { activeTopologyOptionsSelector } = topologySelectors;
const { getAdjacentNodes, isNodesDisplayEmpty, isTopologyNodeCountZero } = topologyUtils;
const { getUrlState } = require('../../utils/router-utils');
// fixtures
const NODE_SET = {
n1: {
adjacency: ['n1', 'n2'],
filtered: false,
id: 'n1',
},
n2: {
filtered: false,
id: 'n2',
}
};
const topologies = [
{
fullName: 'Processes',
hide_if_empty: true,
id: 'processes',
name: 'Processes',
options: [
{
defaultValue: 'hide',
id: 'unconnected',
options: [
{
label: 'Unconnected nodes hidden',
value: 'hide'
}
],
selectType: 'one'
}
],
rank: 1,
stats: {
edge_count: 379,
filtered_nodes: 214,
node_count: 320,
nonpseudo_node_count: 320
},
sub_topologies: [],
url: '/api/topology/processes'
},
{
hide_if_empty: true,
name: 'Pods',
options: [
{
defaultValue: 'default',
id: 'namespace',
options: [
{
label: 'monitoring',
value: 'monitoring'
},
{
label: 'scope',
value: 'scope'
},
{
label: 'All Namespaces',
value: 'all'
}
],
selectType: 'many'
},
{
defaultValue: 'hide',
id: 'pseudo',
options: [
{
label: 'Show Unmanaged',
value: 'show'
},
{
label: 'Hide Unmanaged',
value: 'hide'
}
]
}
],
rank: 3,
stats: {
edge_count: 15,
filtered_nodes: 16,
node_count: 32,
nonpseudo_node_count: 27
},
sub_topologies: [
{
hide_if_empty: true,
name: 'services',
options: [
{
defaultValue: 'default',
id: 'namespace',
options: [
{
label: 'monitoring',
value: 'monitoring'
},
{
label: 'scope',
value: 'scope'
},
{
label: 'All Namespaces',
value: 'all'
}
],
selectType: 'many'
}
],
rank: 0,
stats: {
edge_count: 14,
filtered_nodes: 16,
node_count: 159,
nonpseudo_node_count: 154
},
url: '/api/topology/services'
}
],
url: '/api/topology/pods'
}
];
// actions
const ChangeTopologyOptionAction = {
option: 'option1',
topologyId: 'topo1',
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
value: ['on']
};
const ChangeTopologyOptionAction2 = {
option: 'option1',
topologyId: 'topo1',
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
value: ['off']
};
const ClickNodeAction = {
nodeId: 'n1',
type: ActionTypes.CLICK_NODE
};
const ClickNode2Action = {
nodeId: 'n2',
type: ActionTypes.CLICK_NODE
};
const ClickRelativeAction = {
nodeId: 'rel1',
type: ActionTypes.CLICK_RELATIVE
};
const ClickShowTopologyForNodeAction = {
nodeId: 'rel1',
topologyId: 'topo2',
type: ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE
};
const ClickSubTopologyAction = {
topologyId: 'topo1-grouped',
type: ActionTypes.CLICK_TOPOLOGY
};
const ClickTopologyAction = {
topologyId: 'topo1',
type: ActionTypes.CLICK_TOPOLOGY
};
const ClickTopology2Action = {
topologyId: 'topo2',
type: ActionTypes.CLICK_TOPOLOGY
};
const CloseWebsocketAction = {
type: ActionTypes.CLOSE_WEBSOCKET
};
const deSelectNode = {
type: ActionTypes.DESELECT_NODE
};
const OpenWebsocketAction = {
type: ActionTypes.OPEN_WEBSOCKET
};
const ReceiveNodesDeltaAction = {
delta: {
add: [{
adjacency: ['n1', 'n2'],
id: 'n1'
}, {
id: 'n2'
}]
},
type: ActionTypes.RECEIVE_NODES_DELTA
};
const ReceiveNodesDeltaUpdateAction = {
delta: {
remove: ['n2'],
update: [{
adjacency: ['n1'],
id: 'n1'
}]
},
type: ActionTypes.RECEIVE_NODES_DELTA
};
const ReceiveTopologiesAction = {
topologies: [{
name: 'Topo1',
options: [{
defaultValue: 'off',
id: 'option1',
options: [
{value: 'on'},
{value: 'off'}
]
}],
stats: {
node_count: 1
},
sub_topologies: [{
name: 'topo 1 grouped',
url: '/topo1-grouped'
}],
url: '/topo1'
}, {
name: 'Topo2',
stats: {
node_count: 0
},
sub_topologies: [{
name: 'topo 2 sub',
url: '/topo2-sub'
}],
url: '/topo2'
}],
type: ActionTypes.RECEIVE_TOPOLOGIES
};
const ReceiveTopologiesHiddenAction = {
topologies: [{
name: 'Topo1',
stats: {
node_count: 1
},
url: '/topo1'
}, {
hide_if_empty: true,
name: 'Topo2',
stats: { filtered_nodes: 0, node_count: 0 },
sub_topologies: [{
hide_if_empty: true,
name: 'topo 2 sub',
stats: { filtered_nodes: 0, node_count: 0 },
url: '/topo2-sub',
}],
url: '/topo2'
}],
type: ActionTypes.RECEIVE_TOPOLOGIES
};
const RouteAction = {
state: {},
type: ActionTypes.ROUTE_TOPOLOGY
};
const ChangeInstanceAction = {
type: ActionTypes.CHANGE_INSTANCE
};
// Basic tests
it('returns initial state', () => {
const nextState = reducer(undefined, {});
expect(is(nextState, initialState)).toBeTruthy();
});
// topology tests
it('init with no topologies', () => {
const nextState = reducer(undefined, {});
expect(nextState.get('topologies').size).toBe(0);
expect(nextState.get('currentTopology')).toBeFalsy();
});
it('get current topology', () => {
let nextState = initialState;
nextState = reducer(nextState, ReceiveTopologiesAction);
nextState = reducer(nextState, ClickTopologyAction);
expect(nextState.get('topologies').size).toBe(2);
expect(nextState.get('currentTopology').get('name')).toBe('Topo1');
expect(nextState.get('currentTopology').get('url')).toBe('/topo1');
expect(nextState.get('currentTopology').get('options').first().get('id')).toEqual('option1');
expect(nextState.getIn(['currentTopology', 'options']).toJS()).toEqual([{
defaultValue: 'off',
id: 'option1',
options: [
{ value: 'on'},
{ value: 'off'}
],
selectType: 'one'
}]);
});
it('get sub-topology', () => {
let nextState = initialState;
nextState = reducer(nextState, ReceiveTopologiesAction);
nextState = reducer(nextState, ClickSubTopologyAction);
expect(nextState.get('topologies').size).toBe(2);
expect(nextState.get('currentTopology').get('name')).toBe('topo 1 grouped');
expect(nextState.get('currentTopology').get('url')).toBe('/topo1-grouped');
expect(nextState.get('currentTopology').get('options')).toBeUndefined();
});
// topology options
it('changes topology option', () => {
let nextState = initialState;
nextState = reducer(nextState, ReceiveTopologiesAction);
nextState = reducer(nextState, ClickTopologyAction);
// default options
expect(activeTopologyOptionsSelector(nextState).has('option1')).toBeTruthy();
expect(activeTopologyOptionsSelector(nextState).get('option1')).toBeInstanceOf(Array);
expect(activeTopologyOptionsSelector(nextState).get('option1')).toEqual(['off']);
expect(getUrlState(nextState).topologyOptions).toBeUndefined();
// turn on
nextState = reducer(nextState, ChangeTopologyOptionAction);
expect(activeTopologyOptionsSelector(nextState).get('option1')).toEqual(['on']);
expect(getUrlState(nextState).topologyOptions.topo1.option1).toEqual(['on']);
// turn off
nextState = reducer(nextState, ChangeTopologyOptionAction2);
expect(activeTopologyOptionsSelector(nextState).get('option1')).toEqual(['off']);
expect(getUrlState(nextState).topologyOptions).toBeUndefined();
// sub-topology should retain main topo options
nextState = reducer(nextState, ClickSubTopologyAction);
expect(activeTopologyOptionsSelector(nextState).get('option1')).toEqual(['off']);
expect(getUrlState(nextState).topologyOptions).toBeUndefined();
// other topology w/o options dont return options, but keep in app state
nextState = reducer(nextState, ClickTopology2Action);
expect(activeTopologyOptionsSelector(nextState).size).toEqual(0);
expect(getUrlState(nextState).topologyOptions).toBeUndefined();
});
it('adds/removes a topology option', () => {
const addAction = {
option: 'namespace',
topologyId: 'services',
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
value: ['default', 'scope'],
};
const removeAction = {
option: 'namespace',
topologyId: 'services',
type: ActionTypes.CHANGE_TOPOLOGY_OPTION,
value: ['default']
};
let nextState = initialState;
nextState = reducer(nextState, { topologies, type: ActionTypes.RECEIVE_TOPOLOGIES});
nextState = reducer(nextState, { topologyId: 'services', type: ActionTypes.CLICK_TOPOLOGY });
nextState = reducer(nextState, addAction);
expect(activeTopologyOptionsSelector(nextState).toJS()).toEqual({
namespace: ['default', 'scope'],
pseudo: ['hide']
});
nextState = reducer(nextState, removeAction);
expect(activeTopologyOptionsSelector(nextState).toJS()).toEqual({
namespace: ['default'],
pseudo: ['hide']
});
});
it('sets topology options from route', () => {
RouteAction.state = {
selectedNodeId: null,
topologyId: 'topo1',
topologyOptions: {topo1: {option1: 'on'}}
};
let nextState = initialState;
nextState = reducer(nextState, RouteAction);
expect(activeTopologyOptionsSelector(nextState).get('option1')).toBe('on');
expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('on');
// stay same after topos have been received
nextState = reducer(nextState, ReceiveTopologiesAction);
nextState = reducer(nextState, ClickTopologyAction);
expect(activeTopologyOptionsSelector(nextState).get('option1')).toBe('on');
expect(getUrlState(nextState).topologyOptions.topo1.option1).toBe('on');
});
it('uses default topology options from route', () => {
RouteAction.state = {
selectedNodeId: null,
topologyId: 'topo1',
topologyOptions: null
};
let nextState = initialState;
nextState = reducer(nextState, RouteAction);
nextState = reducer(nextState, ReceiveTopologiesAction);
nextState = reducer(nextState, ClickTopologyAction);
expect(activeTopologyOptionsSelector(nextState).get('option1')).toEqual(['off']);
expect(getUrlState(nextState).topologyOptions).toBeUndefined();
});
// nodes delta
it('replaces adjacency on update', () => {
let nextState = initialState;
nextState = reducer(nextState, ReceiveNodesDeltaAction);
expect(nextState.get('nodes').toJS().n1.adjacency).toEqual(['n1', 'n2']);
nextState = reducer(nextState, ReceiveNodesDeltaUpdateAction);
expect(nextState.get('nodes').toJS().n1.adjacency).toEqual(['n1']);
});
// browsing
it('shows nodes that were received', () => {
let nextState = initialState;
nextState = reducer(nextState, ReceiveNodesDeltaAction);
expect(nextState.get('nodes').toJS()).toEqual(NODE_SET);
});
it('knows a route was set', () => {
let nextState = initialState;
expect(nextState.get('routeSet')).toBeFalsy();
nextState = reducer(nextState, RouteAction);
expect(nextState.get('routeSet')).toBeTruthy();
});
it('gets selected node after click', () => {
let nextState = initialState;
nextState = reducer(nextState, ReceiveNodesDeltaAction);
nextState = reducer(nextState, ClickNodeAction);
expect(nextState.get('selectedNodeId')).toBe('n1');
expect(nextState.get('nodes').toJS()).toEqual(NODE_SET);
nextState = reducer(nextState, deSelectNode);
expect(nextState.get('selectedNodeId')).toBe(null);
expect(nextState.get('nodes').toJS()).toEqual(NODE_SET);
});
it('keeps showing nodes on navigating back after node click', () => {
let nextState = initialState;
nextState = reducer(nextState, ReceiveTopologiesAction);
nextState = reducer(nextState, ClickTopologyAction);
nextState = reducer(nextState, ReceiveNodesDeltaAction);
expect(getUrlState(nextState).selectedNodeId).toBeUndefined();
nextState = reducer(nextState, ClickNodeAction);
expect(getUrlState(nextState).selectedNodeId).toEqual('n1');
// go back in browsing
RouteAction.state = {selectedNodeId: null, topologyId: 'topo1'};
nextState = reducer(nextState, RouteAction);
expect(nextState.get('selectedNodeId')).toBeNull();
expect(nextState.get('nodes').toJS()).toEqual(NODE_SET);
});
it('closes details when changing topologies', () => {
let nextState = initialState;
nextState = reducer(nextState, ReceiveTopologiesAction);
nextState = reducer(nextState, ClickTopologyAction);
nextState = reducer(nextState, ReceiveNodesDeltaAction);
expect(getUrlState(nextState).selectedNodeId).toBeUndefined();
expect(getUrlState(nextState).topologyId).toEqual('topo1');
nextState = reducer(nextState, ClickNodeAction);
expect(getUrlState(nextState).selectedNodeId).toEqual('n1');
expect(getUrlState(nextState).topologyId).toEqual('topo1');
nextState = reducer(nextState, ClickSubTopologyAction);
expect(getUrlState(nextState).selectedNodeId).toBeUndefined();
expect(getUrlState(nextState).topologyId).toEqual('topo1-grouped');
});
// connection errors
it('resets topology on websocket reconnect', () => {
let nextState = initialState;
nextState = reducer(nextState, ReceiveNodesDeltaAction);
expect(nextState.get('nodes').toJS()).toEqual(NODE_SET);
nextState = reducer(nextState, CloseWebsocketAction);
expect(nextState.get('websocketClosed')).toBeTruthy();
// keep showing old nodes
expect(nextState.get('nodes').toJS()).toEqual(NODE_SET);
nextState = reducer(nextState, OpenWebsocketAction);
expect(nextState.get('websocketClosed')).toBeFalsy();
});
// adjacency test
it('returns the correct adjacency set for a node', () => {
let nextState = initialState;
nextState = reducer(nextState, ReceiveNodesDeltaAction);
expect(getAdjacentNodes(nextState).size).toEqual(0);
nextState = reducer(nextState, ClickNodeAction);
expect(getAdjacentNodes(nextState, 'n1').size).toEqual(2);
expect(getAdjacentNodes(nextState, 'n1').has('n1')).toBeTruthy();
expect(getAdjacentNodes(nextState, 'n1').has('n2')).toBeTruthy();
nextState = reducer(nextState, deSelectNode);
expect(getAdjacentNodes(nextState).size).toEqual(0);
});
// empty topology
it('detects that the nodes display is empty', () => {
let nextState = initialState;
nextState = reducer(nextState, ReceiveTopologiesAction);
nextState = reducer(nextState, ClickTopologyAction);
expect(isNodesDisplayEmpty(nextState)).toBeTruthy();
nextState = reducer(nextState, ReceiveNodesDeltaAction);
expect(isNodesDisplayEmpty(nextState)).toBeFalsy();
nextState = reducer(nextState, ClickTopology2Action);
expect(isNodesDisplayEmpty(nextState)).toBeTruthy();
nextState = reducer(nextState, ReceiveNodesDeltaAction);
expect(isNodesDisplayEmpty(nextState)).toBeFalsy();
});
it('detects that the topo stats are empty', () => {
let nextState = initialState;
nextState = reducer(nextState, ReceiveTopologiesAction);
nextState = reducer(nextState, ClickTopologyAction);
expect(isTopologyNodeCountZero(nextState)).toBeFalsy();
nextState = reducer(nextState, ReceiveNodesDeltaAction);
expect(isTopologyNodeCountZero(nextState)).toBeFalsy();
nextState = reducer(nextState, ClickTopology2Action);
expect(isTopologyNodeCountZero(nextState)).toBeTruthy();
nextState = reducer(nextState, ClickTopologyAction);
expect(isTopologyNodeCountZero(nextState)).toBeFalsy();
});
it('keeps hidden topology visible if selected', () => {
let nextState = initialState;
nextState = reducer(nextState, ReceiveTopologiesAction);
nextState = reducer(nextState, ClickTopology2Action);
nextState = reducer(nextState, ReceiveTopologiesHiddenAction);
expect(nextState.get('currentTopologyId')).toEqual('topo2');
expect(nextState.get('topologies').toJS().length).toEqual(2);
});
it('keeps hidden topology visible if sub_topology selected', () => {
let nextState = initialState;
nextState = reducer(nextState, ReceiveTopologiesAction);
nextState = reducer(nextState, { topologyId: 'topo2-sub', type: ActionTypes.CLICK_TOPOLOGY });
nextState = reducer(nextState, ReceiveTopologiesHiddenAction);
expect(nextState.get('currentTopologyId')).toEqual('topo2-sub');
expect(nextState.get('topologies').toJS().length).toEqual(2);
});
it('hides hidden topology if not selected', () => {
let nextState = initialState;
nextState = reducer(nextState, ClickTopologyAction);
nextState = reducer(nextState, ReceiveTopologiesHiddenAction);
expect(nextState.get('topologies').toJS().length).toEqual(1);
});
// selection of relatives
it('keeps relatives as a stack', () => {
let nextState = initialState;
nextState = reducer(nextState, ClickNodeAction);
expect(nextState.get('selectedNodeId')).toBe('n1');
expect(nextState.get('nodeDetails').size).toEqual(1);
expect(nextState.get('nodeDetails').has('n1')).toBeTruthy();
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('n1');
nextState = reducer(nextState, ClickRelativeAction);
// stack relative, first node stays main node
expect(nextState.get('selectedNodeId')).toBe('n1');
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('rel1');
expect(nextState.get('nodeDetails').size).toEqual(2);
expect(nextState.get('nodeDetails').has('rel1')).toBeTruthy();
// click on first node should clear the stack
nextState = reducer(nextState, ClickNodeAction);
expect(nextState.get('selectedNodeId')).toBe('n1');
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('n1');
expect(nextState.get('nodeDetails').size).toEqual(1);
expect(nextState.get('nodeDetails').has('rel1')).toBeFalsy();
});
it('keeps clears stack when sibling is clicked', () => {
let nextState = initialState;
nextState = reducer(nextState, ClickNodeAction);
expect(nextState.get('selectedNodeId')).toBe('n1');
expect(nextState.get('nodeDetails').size).toEqual(1);
expect(nextState.get('nodeDetails').has('n1')).toBeTruthy();
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('n1');
nextState = reducer(nextState, ClickRelativeAction);
// stack relative, first node stays main node
expect(nextState.get('selectedNodeId')).toBe('n1');
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('rel1');
expect(nextState.get('nodeDetails').size).toEqual(2);
expect(nextState.get('nodeDetails').has('rel1')).toBeTruthy();
// click on sibling node should clear the stack
nextState = reducer(nextState, ClickNode2Action);
expect(nextState.get('selectedNodeId')).toBe('n2');
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('n2');
expect(nextState.get('nodeDetails').size).toEqual(1);
expect(nextState.get('nodeDetails').has('n1')).toBeFalsy();
expect(nextState.get('nodeDetails').has('rel1')).toBeFalsy();
});
it('selectes relatives topology while keeping node selected', () => {
let nextState = initialState;
nextState = reducer(nextState, ClickTopologyAction);
nextState = reducer(nextState, ReceiveTopologiesAction);
expect(nextState.get('currentTopology').get('name')).toBe('Topo1');
nextState = reducer(nextState, ClickNodeAction);
expect(nextState.get('selectedNodeId')).toBe('n1');
expect(nextState.get('nodeDetails').size).toEqual(1);
expect(nextState.get('nodeDetails').has('n1')).toBeTruthy();
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('n1');
nextState = reducer(nextState, ClickRelativeAction);
// stack relative, first node stays main node
expect(nextState.get('selectedNodeId')).toBe('n1');
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('rel1');
expect(nextState.get('nodeDetails').size).toEqual(2);
expect(nextState.get('nodeDetails').has('rel1')).toBeTruthy();
// click switches over to relative's topology and selectes relative
nextState = reducer(nextState, ClickShowTopologyForNodeAction);
expect(nextState.get('selectedNodeId')).toBe('rel1');
expect(nextState.get('nodeDetails').keySeq().last()).toEqual('rel1');
expect(nextState.get('nodeDetails').size).toEqual(1);
expect(nextState.get('currentTopology').get('name')).toBe('Topo2');
});
it('closes the help dialog if the canvas is clicked', () => {
let nextState = initialState.set('showingHelp', true);
nextState = reducer(nextState, { type: ActionTypes.CLICK_BACKGROUND });
expect(nextState.get('showingHelp')).toBe(false);
});
it('switches to table view when complexity is high', () => {
let nextState = initialState.set('currentTopology', fromJS(topologies[0]));
nextState = reducer(nextState, {type: ActionTypes.SET_RECEIVED_NODES_DELTA});
expect(nextState.get('topologyViewMode')).toEqual(TABLE_VIEW_MODE);
expect(nextState.get('initialNodesLoaded')).toBe(true);
});
it('cleans up old adjacencies', () => {
// Add some nodes
const action1 = {
delta: { add: [{ id: 'n1' }, { id: 'n2' }] },
type: ActionTypes.RECEIVE_NODES_DELTA
};
// Show nodes as connected
const action2 = {
delta: {
update: [{ adjacency: ['n2'], id: 'n1' }]
},
type: ActionTypes.RECEIVE_NODES_DELTA
};
// Remove the connection
const action3 = {
delta: {
update: [{ id: 'n1' }]
},
type: ActionTypes.RECEIVE_NODES_DELTA
};
let nextState = reducer(initialState, action1);
nextState = reducer(nextState, action2);
nextState = reducer(nextState, action3);
expect(nextState.getIn(['nodes', 'n1', 'adjacency'])).toBeFalsy();
});
it('removes non-transferrable state values when changing instances', () => {
let nextState = initialState;
nextState = reducer(nextState, ClickNodeAction);
expect(nextState.get('selectedNodeId')).toEqual('n1');
expect(nextState.getIn(['nodeDetails', 'n1'])).toBeTruthy();
nextState = reducer(nextState, ChangeInstanceAction);
expect(nextState.get('selectedNodeId')).toBeFalsy();
expect(nextState.getIn(['nodeDetails', 'n1'])).toBeFalsy();
});
it('highlights bidirectional edges', () => {
const action = {
edgeId: constructEdgeId('abc123', 'def456'),
type: ActionTypes.ENTER_EDGE
};
const nextState = reducer(initialState, action);
expect(highlightedEdgeIdsSelector(nextState).toJS()).toEqual([
constructEdgeId('abc123', 'def456'),
constructEdgeId('def456', 'abc123')
]);
});
});