From fd146dde30f1f05ff5fc559c3c68635ec00369a6 Mon Sep 17 00:00:00 2001
From: mabashian
Date: Wed, 22 Jan 2020 16:46:34 -0500
Subject: [PATCH] Adds unit test coverage to some of the workflow output
components. Also adds support for hovering on workflow results links to see
the edge type (success/fail/always).
---
.../WorkflowOutput/WorkflowOutputGraph.jsx | 63 +++--
.../Job/WorkflowOutput/WorkflowOutputLink.jsx | 45 +++-
.../WorkflowOutputLink.test.jsx | 42 ++++
.../Job/WorkflowOutput/WorkflowOutputNode.jsx | 4 +-
.../WorkflowOutputNode.test.jsx | 102 ++++++++
.../WorkflowOutputStartNode.test.jsx | 21 ++
.../WorkflowOutputToolbar.test.jsx | 51 ++++
.../VisualizerGraph.jsx | 54 +++--
.../VisualizerLink.jsx | 8 +-
awx/ui_next/src/util/workflow.jsx | 52 ++--
awx/ui_next/src/util/workflow.test.jsx | 225 ++++++++++++++++++
11 files changed, 582 insertions(+), 85 deletions(-)
create mode 100644 awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputLink.test.jsx
create mode 100644 awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.test.jsx
create mode 100644 awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputStartNode.test.jsx
create mode 100644 awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.test.jsx
create mode 100644 awx/ui_next/src/util/workflow.test.jsx
diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputGraph.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputGraph.jsx
index 04ad3219da..e053d7d346 100644
--- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputGraph.jsx
+++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputGraph.jsx
@@ -1,7 +1,10 @@
import React, { Fragment, useEffect, useRef, useState } from 'react';
import * as d3 from 'd3';
import { arrayOf, bool, shape, func } from 'prop-types';
-import { calcZoomAndFit, getZoomTranslate } from '@util/workflow';
+import {
+ getScaleAndOffsetToFit,
+ getTranslatePointsForZoom,
+} from '@util/workflow';
import {
WorkflowOutputLink,
WorkflowOutputNode,
@@ -10,6 +13,7 @@ import {
import {
WorkflowHelp,
WorkflowKey,
+ WorkflowLinkHelp,
WorkflowNodeHelp,
WorkflowTools,
} from '@components/Workflow';
@@ -23,6 +27,7 @@ function WorkflowOutputGraph({
showKey,
showTools,
}) {
+ const [linkHelp, setLinkHelp] = useState();
const [nodeHelp, setNodeHelp] = useState();
const [zoomPercentage, setZoomPercentage] = useState(100);
const svgRef = useRef(null);
@@ -83,7 +88,17 @@ function WorkflowOutputGraph({
};
const handleZoomChange = newScale => {
- const [translateX, translateY] = getZoomTranslate(svgRef.current, newScale);
+ const svgElement = document.getElementById('workflow-svg');
+ const svgBoundingClientRect = svgElement.getBoundingClientRect();
+ const currentScaleAndOffset = d3.zoomTransform(
+ d3.select(svgRef.current).node()
+ );
+
+ const [translateX, translateY] = getTranslatePointsForZoom(
+ svgBoundingClientRect,
+ currentScaleAndOffset,
+ newScale
+ );
d3.select(svgRef.current).call(
zoomRef.transform,
@@ -93,9 +108,27 @@ function WorkflowOutputGraph({
};
const handleFitGraph = () => {
- const [scaleToFit, yTranslate] = calcZoomAndFit(
- gRef.current,
- svgRef.current
+ const { k: currentScale } = d3.zoomTransform(
+ d3.select(svgRef.current).node()
+ );
+ const gBoundingClientRect = d3
+ .select(gRef.current)
+ .node()
+ .getBoundingClientRect();
+
+ const gBBoxDimensions = d3
+ .select(gRef.current)
+ .node()
+ .getBBox();
+
+ const svgElement = document.getElementById('workflow-svg');
+ const svgBoundingClientRect = svgElement.getBoundingClientRect();
+
+ const [scaleToFit, yTranslate] = getScaleAndOffsetToFit(
+ gBoundingClientRect,
+ svgBoundingClientRect,
+ gBBoxDimensions,
+ currentScale
);
d3.select(svgRef.current).call(
@@ -118,19 +151,9 @@ function WorkflowOutputGraph({
// Attempt to zoom the graph to fit the available screen space
useEffect(() => {
- const [scaleToFit, yTranslate] = calcZoomAndFit(
- gRef.current,
- svgRef.current
- );
-
- d3.select(svgRef.current).call(
- zoomRef.transform,
- d3.zoomIdentity.translate(0, yTranslate).scale(scaleToFit)
- );
-
- setZoomPercentage(scaleToFit * 100);
+ handleFitGraph();
// We only want this to run once (when the component mounts)
- // Including zoomRef.transform in the deps array will cause this to
+ // Including handleFitGraph in the deps array will cause this to
// run very frequently.
// Discussion: https://github.com/facebook/create-react-app/issues/6880
// and https://github.com/facebook/react/issues/15865 amongst others
@@ -139,9 +162,10 @@ function WorkflowOutputGraph({
return (
- {nodeHelp && (
+ {(nodeHelp || linkHelp) && (
-
+ {nodeHelp && }
+ {linkHelp && }
)}
)),
nodes.map(node => {
diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputLink.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputLink.jsx
index fdfc91d6cd..1c29899404 100644
--- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputLink.jsx
+++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputLink.jsx
@@ -1,11 +1,32 @@
import React, { useEffect, useState } from 'react';
import { shape } from 'prop-types';
-import { generateLine, getLinePoints } from '@util/workflow';
+import {
+ generateLine,
+ getLinePoints,
+ getLinkOverlayPoints,
+} from '@util/workflow';
-function WorkflowOutputLink({ link, nodePositions }) {
+function WorkflowOutputLink({ link, nodePositions, onUpdateLinkHelp }) {
+ const [hovering, setHovering] = useState(false);
const [pathD, setPathD] = useState();
const [pathStroke, setPathStroke] = useState('#CCCCCC');
+ const handleLinkMouseEnter = () => {
+ const linkEl = document.getElementById(
+ `link-${link.source.id}-${link.target.id}`
+ );
+ linkEl.parentNode.appendChild(linkEl);
+ setHovering(true);
+ };
+
+ const handleLinkMouseLeave = () => {
+ const linkEl = document.getElementById(
+ `link-${link.source.id}-${link.target.id}`
+ );
+ linkEl.parentNode.prepend(linkEl);
+ setHovering(null);
+ };
+
useEffect(() => {
if (link.linkType === 'failure') {
setPathStroke('#d9534f');
@@ -25,14 +46,22 @@ function WorkflowOutputLink({ link, nodePositions }) {
return (
-
+
+ onUpdateLinkHelp(link)}
+ onMouseLeave={() => onUpdateLinkHelp(null)}
+ opacity="0"
+ points={getLinkOverlayPoints(link, nodePositions)}
/>
);
diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputLink.test.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputLink.test.jsx
new file mode 100644
index 0000000000..651efc1060
--- /dev/null
+++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputLink.test.jsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import WorkflowOutputLink from './WorkflowOutputLink';
+
+const link = {
+ source: {
+ id: 1,
+ },
+ target: {
+ id: 2,
+ },
+};
+
+const nodePositions = {
+ 1: {
+ width: 72,
+ height: 40,
+ x: 0,
+ y: 0,
+ },
+ 2: {
+ width: 180,
+ height: 60,
+ x: 282,
+ y: 40,
+ },
+};
+
+describe('WorkflowOutputLink', () => {
+ test('mounts successfully', () => {
+ const wrapper = mountWithContexts(
+
+ );
+ expect(wrapper).toHaveLength(1);
+ });
+});
diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx
index 69ebd533aa..8b111bc80c 100644
--- a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx
+++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.jsx
@@ -114,9 +114,7 @@ function WorkflowOutputNode({
: i18n._(t`DELETED`)}
-
- {secondsToHHMMSS(node.job.elapsed)}
-
+ {secondsToHHMMSS(node.job.elapsed)}
) : (
diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.test.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.test.jsx
new file mode 100644
index 0000000000..046ee99c73
--- /dev/null
+++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputNode.test.jsx
@@ -0,0 +1,102 @@
+import React from 'react';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import WorkflowOutputNode from './WorkflowOutputNode';
+
+const nodeWithJT = {
+ id: 2,
+ job: {
+ elapsed: 7,
+ id: 9000,
+ name: 'Automation JT',
+ status: 'successful',
+ type: 'job',
+ },
+ unifiedJobTemplate: {
+ id: 77,
+ name: 'Automation JT',
+ unified_job_type: 'job',
+ },
+};
+
+const nodeWithoutJT = {
+ id: 2,
+ job: {
+ elapsed: 7,
+ id: 9000,
+ name: 'Automation JT',
+ status: 'successful',
+ type: 'job',
+ },
+};
+
+const nodePositions = {
+ 1: {
+ width: 72,
+ height: 40,
+ x: 0,
+ y: 0,
+ },
+ 2: {
+ width: 180,
+ height: 60,
+ x: 282,
+ y: 40,
+ },
+};
+
+describe('WorkflowOutputNode', () => {
+ test('mounts successfully', () => {
+ const wrapper = mountWithContexts(
+
+ );
+ expect(wrapper).toHaveLength(1);
+ });
+ test('node contents displayed correctly when Job and Job Template exist', () => {
+ const wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.contains(Automation JT
)).toEqual(true);
+ expect(wrapper.find('WorkflowOutputNode__Elapsed').text()).toBe('00:00:07');
+ });
+ test('node contents displayed correctly when Job Template deleted', () => {
+ const wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.contains(DELETED
)).toEqual(true);
+ expect(wrapper.find('WorkflowOutputNode__Elapsed').text()).toBe('00:00:07');
+ });
+ test('node contents displayed correctly when Job deleted', () => {
+ const wrapper = mountWithContexts(
+
+ );
+ expect(wrapper.text()).toBe('DELETED');
+ });
+});
diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputStartNode.test.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputStartNode.test.jsx
new file mode 100644
index 0000000000..456c8aa19c
--- /dev/null
+++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputStartNode.test.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import WorkflowOutputStartNode from './WorkflowOutputStartNode';
+
+const nodePositions = {
+ 1: {
+ x: 0,
+ y: 0,
+ },
+};
+
+describe('WorkflowOutputStartNode', () => {
+ test('mounts successfully', () => {
+ const wrapper = mount(
+
+ );
+ expect(wrapper).toHaveLength(1);
+ });
+});
diff --git a/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.test.jsx b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.test.jsx
new file mode 100644
index 0000000000..02c2fecba8
--- /dev/null
+++ b/awx/ui_next/src/screens/Job/WorkflowOutput/WorkflowOutputToolbar.test.jsx
@@ -0,0 +1,51 @@
+import React from 'react';
+import { mountWithContexts } from '@testUtils/enzymeHelpers';
+import WorkflowOutputToolbar from './WorkflowOutputToolbar';
+
+const job = {
+ id: 1,
+ status: 'successful',
+};
+
+describe('WorkflowOutputToolbar', () => {
+ test('mounts successfully', () => {
+ const wrapper = mountWithContexts(
+ {}}
+ onToolsToggle={() => {}}
+ toolsShown={false}
+ />
+ );
+ expect(wrapper).toHaveLength(1);
+ });
+
+ test('shows correct number of nodes', () => {
+ const nodes = [
+ {
+ id: 1,
+ },
+ {
+ id: 2,
+ },
+ {
+ id: 3,
+ isDeleted: true,
+ },
+ ];
+ const wrapper = mountWithContexts(
+ {}}
+ onToolsToggle={() => {}}
+ toolsShown={false}
+ />
+ );
+ // The start node (id=1) and deleted nodes (isDeleted=true) should be ignored
+ expect(wrapper.find('Badge').text()).toBe('1');
+ });
+});
diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerGraph.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerGraph.jsx
index 58081edad1..a37b022e14 100644
--- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerGraph.jsx
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerGraph.jsx
@@ -5,9 +5,9 @@ import styled from 'styled-components';
import { arrayOf, bool, func, shape } from 'prop-types';
import * as d3 from 'd3';
import {
- calcZoomAndFit,
+ getScaleAndOffsetToFit,
constants as wfConstants,
- getZoomTranslate,
+ getTranslatePointsForZoom,
} from '@util/workflow';
import {
WorkflowHelp,
@@ -161,7 +161,17 @@ function VisualizerGraph({
};
const handleZoomChange = newScale => {
- const [translateX, translateY] = getZoomTranslate(svgRef.current, newScale);
+ const svgElement = document.getElementById('workflow-svg');
+ const svgBoundingClientRect = svgElement.getBoundingClientRect();
+ const currentScaleAndOffset = d3.zoomTransform(
+ d3.select(svgRef.current).node()
+ );
+
+ const [translateX, translateY] = getTranslatePointsForZoom(
+ svgBoundingClientRect,
+ currentScaleAndOffset,
+ newScale
+ );
d3.select(svgRef.current).call(
zoomRef.transform,
@@ -171,9 +181,27 @@ function VisualizerGraph({
};
const handleFitGraph = () => {
- const [scaleToFit, yTranslate] = calcZoomAndFit(
- gRef.current,
- svgRef.current
+ const { k: currentScale } = d3.zoomTransform(
+ d3.select(svgRef.current).node()
+ );
+ const gBoundingClientRect = d3
+ .select(gRef.current)
+ .node()
+ .getBoundingClientRect();
+
+ const gBBoxDimensions = d3
+ .select(gRef.current)
+ .node()
+ .getBBox();
+
+ const svgElement = document.getElementById('workflow-svg');
+ const svgBoundingClientRect = svgElement.getBoundingClientRect();
+
+ const [scaleToFit, yTranslate] = getScaleAndOffsetToFit(
+ gBoundingClientRect,
+ svgBoundingClientRect,
+ gBBoxDimensions,
+ currentScale
);
d3.select(svgRef.current).call(
@@ -196,19 +224,9 @@ function VisualizerGraph({
// Attempt to zoom the graph to fit the available screen space
useEffect(() => {
- const [scaleToFit, yTranslate] = calcZoomAndFit(
- gRef.current,
- svgRef.current
- );
-
- d3.select(svgRef.current).call(
- zoomRef.transform,
- d3.zoomIdentity.translate(0, yTranslate).scale(scaleToFit)
- );
-
- setZoomPercentage(scaleToFit * 100);
+ handleFitGraph();
// We only want this to run once (when the component mounts)
- // Including zoomRef.transform in the deps array will cause this to
+ // Including handleFitGraph in the deps array will cause this to
// run very frequently.
// Discussion: https://github.com/facebook/create-react-app/issues/6880
// and https://github.com/facebook/react/issues/15865 amongst others
diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerLink.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerLink.jsx
index c3e3ea7fa4..4a9fd852f1 100644
--- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerLink.jsx
+++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateVisualizer/VisualizerLink.jsx
@@ -116,7 +116,6 @@ function VisualizerLink({
return (
-
+
onUpdateLinkHelp(link)}
onMouseLeave={() => onUpdateLinkHelp(null)}
diff --git a/awx/ui_next/src/util/workflow.jsx b/awx/ui_next/src/util/workflow.jsx
index c37dca118f..6ed9bf903f 100644
--- a/awx/ui_next/src/util/workflow.jsx
+++ b/awx/ui_next/src/util/workflow.jsx
@@ -11,24 +11,15 @@ export const constants = {
rootH: 40,
};
-export function calcZoomAndFit(gRef, svgRef) {
- const { k: currentScale } = d3.zoomTransform(d3.select(svgRef).node());
- const gBoundingClientRect = d3
- .select(gRef)
- .node()
- .getBoundingClientRect();
-
+export function getScaleAndOffsetToFit(
+ gBoundingClientRect,
+ svgBoundingClientRect,
+ gBBoxDimensions,
+ currentScale
+) {
gBoundingClientRect.height /= currentScale;
gBoundingClientRect.width /= currentScale;
- const gBBoxDimensions = d3
- .select(gRef)
- .node()
- .getBBox();
-
- const svgElement = document.getElementById('workflow-svg');
- const svgBoundingClientRect = svgElement.getBoundingClientRect();
-
// For some reason the root width needs to be added?
gBoundingClientRect.width += constants.rootW;
@@ -96,19 +87,19 @@ export function getLinePoints(link, nodePositions) {
];
}
-export function getLinkOverlayPoints(d, nodePositions) {
+export function getLinkOverlayPoints(link, nodePositions) {
const sourceX =
- nodePositions[d.source.id].x + nodePositions[d.source.id].width + 1;
+ nodePositions[link.source.id].x + nodePositions[link.source.id].width + 1;
let sourceY =
- normalizeY(nodePositions, nodePositions[d.source.id].y) +
- nodePositions[d.source.id].height / 2;
- const targetX = nodePositions[d.target.id].x - 1;
+ normalizeY(nodePositions, nodePositions[link.source.id].y) +
+ nodePositions[link.source.id].height / 2;
+ const targetX = nodePositions[link.target.id].x - 1;
const targetY =
- normalizeY(nodePositions, nodePositions[d.target.id].y) +
- nodePositions[d.target.id].height / 2;
+ normalizeY(nodePositions, nodePositions[link.target.id].y) +
+ nodePositions[link.target.id].height / 2;
// There's something off with the math on the root node...
- if (d.source.id === 1) {
+ if (link.source.id === 1) {
sourceY += 10;
}
const slope = (targetY - sourceY) / (targetX - sourceX);
@@ -177,18 +168,19 @@ export function layoutGraph(nodes, links) {
return g;
}
-export function getZoomTranslate(svgRef, newScale) {
- const svgElement = document.getElementById('workflow-svg');
- const svgBoundingClientRect = svgElement.getBoundingClientRect();
- const current = d3.zoomTransform(d3.select(svgRef).node());
- const origScale = current.k;
+export function getTranslatePointsForZoom(
+ svgBoundingClientRect,
+ currentScaleAndOffset,
+ newScale
+) {
+ const origScale = currentScaleAndOffset.k;
const unscaledOffsetX =
- (current.x +
+ (currentScaleAndOffset.x +
(svgBoundingClientRect.width * origScale - svgBoundingClientRect.width) /
2) /
origScale;
const unscaledOffsetY =
- (current.y +
+ (currentScaleAndOffset.y +
(svgBoundingClientRect.height * origScale -
svgBoundingClientRect.height) /
2) /
diff --git a/awx/ui_next/src/util/workflow.test.jsx b/awx/ui_next/src/util/workflow.test.jsx
new file mode 100644
index 0000000000..0cc1ad05a3
--- /dev/null
+++ b/awx/ui_next/src/util/workflow.test.jsx
@@ -0,0 +1,225 @@
+import {
+ getScaleAndOffsetToFit,
+ generateLine,
+ getLinePoints,
+ getLinkOverlayPoints,
+ layoutGraph,
+ getTranslatePointsForZoom,
+} from './workflow';
+
+describe('getScaleAndOffsetToFit', () => {
+ const gBoundingClientRect = {
+ x: 36,
+ y: 11,
+ width: 798,
+ height: 160,
+ top: 11,
+ right: 834,
+ bottom: 171,
+ left: 36,
+ };
+ const svgBoundingClientRect = {
+ x: 0,
+ y: 56,
+ width: 1680,
+ height: 455,
+ top: 56,
+ right: 1680,
+ bottom: 511,
+ left: 0,
+ };
+ const gBBoxDimensions = {
+ x: 36,
+ y: -45,
+ width: 726,
+ height: 160,
+ };
+ const currentScale = 1;
+ test('returns correct scale and y-offset for zooming the graph to best fit the available space', () => {
+ expect(
+ getScaleAndOffsetToFit(
+ gBoundingClientRect,
+ svgBoundingClientRect,
+ gBBoxDimensions,
+ currentScale
+ )
+ ).toEqual([1.931, 159.91499999999996]);
+ });
+});
+
+describe('generateLine', () => {
+ test('returns correct svg path string', () => {
+ expect(
+ generateLine([
+ {
+ x: 0,
+ y: 0,
+ },
+ {
+ x: 10,
+ y: 10,
+ },
+ ])
+ ).toEqual('M0,0L10,10');
+ expect(
+ generateLine([
+ {
+ x: 900,
+ y: 44,
+ },
+ {
+ x: 5000,
+ y: 359,
+ },
+ ])
+ ).toEqual('M900,44L5000,359');
+ });
+});
+
+describe('getLinePoints', () => {
+ const link = {
+ source: {
+ id: 1,
+ },
+ target: {
+ id: 2,
+ },
+ };
+ const nodePositions = {
+ 1: {
+ width: 72,
+ height: 40,
+ x: 36,
+ y: 130,
+ },
+ 2: {
+ width: 180,
+ height: 60,
+ x: 282,
+ y: 40,
+ },
+ };
+ test('returns the correct endpoints of the line', () => {
+ expect(getLinePoints(link, nodePositions)).toEqual([
+ { x: 109, y: 30 },
+ { x: 281, y: -60 },
+ ]);
+ });
+});
+
+describe('getLinkOverlayPoints', () => {
+ const link = {
+ source: {
+ id: 1,
+ },
+ target: {
+ id: 2,
+ },
+ };
+ const nodePositions = {
+ 1: {
+ width: 72,
+ height: 40,
+ x: 36,
+ y: 130,
+ },
+ 2: {
+ width: 180,
+ height: 60,
+ x: 282,
+ y: 40,
+ },
+ };
+ test('returns the four points of the polygon that will act as the overlay for the link', () => {
+ expect(getLinkOverlayPoints(link, nodePositions)).toEqual(
+ '281,-50.970992003685446 109,39.02900799631457 109,20.97099200368546 281,-69.02900799631456'
+ );
+ });
+});
+
+describe('layoutGraph', () => {
+ const nodes = [
+ {
+ id: 1,
+ },
+ {
+ id: 2,
+ },
+ {
+ id: 3,
+ },
+ {
+ id: 4,
+ },
+ ];
+ const links = [
+ {
+ source: {
+ id: 1,
+ },
+ target: {
+ id: 2,
+ },
+ },
+ {
+ source: {
+ id: 1,
+ },
+ target: {
+ id: 4,
+ },
+ },
+ {
+ source: {
+ id: 2,
+ },
+ target: {
+ id: 3,
+ },
+ },
+ {
+ source: {
+ id: 4,
+ },
+ target: {
+ id: 3,
+ },
+ },
+ ];
+ test('returns the correct dimensions and positions for the nodes', () => {
+ expect(layoutGraph(nodes, links)._nodes).toEqual({
+ 1: { height: 40, label: '', width: 72, x: 36, y: 75 },
+ 2: { height: 60, label: '', width: 180, x: 282, y: 30 },
+ 3: { height: 60, label: '', width: 180, x: 582, y: 75 },
+ 4: { height: 60, label: '', width: 180, x: 282, y: 120 },
+ });
+ });
+});
+
+describe('getTranslatePointsForZoom', () => {
+ const svgBoundingClientRect = {
+ x: 0,
+ y: 56,
+ width: 1680,
+ height: 455,
+ top: 56,
+ right: 1680,
+ bottom: 511,
+ left: 0,
+ };
+ const currentScaleAndOffset = {
+ k: 2,
+ x: 0,
+ y: 167.5,
+ };
+ const newScale = 1.9;
+ test('returns the correct translation point', () => {
+ expect(
+ getTranslatePointsForZoom(
+ svgBoundingClientRect,
+ currentScaleAndOffset,
+ newScale
+ )
+ ).toEqual([42, 170.5]);
+ });
+});