mirror of
https://github.com/ansible/awx.git
synced 2026-02-28 08:18:43 -03:30
Adds basic unit test coverage to visualizer components (not including modals).
This commit is contained in:
@@ -1,13 +1,17 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { WorkflowStateContext } from '@contexts/Workflow';
|
import {
|
||||||
|
WorkflowDispatchContext,
|
||||||
|
WorkflowStateContext,
|
||||||
|
} from '@contexts/Workflow';
|
||||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
import WorkflowOutputToolbar from './WorkflowOutputToolbar';
|
import WorkflowOutputToolbar from './WorkflowOutputToolbar';
|
||||||
|
|
||||||
|
let wrapper;
|
||||||
|
const dispatch = jest.fn();
|
||||||
const job = {
|
const job = {
|
||||||
id: 1,
|
id: 1,
|
||||||
status: 'successful',
|
status: 'successful',
|
||||||
};
|
};
|
||||||
|
|
||||||
const workflowContext = {
|
const workflowContext = {
|
||||||
nodes: [],
|
nodes: [],
|
||||||
showLegend: false,
|
showLegend: false,
|
||||||
@@ -15,16 +19,7 @@ const workflowContext = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe('WorkflowOutputToolbar', () => {
|
describe('WorkflowOutputToolbar', () => {
|
||||||
test('mounts successfully', () => {
|
beforeAll(() => {
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<WorkflowStateContext.Provider value={workflowContext}>
|
|
||||||
<WorkflowOutputToolbar job={job} />
|
|
||||||
</WorkflowStateContext.Provider>
|
|
||||||
);
|
|
||||||
expect(wrapper).toHaveLength(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('shows correct number of nodes', () => {
|
|
||||||
const nodes = [
|
const nodes = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -37,12 +32,31 @@ describe('WorkflowOutputToolbar', () => {
|
|||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<WorkflowStateContext.Provider value={{ ...workflowContext, nodes }}>
|
<WorkflowDispatchContext.Provider value={dispatch}>
|
||||||
<WorkflowOutputToolbar job={job} />
|
<WorkflowStateContext.Provider value={{ ...workflowContext, nodes }}>
|
||||||
</WorkflowStateContext.Provider>
|
<WorkflowOutputToolbar job={job} />
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
|
</WorkflowDispatchContext.Provider>
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Shows correct number of nodes', () => {
|
||||||
// The start node (id=1) and deleted nodes (isDeleted=true) should be ignored
|
// The start node (id=1) and deleted nodes (isDeleted=true) should be ignored
|
||||||
expect(wrapper.find('Badge').text()).toBe('1');
|
expect(wrapper.find('Badge').text()).toBe('1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Toggle Legend button dispatches as expected', () => {
|
||||||
|
wrapper.find('CompassIcon').simulate('click');
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({ type: 'TOGGLE_LEGEND' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Toggle Tools button dispatches as expected', () => {
|
||||||
|
wrapper.find('WrenchIcon').simulate('click');
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({ type: 'TOGGLE_TOOLS' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ function DeleteAllNodesModal({ i18n }) {
|
|||||||
<AlertModal
|
<AlertModal
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Button
|
||||||
|
id="confirm-delete-all-nodes"
|
||||||
key="remove"
|
key="remove"
|
||||||
variant="danger"
|
variant="danger"
|
||||||
aria-label={i18n._(t`Confirm removal of all nodes`)}
|
aria-label={i18n._(t`Confirm removal of all nodes`)}
|
||||||
@@ -19,6 +20,7 @@ function DeleteAllNodesModal({ i18n }) {
|
|||||||
{i18n._(t`Remove`)}
|
{i18n._(t`Remove`)}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Button
|
||||||
|
id="cancel-delete-all-nodes"
|
||||||
key="cancel"
|
key="cancel"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
aria-label={i18n._(t`Cancel node removal`)}
|
aria-label={i18n._(t`Cancel node removal`)}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ function LinkModal({ header, i18n, onConfirm }) {
|
|||||||
onClose={() => dispatch({ type: 'CANCEL_LINK_MODAL' })}
|
onClose={() => dispatch({ type: 'CANCEL_LINK_MODAL' })}
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Button
|
||||||
|
id="link-confirm"
|
||||||
key="save"
|
key="save"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
aria-label={i18n._(t`Save link changes`)}
|
aria-label={i18n._(t`Save link changes`)}
|
||||||
@@ -32,6 +33,7 @@ function LinkModal({ header, i18n, onConfirm }) {
|
|||||||
{i18n._(t`Save`)}
|
{i18n._(t`Save`)}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button
|
<Button
|
||||||
|
id="link-cancel"
|
||||||
key="cancel"
|
key="cancel"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
aria-label={i18n._(t`Cancel link changes`)}
|
aria-label={i18n._(t`Cancel link changes`)}
|
||||||
@@ -44,6 +46,7 @@ function LinkModal({ header, i18n, onConfirm }) {
|
|||||||
<FormGroup fieldId="link-select" label={i18n._(t`Run`)}>
|
<FormGroup fieldId="link-select" label={i18n._(t`Run`)}>
|
||||||
<AnsibleSelect
|
<AnsibleSelect
|
||||||
id="link-select"
|
id="link-select"
|
||||||
|
name="linkType"
|
||||||
value={linkType}
|
value={linkType}
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
|
import { WorkflowJobTemplateNodesAPI, WorkflowJobTemplatesAPI } from '@api';
|
||||||
|
import Visualizer from './Visualizer';
|
||||||
|
|
||||||
|
jest.mock('@api');
|
||||||
|
|
||||||
|
const template = {
|
||||||
|
id: 1,
|
||||||
|
name: 'Foo WFJT',
|
||||||
|
summary_fields: {
|
||||||
|
user_capabilities: {
|
||||||
|
edit: true,
|
||||||
|
delete: true,
|
||||||
|
start: true,
|
||||||
|
schedule: true,
|
||||||
|
copy: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockWorkflowNodes = [
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
success_nodes: [10],
|
||||||
|
failure_nodes: [],
|
||||||
|
always_nodes: [9],
|
||||||
|
summary_fields: {
|
||||||
|
unified_job_template: {
|
||||||
|
id: 14,
|
||||||
|
name: 'A Playbook',
|
||||||
|
type: 'job_template',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
success_nodes: [],
|
||||||
|
failure_nodes: [],
|
||||||
|
always_nodes: [],
|
||||||
|
summary_fields: {
|
||||||
|
unified_job_template: {
|
||||||
|
id: 14,
|
||||||
|
name: 'A Project Update',
|
||||||
|
type: 'project',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10,
|
||||||
|
success_nodes: [],
|
||||||
|
failure_nodes: [],
|
||||||
|
always_nodes: [],
|
||||||
|
summary_fields: {
|
||||||
|
unified_job_template: {
|
||||||
|
elapsed: 10,
|
||||||
|
name: 'An Inventory Source Sync',
|
||||||
|
type: 'inventory_source',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11,
|
||||||
|
success_nodes: [9],
|
||||||
|
failure_nodes: [],
|
||||||
|
always_nodes: [],
|
||||||
|
summary_fields: {
|
||||||
|
unified_job_template: {
|
||||||
|
id: 14,
|
||||||
|
name: 'Pause',
|
||||||
|
type: 'workflow_approval_template',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('Visualizer', () => {
|
||||||
|
let wrapper;
|
||||||
|
beforeAll(() => {
|
||||||
|
WorkflowJobTemplatesAPI.readNodes.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
count: mockWorkflowNodes.length,
|
||||||
|
results: mockWorkflowNodes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
window.SVGElement.prototype.height = {
|
||||||
|
baseVal: {
|
||||||
|
value: 100,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
window.SVGElement.prototype.width = {
|
||||||
|
baseVal: {
|
||||||
|
value: 100,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
window.SVGElement.prototype.getBBox = () => ({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 500,
|
||||||
|
height: 250,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.SVGElement.prototype.getBoundingClientRect = () => ({
|
||||||
|
x: 303,
|
||||||
|
y: 252.359375,
|
||||||
|
width: 1329,
|
||||||
|
height: 259.640625,
|
||||||
|
top: 252.359375,
|
||||||
|
right: 1632,
|
||||||
|
bottom: 512,
|
||||||
|
left: 303,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
wrapper.unmount();
|
||||||
|
delete window.SVGElement.prototype.getBBox;
|
||||||
|
delete window.SVGElement.prototype.getBoundingClientRect;
|
||||||
|
delete window.SVGElement.prototype.height;
|
||||||
|
delete window.SVGElement.prototype.width;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renders successfully', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<svg>
|
||||||
|
<Visualizer template={template} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('ContentError')).toHaveLength(0);
|
||||||
|
expect(wrapper.find('WorkflowStartNode')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('VisualizerNode')).toHaveLength(4);
|
||||||
|
expect(wrapper.find('VisualizerLink')).toHaveLength(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Successfully deletes all nodes', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<svg>
|
||||||
|
<Visualizer template={template} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('DeleteAllNodesModal').length).toBe(0);
|
||||||
|
wrapper.find('TrashAltIcon').simulate('click');
|
||||||
|
expect(wrapper.find('DeleteAllNodesModal').length).toBe(1);
|
||||||
|
wrapper.find('button#confirm-delete-all-nodes').simulate('click');
|
||||||
|
expect(wrapper.find('VisualizerStartScreen')).toHaveLength(1);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||||
|
});
|
||||||
|
expect(WorkflowJobTemplateNodesAPI.destroy).toHaveBeenCalledWith(8);
|
||||||
|
expect(WorkflowJobTemplateNodesAPI.destroy).toHaveBeenCalledWith(9);
|
||||||
|
expect(WorkflowJobTemplateNodesAPI.destroy).toHaveBeenCalledWith(10);
|
||||||
|
expect(WorkflowJobTemplateNodesAPI.destroy).toHaveBeenCalledWith(11);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Successfully changes link type', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<svg>
|
||||||
|
<Visualizer template={template} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('LinkEditModal').length).toBe(0);
|
||||||
|
wrapper.find('g#link-2-3').simulate('mouseenter');
|
||||||
|
wrapper.find('#link-edit').simulate('click');
|
||||||
|
expect(wrapper.find('LinkEditModal').length).toBe(1);
|
||||||
|
act(() => {
|
||||||
|
wrapper
|
||||||
|
.find('LinkEditModal')
|
||||||
|
.find('AnsibleSelect')
|
||||||
|
.prop('onChange')(null, 'success');
|
||||||
|
});
|
||||||
|
wrapper.find('button#link-confirm').simulate('click');
|
||||||
|
expect(wrapper.find('LinkEditModal').length).toBe(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
WorkflowJobTemplateNodesAPI.disassociateAlwaysNode
|
||||||
|
).toHaveBeenCalledWith(8, 9);
|
||||||
|
expect(
|
||||||
|
WorkflowJobTemplateNodesAPI.associateSuccessNode
|
||||||
|
).toHaveBeenCalledWith(8, 9);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Error shown to user when error thrown fetching workflow nodes', async () => {
|
||||||
|
WorkflowJobTemplatesAPI.readNodes.mockRejectedValue(new Error());
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<svg>
|
||||||
|
<Visualizer template={template} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('ContentError')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -283,8 +283,8 @@ function VisualizerGraph({ i18n, readOnly }) {
|
|||||||
key={`link-${link.source.id}-${link.target.id}`}
|
key={`link-${link.source.id}-${link.target.id}`}
|
||||||
link={link}
|
link={link}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
onUpdateHelpText={setHelpText}
|
updateLinkHelp={newLinkHelp => setLinkHelp(newLinkHelp)}
|
||||||
onUpdateLinkHelp={setLinkHelp}
|
updateHelpText={newHelpText => setHelpText(newHelpText)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -297,8 +297,8 @@ function VisualizerGraph({ i18n, readOnly }) {
|
|||||||
key={`node-${node.id}`}
|
key={`node-${node.id}`}
|
||||||
node={node}
|
node={node}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
onUpdateHelpText={setHelpText}
|
updateHelpText={newHelpText => setHelpText(newHelpText)}
|
||||||
updateNodeHelp={setNodeHelp}
|
updateNodeHelp={newNodeHelp => setNodeHelp(newNodeHelp)}
|
||||||
{...(addingLink && {
|
{...(addingLink && {
|
||||||
onMouseOver: () => drawPotentialLinkToNode(node),
|
onMouseOver: () => drawPotentialLinkToNode(node),
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
|
import { WorkflowStateContext } from '@contexts/Workflow';
|
||||||
|
import VisualizerGraph from './VisualizerGraph';
|
||||||
|
|
||||||
|
const workflowContext = {
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
source: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
linkType: 'always',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
id: 5,
|
||||||
|
},
|
||||||
|
linkType: 'always',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: {
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
id: 4,
|
||||||
|
},
|
||||||
|
linkType: 'success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: {
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
id: 3,
|
||||||
|
},
|
||||||
|
linkType: 'always',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: {
|
||||||
|
id: 5,
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
id: 3,
|
||||||
|
},
|
||||||
|
linkType: 'success',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
nodePositions: {
|
||||||
|
1: { label: '', width: 72, height: 40, x: 36, y: 85 },
|
||||||
|
2: { label: '', width: 180, height: 60, x: 282, y: 40 },
|
||||||
|
3: { label: '', width: 180, height: 60, x: 582, y: 130 },
|
||||||
|
4: { label: '', width: 180, height: 60, x: 582, y: 30 },
|
||||||
|
5: { label: '', width: 180, height: 60, x: 282, y: 140 },
|
||||||
|
},
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
unifiedJobTemplate: {
|
||||||
|
name: 'Foo JT',
|
||||||
|
type: 'job_template',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showLegend: false,
|
||||||
|
showTools: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('VisualizerGraph', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
window.SVGElement.prototype.height = {
|
||||||
|
baseVal: {
|
||||||
|
value: 100,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
window.SVGElement.prototype.width = {
|
||||||
|
baseVal: {
|
||||||
|
value: 100,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
window.SVGElement.prototype.getBBox = () => ({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 500,
|
||||||
|
height: 250,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.SVGElement.prototype.getBoundingClientRect = () => ({
|
||||||
|
x: 303,
|
||||||
|
y: 252.359375,
|
||||||
|
width: 1329,
|
||||||
|
height: 259.640625,
|
||||||
|
top: 252.359375,
|
||||||
|
right: 1632,
|
||||||
|
bottom: 512,
|
||||||
|
left: 303,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
delete window.SVGElement.prototype.getBBox;
|
||||||
|
delete window.SVGElement.prototype.getBoundingClientRect;
|
||||||
|
delete window.SVGElement.prototype.height;
|
||||||
|
delete window.SVGElement.prototype.width;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mounts successfully', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<svg>
|
||||||
|
<WorkflowStateContext.Provider value={workflowContext}>
|
||||||
|
<VisualizerGraph readOnly={false} />
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
expect(wrapper).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tools and legend are shown when flags are true', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<svg>
|
||||||
|
<WorkflowStateContext.Provider
|
||||||
|
value={{ ...workflowContext, showLegend: true, showTools: true }}
|
||||||
|
>
|
||||||
|
<VisualizerGraph readOnly={false} />
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.find('WorkflowLegend')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('WorkflowTools')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('nodes and links are properly rendered', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<svg>
|
||||||
|
<WorkflowStateContext.Provider value={workflowContext}>
|
||||||
|
<VisualizerGraph readOnly={false} />
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.find('WorkflowStartNode')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('VisualizerNode')).toHaveLength(4);
|
||||||
|
expect(wrapper.find('VisualizerLink')).toHaveLength(5);
|
||||||
|
expect(wrapper.find('g#link-2-4')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('g#link-2-3')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('g#link-5-3')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('g#link-1-2')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('g#link-1-5')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('proper help text is shown when hovering over nodes', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<svg>
|
||||||
|
<WorkflowStateContext.Provider value={workflowContext}>
|
||||||
|
<VisualizerGraph readOnly={false} />
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.find('WorkflowNodeHelp')).toHaveLength(0);
|
||||||
|
expect(wrapper.find('WorkflowLinkHelp')).toHaveLength(0);
|
||||||
|
wrapper
|
||||||
|
.find('g#node-2')
|
||||||
|
.find('foreignObject')
|
||||||
|
.first()
|
||||||
|
.simulate('mouseenter');
|
||||||
|
expect(wrapper.find('WorkflowNodeHelp')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('WorkflowNodeHelp').contains(<b>Name</b>)).toEqual(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
wrapper.find('WorkflowNodeHelp').containsMatchingElement(<dd>Foo JT</dd>)
|
||||||
|
).toEqual(true);
|
||||||
|
expect(wrapper.find('WorkflowNodeHelp').contains(<b>Type</b>)).toEqual(
|
||||||
|
true
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.find('WorkflowNodeHelp')
|
||||||
|
.containsMatchingElement(<dd>Job Template</dd>)
|
||||||
|
).toEqual(true);
|
||||||
|
wrapper
|
||||||
|
.find('g#node-2')
|
||||||
|
.find('foreignObject')
|
||||||
|
.first()
|
||||||
|
.simulate('mouseleave');
|
||||||
|
expect(wrapper.find('WorkflowNodeHelp')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('proper help text is shown when hovering over links', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<svg>
|
||||||
|
<WorkflowStateContext.Provider value={workflowContext}>
|
||||||
|
<VisualizerGraph readOnly={false} />
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
wrapper.find('#link-2-3-overlay').simulate('mouseenter');
|
||||||
|
expect(wrapper.find('WorkflowLinkHelp')).toHaveLength(1);
|
||||||
|
expect(wrapper.find('WorkflowLinkHelp').contains(<b>Run</b>)).toEqual(true);
|
||||||
|
expect(
|
||||||
|
wrapper.find('WorkflowLinkHelp').containsMatchingElement(<dd>Always</dd>)
|
||||||
|
).toEqual(true);
|
||||||
|
wrapper.find('#link-2-3-overlay').simulate('mouseleave');
|
||||||
|
expect(wrapper.find('WorkflowLinkHelp')).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -25,9 +25,9 @@ const LinkG = styled.g`
|
|||||||
function VisualizerLink({
|
function VisualizerLink({
|
||||||
i18n,
|
i18n,
|
||||||
link,
|
link,
|
||||||
onUpdateHelpText,
|
updateLinkHelp,
|
||||||
onUpdateLinkHelp,
|
|
||||||
readOnly,
|
readOnly,
|
||||||
|
updateHelpText,
|
||||||
}) {
|
}) {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const [hovering, setHovering] = useState(false);
|
const [hovering, setHovering] = useState(false);
|
||||||
@@ -43,7 +43,7 @@ function VisualizerLink({
|
|||||||
id="link-add-node"
|
id="link-add-node"
|
||||||
key="add"
|
key="add"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdateHelpText(null);
|
updateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'START_ADD_NODE',
|
type: 'START_ADD_NODE',
|
||||||
@@ -52,9 +52,9 @@ function VisualizerLink({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() =>
|
onMouseEnter={() =>
|
||||||
onUpdateHelpText(i18n._(t`Add a new node between these two nodes`))
|
updateHelpText(i18n._(t`Add a new node between these two nodes`))
|
||||||
}
|
}
|
||||||
onMouseLeave={() => onUpdateHelpText(null)}
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
>
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</WorkflowActionTooltipItem>
|
</WorkflowActionTooltipItem>
|
||||||
@@ -68,18 +68,26 @@ function VisualizerLink({
|
|||||||
<WorkflowActionTooltipItem
|
<WorkflowActionTooltipItem
|
||||||
id="link-edit"
|
id="link-edit"
|
||||||
key="edit"
|
key="edit"
|
||||||
onClick={() => dispatch({ type: 'SET_LINK_TO_EDIT', value: link })}
|
onClick={() => {
|
||||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`Edit this link`))}
|
updateHelpText(null);
|
||||||
onMouseLeave={() => onUpdateHelpText(null)}
|
setHovering(false);
|
||||||
|
dispatch({ type: 'SET_LINK_TO_EDIT', value: link });
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => updateHelpText(i18n._(t`Edit this link`))}
|
||||||
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
>
|
>
|
||||||
<PencilAltIcon />
|
<PencilAltIcon />
|
||||||
</WorkflowActionTooltipItem>,
|
</WorkflowActionTooltipItem>,
|
||||||
<WorkflowActionTooltipItem
|
<WorkflowActionTooltipItem
|
||||||
id="link-delete"
|
id="link-delete"
|
||||||
key="delete"
|
key="delete"
|
||||||
onClick={() => dispatch({ type: 'START_DELETE_LINK', link })}
|
onClick={() => {
|
||||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`Delete this link`))}
|
updateHelpText(null);
|
||||||
onMouseLeave={() => onUpdateHelpText(null)}
|
setHovering(false);
|
||||||
|
dispatch({ type: 'START_DELETE_LINK', link });
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => updateHelpText(i18n._(t`Delete this link`))}
|
||||||
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
>
|
>
|
||||||
<TrashAltIcon />
|
<TrashAltIcon />
|
||||||
</WorkflowActionTooltipItem>,
|
</WorkflowActionTooltipItem>,
|
||||||
@@ -124,14 +132,15 @@ function VisualizerLink({
|
|||||||
>
|
>
|
||||||
<polygon
|
<polygon
|
||||||
fill="#E1E1E1"
|
fill="#E1E1E1"
|
||||||
id={`link-${link.source.id}-${link.target.id}-overlay`}
|
id={`link-${link.source.id}-${link.target.id}-background`}
|
||||||
opacity={hovering ? '1' : '0'}
|
opacity={hovering ? '1' : '0'}
|
||||||
points={getLinkOverlayPoints(link, nodePositions)}
|
points={getLinkOverlayPoints(link, nodePositions)}
|
||||||
/>
|
/>
|
||||||
<path d={pathD} stroke={pathStroke} strokeWidth="2px" />
|
<path d={pathD} stroke={pathStroke} strokeWidth="2px" />
|
||||||
<polygon
|
<polygon
|
||||||
onMouseEnter={() => onUpdateLinkHelp(link)}
|
id={`link-${link.source.id}-${link.target.id}-overlay`}
|
||||||
onMouseLeave={() => onUpdateLinkHelp(null)}
|
onMouseEnter={() => updateLinkHelp(link)}
|
||||||
|
onMouseLeave={() => updateLinkHelp(null)}
|
||||||
opacity="0"
|
opacity="0"
|
||||||
points={getLinkOverlayPoints(link, nodePositions)}
|
points={getLinkOverlayPoints(link, nodePositions)}
|
||||||
/>
|
/>
|
||||||
@@ -149,8 +158,8 @@ function VisualizerLink({
|
|||||||
VisualizerLink.propTypes = {
|
VisualizerLink.propTypes = {
|
||||||
link: shape().isRequired,
|
link: shape().isRequired,
|
||||||
readOnly: bool.isRequired,
|
readOnly: bool.isRequired,
|
||||||
onUpdateHelpText: func.isRequired,
|
updateHelpText: func.isRequired,
|
||||||
onUpdateLinkHelp: func.isRequired,
|
updateLinkHelp: func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(VisualizerLink);
|
export default withI18n()(VisualizerLink);
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
WorkflowDispatchContext,
|
||||||
|
WorkflowStateContext,
|
||||||
|
} from '@contexts/Workflow';
|
||||||
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
|
import VisualizerLink from './VisualizerLink';
|
||||||
|
|
||||||
|
const link = {
|
||||||
|
source: {
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
id: 3,
|
||||||
|
},
|
||||||
|
linkType: 'success',
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockedContext = {
|
||||||
|
addingLink: false,
|
||||||
|
nodePositions: {
|
||||||
|
1: {
|
||||||
|
width: 72,
|
||||||
|
height: 40,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
width: 180,
|
||||||
|
height: 60,
|
||||||
|
x: 282,
|
||||||
|
y: 40,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
width: 180,
|
||||||
|
height: 60,
|
||||||
|
x: 564,
|
||||||
|
y: 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const dispatch = jest.fn();
|
||||||
|
const updateHelpText = jest.fn();
|
||||||
|
const updateLinkHelp = jest.fn();
|
||||||
|
|
||||||
|
describe('VisualizerLink', () => {
|
||||||
|
let wrapper;
|
||||||
|
beforeAll(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<WorkflowDispatchContext.Provider value={dispatch}>
|
||||||
|
<WorkflowStateContext.Provider value={mockedContext}>
|
||||||
|
<svg>
|
||||||
|
<VisualizerLink
|
||||||
|
link={link}
|
||||||
|
readOnly={false}
|
||||||
|
updateHelpText={updateHelpText}
|
||||||
|
updateLinkHelp={updateLinkHelp}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
|
</WorkflowDispatchContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Displays action tooltip on hover and updates help text on hover', () => {
|
||||||
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
|
wrapper
|
||||||
|
.find('g')
|
||||||
|
.first()
|
||||||
|
.simulate('mouseenter');
|
||||||
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(1);
|
||||||
|
expect(wrapper.find('WorkflowActionTooltipItem').length).toBe(3);
|
||||||
|
wrapper
|
||||||
|
.find('g')
|
||||||
|
.first()
|
||||||
|
.simulate('mouseleave');
|
||||||
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
|
wrapper
|
||||||
|
.find('#link-2-3-overlay')
|
||||||
|
.first()
|
||||||
|
.simulate('mouseenter');
|
||||||
|
expect(updateLinkHelp).toHaveBeenCalledWith(link);
|
||||||
|
wrapper
|
||||||
|
.find('#link-2-3-overlay')
|
||||||
|
.first()
|
||||||
|
.simulate('mouseleave');
|
||||||
|
expect(updateLinkHelp).toHaveBeenCalledWith(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Add Node tooltip action hover/click updates help text and dispatches properly', () => {
|
||||||
|
wrapper
|
||||||
|
.find('g')
|
||||||
|
.first()
|
||||||
|
.simulate('mouseenter');
|
||||||
|
wrapper.find('#link-add-node').simulate('mouseenter');
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith(
|
||||||
|
'Add a new node between these two nodes'
|
||||||
|
);
|
||||||
|
wrapper.find('#link-add-node').simulate('mouseleave');
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith(null);
|
||||||
|
wrapper.find('#link-add-node').simulate('click');
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
type: 'START_ADD_NODE',
|
||||||
|
sourceNodeId: 2,
|
||||||
|
targetNodeId: 3,
|
||||||
|
});
|
||||||
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Edit tooltip action hover/click updates help text and dispatches properly', () => {
|
||||||
|
wrapper
|
||||||
|
.find('g')
|
||||||
|
.first()
|
||||||
|
.simulate('mouseenter');
|
||||||
|
wrapper.find('#link-edit').simulate('mouseenter');
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith('Edit this link');
|
||||||
|
wrapper.find('#link-edit').simulate('mouseleave');
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith(null);
|
||||||
|
wrapper.find('#link-edit').simulate('click');
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
type: 'SET_LINK_TO_EDIT',
|
||||||
|
value: link,
|
||||||
|
});
|
||||||
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Delete tooltip action hover/click updates help text and dispatches properly', () => {
|
||||||
|
wrapper
|
||||||
|
.find('g')
|
||||||
|
.first()
|
||||||
|
.simulate('mouseenter');
|
||||||
|
wrapper.find('#link-delete').simulate('mouseenter');
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith('Delete this link');
|
||||||
|
wrapper.find('#link-delete').simulate('mouseleave');
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith(null);
|
||||||
|
wrapper.find('#link-delete').simulate('click');
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
type: 'START_DELETE_LINK',
|
||||||
|
link,
|
||||||
|
});
|
||||||
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -33,20 +33,21 @@ const NodeContents = styled.div`
|
|||||||
props.isInvalidLinkTarget ? '#D7D7D7' : '#FFFFFF'};
|
props.isInvalidLinkTarget ? '#D7D7D7' : '#FFFFFF'};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const NodeDefaultLabel = styled.p`
|
const NodeResourceName = styled.p`
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
`;
|
`;
|
||||||
|
NodeResourceName.displayName = 'NodeResourceName';
|
||||||
|
|
||||||
function VisualizerNode({
|
function VisualizerNode({
|
||||||
i18n,
|
i18n,
|
||||||
node,
|
node,
|
||||||
onMouseOver,
|
onMouseOver,
|
||||||
readOnly,
|
readOnly,
|
||||||
onUpdateHelpText,
|
updateHelpText,
|
||||||
updateNodeHelp,
|
updateNodeHelp,
|
||||||
}) {
|
}) {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
@@ -62,7 +63,7 @@ function VisualizerNode({
|
|||||||
ref.current.parentNode.appendChild(ref.current);
|
ref.current.parentNode.appendChild(ref.current);
|
||||||
setHovering(true);
|
setHovering(true);
|
||||||
if (addingLink) {
|
if (addingLink) {
|
||||||
onUpdateHelpText(
|
updateHelpText(
|
||||||
node.isInvalidLinkTarget
|
node.isInvalidLinkTarget
|
||||||
? i18n._(
|
? i18n._(
|
||||||
t`Invalid link target. Unable to link to children or ancestor nodes. Graph cycles are not supported.`
|
t`Invalid link target. Unable to link to children or ancestor nodes. Graph cycles are not supported.`
|
||||||
@@ -76,7 +77,7 @@ function VisualizerNode({
|
|||||||
const handleNodeMouseLeave = () => {
|
const handleNodeMouseLeave = () => {
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
if (addingLink) {
|
if (addingLink) {
|
||||||
onUpdateHelpText(null);
|
updateHelpText(null);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,12 +92,12 @@ function VisualizerNode({
|
|||||||
id="node-details"
|
id="node-details"
|
||||||
key="details"
|
key="details"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdateHelpText(null);
|
updateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
dispatch({ type: 'SET_NODE_TO_VIEW', value: node });
|
dispatch({ type: 'SET_NODE_TO_VIEW', value: node });
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`View node details`))}
|
onMouseEnter={() => updateHelpText(i18n._(t`View node details`))}
|
||||||
onMouseLeave={() => onUpdateHelpText(null)}
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
>
|
>
|
||||||
<InfoIcon />
|
<InfoIcon />
|
||||||
</WorkflowActionTooltipItem>
|
</WorkflowActionTooltipItem>
|
||||||
@@ -109,12 +110,12 @@ function VisualizerNode({
|
|||||||
id="node-add"
|
id="node-add"
|
||||||
key="add"
|
key="add"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdateHelpText(null);
|
updateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
dispatch({ type: 'START_ADD_NODE', sourceNodeId: node.id });
|
dispatch({ type: 'START_ADD_NODE', sourceNodeId: node.id });
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`Add a new node`))}
|
onMouseEnter={() => updateHelpText(i18n._(t`Add a new node`))}
|
||||||
onMouseLeave={() => onUpdateHelpText(null)}
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
>
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
</WorkflowActionTooltipItem>,
|
</WorkflowActionTooltipItem>,
|
||||||
@@ -123,12 +124,12 @@ function VisualizerNode({
|
|||||||
id="node-edit"
|
id="node-edit"
|
||||||
key="edit"
|
key="edit"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdateHelpText(null);
|
updateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
dispatch({ type: 'SET_NODE_TO_EDIT', value: node });
|
dispatch({ type: 'SET_NODE_TO_EDIT', value: node });
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`Edit this node`))}
|
onMouseEnter={() => updateHelpText(i18n._(t`Edit this node`))}
|
||||||
onMouseLeave={() => onUpdateHelpText(null)}
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
>
|
>
|
||||||
<PencilAltIcon />
|
<PencilAltIcon />
|
||||||
</WorkflowActionTooltipItem>,
|
</WorkflowActionTooltipItem>,
|
||||||
@@ -136,14 +137,14 @@ function VisualizerNode({
|
|||||||
id="node-link"
|
id="node-link"
|
||||||
key="link"
|
key="link"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdateHelpText(null);
|
updateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
dispatch({ type: 'SELECT_SOURCE_FOR_LINKING', node });
|
dispatch({ type: 'SELECT_SOURCE_FOR_LINKING', node });
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() =>
|
onMouseEnter={() =>
|
||||||
onUpdateHelpText(i18n._(t`Link to an available node`))
|
updateHelpText(i18n._(t`Link to an available node`))
|
||||||
}
|
}
|
||||||
onMouseLeave={() => onUpdateHelpText(null)}
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
>
|
>
|
||||||
<LinkIcon />
|
<LinkIcon />
|
||||||
</WorkflowActionTooltipItem>,
|
</WorkflowActionTooltipItem>,
|
||||||
@@ -151,12 +152,12 @@ function VisualizerNode({
|
|||||||
id="node-delete"
|
id="node-delete"
|
||||||
key="delete"
|
key="delete"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onUpdateHelpText(null);
|
updateHelpText(null);
|
||||||
setHovering(false);
|
setHovering(false);
|
||||||
dispatch({ type: 'SET_NODE_TO_DELETE', value: node });
|
dispatch({ type: 'SET_NODE_TO_DELETE', value: node });
|
||||||
}}
|
}}
|
||||||
onMouseEnter={() => onUpdateHelpText(i18n._(t`Delete this node`))}
|
onMouseEnter={() => updateHelpText(i18n._(t`Delete this node`))}
|
||||||
onMouseLeave={() => onUpdateHelpText(null)}
|
onMouseLeave={() => updateHelpText(null)}
|
||||||
>
|
>
|
||||||
<TrashAltIcon />
|
<TrashAltIcon />
|
||||||
</WorkflowActionTooltipItem>,
|
</WorkflowActionTooltipItem>,
|
||||||
@@ -198,11 +199,11 @@ function VisualizerNode({
|
|||||||
y="1"
|
y="1"
|
||||||
>
|
>
|
||||||
<NodeContents isInvalidLinkTarget={node.isInvalidLinkTarget}>
|
<NodeContents isInvalidLinkTarget={node.isInvalidLinkTarget}>
|
||||||
<NodeDefaultLabel>
|
<NodeResourceName>
|
||||||
{node.unifiedJobTemplate
|
{node.unifiedJobTemplate
|
||||||
? node.unifiedJobTemplate.name
|
? node.unifiedJobTemplate.name
|
||||||
: i18n._(t`DELETED`)}
|
: i18n._(t`DELETED`)}
|
||||||
</NodeDefaultLabel>
|
</NodeResourceName>
|
||||||
</NodeContents>
|
</NodeContents>
|
||||||
</foreignObject>
|
</foreignObject>
|
||||||
{node.unifiedJobTemplate && <WorkflowNodeTypeLetter node={node} />}
|
{node.unifiedJobTemplate && <WorkflowNodeTypeLetter node={node} />}
|
||||||
@@ -221,7 +222,7 @@ VisualizerNode.propTypes = {
|
|||||||
node: shape().isRequired,
|
node: shape().isRequired,
|
||||||
onMouseOver: func,
|
onMouseOver: func,
|
||||||
readOnly: bool.isRequired,
|
readOnly: bool.isRequired,
|
||||||
onUpdateHelpText: func.isRequired,
|
updateHelpText: func.isRequired,
|
||||||
updateNodeHelp: func.isRequired,
|
updateNodeHelp: func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
WorkflowDispatchContext,
|
||||||
|
WorkflowStateContext,
|
||||||
|
} from '@contexts/Workflow';
|
||||||
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
|
import VisualizerNode from './VisualizerNode';
|
||||||
|
|
||||||
|
const mockedContext = {
|
||||||
|
addingLink: false,
|
||||||
|
addLinkSourceNode: null,
|
||||||
|
nodePositions: {
|
||||||
|
1: {
|
||||||
|
width: 72,
|
||||||
|
height: 40,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
width: 180,
|
||||||
|
height: 60,
|
||||||
|
x: 282,
|
||||||
|
y: 40,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodeWithJT = {
|
||||||
|
id: 2,
|
||||||
|
unifiedJobTemplate: {
|
||||||
|
id: 77,
|
||||||
|
name: 'Automation JT',
|
||||||
|
type: 'job_template',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const dispatch = jest.fn();
|
||||||
|
const updateHelpText = jest.fn();
|
||||||
|
const updateNodeHelp = jest.fn();
|
||||||
|
|
||||||
|
describe('VisualizerNode', () => {
|
||||||
|
describe('Node with unified job template', () => {
|
||||||
|
let wrapper;
|
||||||
|
beforeAll(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<WorkflowDispatchContext.Provider value={dispatch}>
|
||||||
|
<WorkflowStateContext.Provider value={mockedContext}>
|
||||||
|
<svg>
|
||||||
|
<VisualizerNode
|
||||||
|
mouseEnter={() => {}}
|
||||||
|
mouseLeave={() => {}}
|
||||||
|
node={nodeWithJT}
|
||||||
|
readOnly={false}
|
||||||
|
updateHelpText={updateHelpText}
|
||||||
|
updateNodeHelp={updateNodeHelp}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
|
</WorkflowDispatchContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
test('Displays unified job template name inside node', () => {
|
||||||
|
expect(wrapper.find('NodeResourceName').text()).toBe('Automation JT');
|
||||||
|
});
|
||||||
|
test('Displays action tooltip on hover and updates help text on hover', () => {
|
||||||
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
|
wrapper.find('VisualizerNode').simulate('mouseenter');
|
||||||
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(1);
|
||||||
|
expect(wrapper.find('WorkflowActionTooltipItem').length).toBe(5);
|
||||||
|
wrapper.find('VisualizerNode').simulate('mouseleave');
|
||||||
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
|
wrapper
|
||||||
|
.find('foreignObject')
|
||||||
|
.first()
|
||||||
|
.simulate('mouseenter');
|
||||||
|
expect(updateNodeHelp).toHaveBeenCalledWith(nodeWithJT);
|
||||||
|
wrapper
|
||||||
|
.find('foreignObject')
|
||||||
|
.first()
|
||||||
|
.simulate('mouseleave');
|
||||||
|
expect(updateNodeHelp).toHaveBeenCalledWith(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Add tooltip action hover/click updates help text and dispatches properly', () => {
|
||||||
|
wrapper.find('VisualizerNode').simulate('mouseenter');
|
||||||
|
wrapper.find('#node-add').simulate('mouseenter');
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith('Add a new node');
|
||||||
|
wrapper.find('#node-add').simulate('mouseleave');
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith(null);
|
||||||
|
wrapper.find('#node-add').simulate('click');
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
type: 'START_ADD_NODE',
|
||||||
|
sourceNodeId: 2,
|
||||||
|
});
|
||||||
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Edit tooltip action hover/click updates help text and dispatches properly', () => {
|
||||||
|
wrapper.find('VisualizerNode').simulate('mouseenter');
|
||||||
|
wrapper.find('#node-edit').simulate('mouseenter');
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith('Edit this node');
|
||||||
|
wrapper.find('#node-edit').simulate('mouseleave');
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith(null);
|
||||||
|
wrapper.find('#node-edit').simulate('click');
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
type: 'SET_NODE_TO_EDIT',
|
||||||
|
value: nodeWithJT,
|
||||||
|
});
|
||||||
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Details tooltip action hover/click updates help text and dispatches properly', () => {
|
||||||
|
wrapper.find('VisualizerNode').simulate('mouseenter');
|
||||||
|
wrapper.find('#node-details').simulate('mouseenter');
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith('View node details');
|
||||||
|
wrapper.find('#node-details').simulate('mouseleave');
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith(null);
|
||||||
|
wrapper.find('#node-details').simulate('click');
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
type: 'SET_NODE_TO_VIEW',
|
||||||
|
value: nodeWithJT,
|
||||||
|
});
|
||||||
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Link tooltip action hover/click updates help text and dispatches properly', () => {
|
||||||
|
wrapper.find('VisualizerNode').simulate('mouseenter');
|
||||||
|
wrapper.find('#node-link').simulate('mouseenter');
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith('Link to an available node');
|
||||||
|
wrapper.find('#node-link').simulate('mouseleave');
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith(null);
|
||||||
|
wrapper.find('#node-link').simulate('click');
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
type: 'SELECT_SOURCE_FOR_LINKING',
|
||||||
|
node: nodeWithJT,
|
||||||
|
});
|
||||||
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Delete tooltip action hover/click updates help text and dispatches properly', () => {
|
||||||
|
wrapper.find('VisualizerNode').simulate('mouseenter');
|
||||||
|
wrapper.find('#node-delete').simulate('mouseenter');
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith('Delete this node');
|
||||||
|
wrapper.find('#node-delete').simulate('mouseleave');
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith(null);
|
||||||
|
wrapper.find('#node-delete').simulate('click');
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
type: 'SET_NODE_TO_DELETE',
|
||||||
|
value: nodeWithJT,
|
||||||
|
});
|
||||||
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Node actions while adding a new link', () => {
|
||||||
|
let wrapper;
|
||||||
|
beforeAll(() => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<WorkflowDispatchContext.Provider value={dispatch}>
|
||||||
|
<WorkflowStateContext.Provider
|
||||||
|
value={{
|
||||||
|
...mockedContext,
|
||||||
|
addingLink: true,
|
||||||
|
addLinkSourceNode: 323,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg>
|
||||||
|
<VisualizerNode
|
||||||
|
mouseEnter={() => {}}
|
||||||
|
mouseLeave={() => {}}
|
||||||
|
node={nodeWithJT}
|
||||||
|
readOnly={false}
|
||||||
|
updateHelpText={updateHelpText}
|
||||||
|
updateNodeHelp={updateNodeHelp}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
|
</WorkflowDispatchContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
afterAll(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
test('Displays correct help text when hovering over node while adding link', () => {
|
||||||
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
|
wrapper.find('VisualizerNode').simulate('mouseenter');
|
||||||
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith(
|
||||||
|
'Click to create a new link to this node.'
|
||||||
|
);
|
||||||
|
wrapper.find('VisualizerNode').simulate('mouseleave');
|
||||||
|
expect(wrapper.find('WorkflowActionTooltip').length).toBe(0);
|
||||||
|
expect(updateHelpText).toHaveBeenCalledWith(null);
|
||||||
|
});
|
||||||
|
test('Dispatches properly when node is clicked', () => {
|
||||||
|
wrapper
|
||||||
|
.find('foreignObject')
|
||||||
|
.first()
|
||||||
|
.simulate('click');
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
type: 'SET_ADD_LINK_TARGET_NODE',
|
||||||
|
value: nodeWithJT,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('Node without unified job template', () => {
|
||||||
|
test('Displays DELETED text inside node when unified job template is missing', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<svg>
|
||||||
|
<WorkflowStateContext.Provider value={mockedContext}>
|
||||||
|
<VisualizerNode
|
||||||
|
mouseEnter={() => {}}
|
||||||
|
mouseLeave={() => {}}
|
||||||
|
node={{
|
||||||
|
id: 2,
|
||||||
|
}}
|
||||||
|
readOnly={false}
|
||||||
|
updateHelpText={() => {}}
|
||||||
|
updateNodeHelp={() => {}}
|
||||||
|
/>
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
expect(wrapper).toHaveLength(1);
|
||||||
|
expect(wrapper.find('NodeResourceName').text()).toBe('DELETED');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { WorkflowDispatchContext } from '@contexts/Workflow';
|
||||||
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
|
import VisualizerStartScreen from './VisualizerStartScreen';
|
||||||
|
|
||||||
|
const dispatch = jest.fn();
|
||||||
|
|
||||||
|
describe('VisualizerStartScreen', () => {
|
||||||
|
test('dispatches properly when start button clicked', () => {
|
||||||
|
const wrapper = mountWithContexts(
|
||||||
|
<WorkflowDispatchContext.Provider value={dispatch}>
|
||||||
|
<VisualizerStartScreen />
|
||||||
|
</WorkflowDispatchContext.Provider>
|
||||||
|
);
|
||||||
|
expect(wrapper).toHaveLength(1);
|
||||||
|
wrapper.find('Button').simulate('click');
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
type: 'START_ADD_NODE',
|
||||||
|
sourceNodeId: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -99,7 +99,11 @@ function VisualizerToolbar({ i18n, onClose, onSave, template }) {
|
|||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<VerticalSeparator />
|
<VerticalSeparator />
|
||||||
<Button variant="primary" onClick={onSave}>
|
<Button
|
||||||
|
aria-label={i18n._(t`Save`)}
|
||||||
|
variant="primary"
|
||||||
|
onClick={onSave}
|
||||||
|
>
|
||||||
{i18n._(t`Save`)}
|
{i18n._(t`Save`)}
|
||||||
</Button>
|
</Button>
|
||||||
<VerticalSeparator />
|
<VerticalSeparator />
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
WorkflowDispatchContext,
|
||||||
|
WorkflowStateContext,
|
||||||
|
} from '@contexts/Workflow';
|
||||||
|
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||||
|
import VisualizerToolbar from './VisualizerToolbar';
|
||||||
|
|
||||||
|
let wrapper;
|
||||||
|
const close = jest.fn();
|
||||||
|
const dispatch = jest.fn();
|
||||||
|
const save = jest.fn();
|
||||||
|
const template = {
|
||||||
|
id: 1,
|
||||||
|
name: 'Test JT',
|
||||||
|
};
|
||||||
|
const workflowContext = {
|
||||||
|
nodes: [],
|
||||||
|
showLegend: false,
|
||||||
|
showTools: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('VisualizerToolbar', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
const nodes = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
isDeleted: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<WorkflowDispatchContext.Provider value={dispatch}>
|
||||||
|
<WorkflowStateContext.Provider value={{ ...workflowContext, nodes }}>
|
||||||
|
<VisualizerToolbar
|
||||||
|
onClose={close}
|
||||||
|
onSave={save}
|
||||||
|
template={template}
|
||||||
|
/>
|
||||||
|
</WorkflowStateContext.Provider>
|
||||||
|
</WorkflowDispatchContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Shows correct number of nodes', () => {
|
||||||
|
// The start node (id=1) and deleted nodes (isDeleted=true) should be ignored
|
||||||
|
expect(wrapper.find('Badge').text()).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Toggle Legend button dispatches as expected', () => {
|
||||||
|
wrapper.find('CompassIcon').simulate('click');
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({ type: 'TOGGLE_LEGEND' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Toggle Tools button dispatches as expected', () => {
|
||||||
|
wrapper.find('WrenchIcon').simulate('click');
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({ type: 'TOGGLE_TOOLS' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Delete All button dispatches as expected', () => {
|
||||||
|
wrapper.find('TrashAltIcon').simulate('click');
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
type: 'SET_SHOW_DELETE_ALL_NODES_MODAL',
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Delete All button dispatches as expected', () => {
|
||||||
|
wrapper.find('TrashAltIcon').simulate('click');
|
||||||
|
expect(dispatch).toHaveBeenCalledWith({
|
||||||
|
type: 'SET_SHOW_DELETE_ALL_NODES_MODAL',
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Save button calls expected function', () => {
|
||||||
|
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||||
|
expect(save).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Close button calls expected function', () => {
|
||||||
|
wrapper.find('TimesIcon').simulate('click');
|
||||||
|
expect(close).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user