gitlab-org--gitlab-foss/spec/frontend/pipelines/components/dag/dag_graph_spec.js

214 lines
8 KiB
JavaScript

import { shallowMount } from '@vue/test-utils';
import { IS_HIGHLIGHTED, LINK_SELECTOR, NODE_SELECTOR } from '~/pipelines/components/dag/constants';
import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
import { createSankey } from '~/pipelines/components/dag/drawing_utils';
import { highlightIn, highlightOut } from '~/pipelines/components/dag/interactions';
import { removeOrphanNodes } from '~/pipelines/components/parsing_utils';
import { parsedData } from './mock_data';
describe('The DAG graph', () => {
let wrapper;
const getGraph = () => wrapper.find('.dag-graph-container > svg');
const getAllLinks = () => wrapper.findAll(`.${LINK_SELECTOR}`);
const getAllNodes = () => wrapper.findAll(`.${NODE_SELECTOR}`);
const getAllLabels = () => wrapper.findAll('foreignObject');
const createComponent = (propsData = {}) => {
if (wrapper?.destroy) {
wrapper.destroy();
}
wrapper = shallowMount(DagGraph, {
attachTo: document.body,
propsData,
data() {
return {
color: () => {},
width: 0,
height: 0,
};
},
});
};
beforeEach(() => {
createComponent({ graphData: parsedData });
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('in the basic case', () => {
beforeEach(() => {
/*
The graph uses random to offset links. To keep the snapshot consistent,
we mock Math.random. Wheeeee!
*/
const randomNumber = jest.spyOn(global.Math, 'random');
randomNumber.mockImplementation(() => 0.2);
createComponent({ graphData: parsedData });
});
it('renders the graph svg', () => {
expect(getGraph().exists()).toBe(true);
expect(getGraph().html()).toMatchSnapshot();
});
});
describe('links', () => {
it('renders the expected number of links', () => {
expect(getAllLinks()).toHaveLength(parsedData.links.length);
});
it('renders the expected number of gradients', () => {
expect(wrapper.findAll('linearGradient')).toHaveLength(parsedData.links.length);
});
it('renders the expected number of clip paths', () => {
expect(wrapper.findAll('clipPath')).toHaveLength(parsedData.links.length);
});
});
describe('nodes and labels', () => {
const sankeyNodes = createSankey()(parsedData).nodes;
const processedNodes = removeOrphanNodes(sankeyNodes);
describe('nodes', () => {
it('renders the expected number of nodes', () => {
expect(getAllNodes()).toHaveLength(processedNodes.length);
});
});
describe('labels', () => {
it('renders the expected number of labels as foreignObjects', () => {
expect(getAllLabels()).toHaveLength(processedNodes.length);
});
it('renders the title as text', () => {
expect(getAllLabels().at(0).text()).toBe(parsedData.nodes[0].name);
});
});
});
describe('interactions', () => {
const strokeOpacity = (opacity) => `stroke-opacity: ${opacity};`;
const baseOpacity = () => wrapper.vm.$options.viewOptions.baseOpacity;
describe('links', () => {
const liveLink = () => getAllLinks().at(4);
const otherLink = () => getAllLinks().at(1);
describe('on hover', () => {
it('sets the link opacity to baseOpacity and background links to 0.2', () => {
liveLink().trigger('mouseover');
expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
});
it('reverts the styles on mouseout', () => {
liveLink().trigger('mouseover');
liveLink().trigger('mouseout');
expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
});
});
describe('on click', () => {
describe('toggles link liveness', () => {
it('turns link on', () => {
liveLink().trigger('click');
expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
});
it('turns link off on second click', () => {
liveLink().trigger('click');
liveLink().trigger('click');
expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
});
});
it('the link remains live even after mouseout', () => {
liveLink().trigger('click');
liveLink().trigger('mouseout');
expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
});
it('preserves state when multiple links are toggled on and off', () => {
const anotherLiveLink = () => getAllLinks().at(2);
liveLink().trigger('click');
anotherLiveLink().trigger('click');
expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
anotherLiveLink().trigger('click');
expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(highlightOut));
expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
liveLink().trigger('click');
expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
});
});
});
describe('nodes', () => {
const liveNode = () => getAllNodes().at(10);
const anotherLiveNode = () => getAllNodes().at(5);
const nodesNotHighlighted = () => getAllNodes().filter((n) => !n.classes(IS_HIGHLIGHTED));
const linksNotHighlighted = () => getAllLinks().filter((n) => !n.classes(IS_HIGHLIGHTED));
const nodesHighlighted = () => getAllNodes().filter((n) => n.classes(IS_HIGHLIGHTED));
const linksHighlighted = () => getAllLinks().filter((n) => n.classes(IS_HIGHLIGHTED));
describe('on click', () => {
it('highlights the clicked node and predecessors', () => {
liveNode().trigger('click');
expect(nodesNotHighlighted().length < getAllNodes().length).toBe(true);
expect(linksNotHighlighted().length < getAllLinks().length).toBe(true);
linksHighlighted().wrappers.forEach((link) => {
expect(link.attributes('style')).toBe(strokeOpacity(highlightIn));
});
nodesHighlighted().wrappers.forEach((node) => {
expect(node.attributes('stroke')).not.toBe('#f2f2f2');
});
linksNotHighlighted().wrappers.forEach((link) => {
expect(link.attributes('style')).toBe(strokeOpacity(highlightOut));
});
nodesNotHighlighted().wrappers.forEach((node) => {
expect(node.attributes('stroke')).toBe('#f2f2f2');
});
});
it('toggles path off on second click', () => {
liveNode().trigger('click');
liveNode().trigger('click');
expect(nodesNotHighlighted().length).toBe(getAllNodes().length);
expect(linksNotHighlighted().length).toBe(getAllLinks().length);
});
it('preserves state when multiple nodes are toggled on and off', () => {
anotherLiveNode().trigger('click');
liveNode().trigger('click');
anotherLiveNode().trigger('click');
expect(nodesNotHighlighted().length < getAllNodes().length).toBe(true);
expect(linksNotHighlighted().length < getAllLinks().length).toBe(true);
});
});
});
});
});