mirror of
https://github.com/ansible/awx.git
synced 2026-01-12 18:40:01 -03:30
Merge pull request #11938 from kialam/mesh-viz-unit-tests
Mesh viz unit tests
This commit is contained in:
commit
ec5e677635
806
awx/ui/package-lock.json
generated
806
awx/ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -45,6 +45,8 @@
|
||||
"@lingui/loader": "^3.8.3",
|
||||
"@lingui/macro": "^3.7.1",
|
||||
"@nteract/mockument": "^1.0.4",
|
||||
"@testing-library/jest-dom": "^5.16.2",
|
||||
"@testing-library/react": "^12.1.4",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "0.6.5",
|
||||
"babel-plugin-macros": "3.1.0",
|
||||
"enzyme": "^3.10.0",
|
||||
|
||||
@ -39,15 +39,23 @@ function MeshGraph({ data, showLegend, zoom, setShowZoomControls }) {
|
||||
const history = useHistory();
|
||||
|
||||
const draw = () => {
|
||||
let width;
|
||||
let height;
|
||||
setShowZoomControls(false);
|
||||
const width = getWidth(SELECTOR);
|
||||
const height = getHeight(SELECTOR);
|
||||
try {
|
||||
width = getWidth(SELECTOR);
|
||||
height = getHeight(SELECTOR);
|
||||
} catch (error) {
|
||||
width = 700;
|
||||
height = 600;
|
||||
}
|
||||
|
||||
/* Add SVG */
|
||||
d3.selectAll(`#chart > svg`).remove();
|
||||
const svg = d3
|
||||
.select('#chart')
|
||||
.append('svg')
|
||||
.attr('aria-label', 'mesh-svg')
|
||||
.attr('class', 'mesh-svg')
|
||||
.attr('width', `${width}px`)
|
||||
.attr('height', `100%`);
|
||||
|
||||
86
awx/ui/src/screens/TopologyView/MeshGraph__RTL.test.js
Normal file
86
awx/ui/src/screens/TopologyView/MeshGraph__RTL.test.js
Normal file
@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { render, waitFor, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import MeshGraph from './MeshGraph';
|
||||
|
||||
jest.mock('util/webWorker', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: () => {
|
||||
return {
|
||||
postMessage: jest.fn().mockReturnValueOnce({
|
||||
data: {
|
||||
type: 'end',
|
||||
links: [],
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
hostname: 'foo',
|
||||
node_type: 'control',
|
||||
node_state: 'healthy',
|
||||
index: 0,
|
||||
vx: -1,
|
||||
vy: -5,
|
||||
x: 400,
|
||||
y: 300,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
hostname: 'bar',
|
||||
node_type: 'control',
|
||||
node_state: 'healthy',
|
||||
index: 1,
|
||||
vx: -1,
|
||||
vy: -5,
|
||||
x: 500,
|
||||
y: 200,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
onmessage: jest.fn(),
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('<MeshGraph />', () => {
|
||||
test('renders correctly', async () => {
|
||||
const mockData = {
|
||||
data: {
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
hostname: 'foo',
|
||||
node_type: 'control',
|
||||
node_state: 'healthy',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
hostname: 'bar',
|
||||
node_type: 'control',
|
||||
node_state: 'healthy',
|
||||
},
|
||||
],
|
||||
links: [],
|
||||
},
|
||||
};
|
||||
const mockZoomFn = jest.fn();
|
||||
const mockSetZoomCtrFn = jest.fn();
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<MeshGraph
|
||||
data={mockData}
|
||||
showLegend={true}
|
||||
zoom={mockZoomFn}
|
||||
setShowZoomControls={mockSetZoomCtrFn}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
await waitFor(() => screen.getByLabelText('mesh-svg'));
|
||||
expect(screen.getByLabelText('mesh-svg')).toBeVisible();
|
||||
});
|
||||
});
|
||||
135
awx/ui/src/screens/TopologyView/TopologyView__RTL.test.js
Normal file
135
awx/ui/src/screens/TopologyView/TopologyView__RTL.test.js
Normal file
@ -0,0 +1,135 @@
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { MeshAPI } from 'api';
|
||||
import { render, waitFor, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import TopologyView from './TopologyView';
|
||||
|
||||
jest.mock('../../api');
|
||||
jest.mock('util/webWorker', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: () => {
|
||||
return {
|
||||
postMessage: jest.fn().mockReturnValueOnce({
|
||||
data: {
|
||||
type: 'end',
|
||||
links: [],
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
hostname: 'foo',
|
||||
node_type: 'control',
|
||||
node_state: 'healthy',
|
||||
index: 0,
|
||||
vx: -1,
|
||||
vy: -5,
|
||||
x: 400,
|
||||
y: 300,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
hostname: 'bar',
|
||||
node_type: 'control',
|
||||
node_state: 'healthy',
|
||||
index: 1,
|
||||
vx: -1,
|
||||
vy: -5,
|
||||
x: 500,
|
||||
y: 200,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
onmessage: function handleWorkerEvent(event) {
|
||||
switch (event.data.type) {
|
||||
case 'tick':
|
||||
return jest.fn(event.data);
|
||||
case 'end':
|
||||
return jest.fn(event.data);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('<TopologyView />', () => {
|
||||
test('should render properly', async () => {
|
||||
MeshAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
nodes: [
|
||||
{
|
||||
id: 1,
|
||||
hostname: 'foo',
|
||||
node_type: 'control',
|
||||
node_state: 'healthy',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
hostname: 'bar',
|
||||
node_type: 'control',
|
||||
node_state: 'healthy',
|
||||
},
|
||||
],
|
||||
links: [],
|
||||
},
|
||||
});
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<TopologyView />
|
||||
</MemoryRouter>
|
||||
);
|
||||
await waitFor(() => screen.getByRole('heading', { level: 2 }));
|
||||
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(
|
||||
'Topology View'
|
||||
);
|
||||
expect(screen.getByLabelText('mesh-svg')).toBeVisible();
|
||||
});
|
||||
test('should render with 0 nodes', async () => {
|
||||
MeshAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
nodes: [],
|
||||
links: [],
|
||||
},
|
||||
});
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<TopologyView />
|
||||
</MemoryRouter>
|
||||
);
|
||||
await waitFor(() => screen.getByRole('heading', { level: 2 }));
|
||||
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(
|
||||
'Topology View'
|
||||
);
|
||||
expect(screen.getByLabelText('mesh-svg')).toBeVisible();
|
||||
});
|
||||
test('should handle API error', async () => {
|
||||
MeshAPI.read.mockRejectedValueOnce(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'get',
|
||||
url: '/api/v2/mesh_visualizer',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
status: 500,
|
||||
},
|
||||
})
|
||||
);
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<TopologyView />
|
||||
</MemoryRouter>
|
||||
);
|
||||
await waitFor(() => screen.getByRole('heading', { level: 2 }));
|
||||
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(
|
||||
'Topology View'
|
||||
);
|
||||
expect(screen.getByText(/something went wrong/i)).toBeVisible();
|
||||
});
|
||||
});
|
||||
@ -8,34 +8,49 @@ import {
|
||||
LABEL_TEXT_MAX_LENGTH,
|
||||
} from '../constants';
|
||||
|
||||
export function getWidth(selector) {
|
||||
return selector ? d3.select(selector).node().clientWidth : 700;
|
||||
}
|
||||
|
||||
export function getHeight(selector) {
|
||||
return selector ? d3.select(selector).node().clientHeight : 600;
|
||||
}
|
||||
|
||||
export function renderStateColor(nodeState) {
|
||||
return NODE_STATE_COLOR_KEY[nodeState];
|
||||
return NODE_STATE_COLOR_KEY[nodeState] ? NODE_STATE_COLOR_KEY[nodeState] : '';
|
||||
}
|
||||
|
||||
export function renderLabelText(nodeState, name) {
|
||||
return `${NODE_STATE_HTML_ENTITY_KEY[nodeState]} ${truncateString(
|
||||
name,
|
||||
LABEL_TEXT_MAX_LENGTH
|
||||
)}`;
|
||||
if (typeof nodeState === 'string' && typeof name === 'string') {
|
||||
return NODE_STATE_HTML_ENTITY_KEY[nodeState]
|
||||
? `${NODE_STATE_HTML_ENTITY_KEY[nodeState]} ${truncateString(
|
||||
name,
|
||||
LABEL_TEXT_MAX_LENGTH
|
||||
)}`
|
||||
: ` ${truncateString(name, LABEL_TEXT_MAX_LENGTH)}`;
|
||||
}
|
||||
return ``;
|
||||
}
|
||||
|
||||
export function renderNodeType(nodeType) {
|
||||
return NODE_TYPE_SYMBOL_KEY[nodeType];
|
||||
return NODE_TYPE_SYMBOL_KEY[nodeType] ? NODE_TYPE_SYMBOL_KEY[nodeType] : ``;
|
||||
}
|
||||
|
||||
export function renderNodeIcon(selectedNode) {
|
||||
if (selectedNode) {
|
||||
const { node_type: nodeType } = selectedNode;
|
||||
return NODE_TYPE_SYMBOL_KEY[nodeType];
|
||||
return NODE_TYPE_SYMBOL_KEY[nodeType] ? NODE_TYPE_SYMBOL_KEY[nodeType] : ``;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function redirectToDetailsPage(selectedNode, history) {
|
||||
const { id: nodeId } = selectedNode;
|
||||
const constructedURL = `/instances/${nodeId}/details`;
|
||||
history.push(constructedURL);
|
||||
if (selectedNode && history) {
|
||||
const { id: nodeId } = selectedNode;
|
||||
const constructedURL = `/instances/${nodeId}/details`;
|
||||
history.push(constructedURL);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// DEBUG TOOLS
|
||||
@ -79,11 +94,3 @@ export const generateRandomNodes = (n) => {
|
||||
}
|
||||
return generateRandomLinks(nodes, getRandomInt(1, n - 1));
|
||||
};
|
||||
|
||||
export function getWidth(selector) {
|
||||
return selector ? d3.select(selector).node().clientWidth : 700;
|
||||
}
|
||||
|
||||
export function getHeight(selector) {
|
||||
return selector !== null ? d3.select(selector).node().clientHeight : 600;
|
||||
}
|
||||
|
||||
84
awx/ui/src/screens/TopologyView/utils/helpers__RTL.test.js
Normal file
84
awx/ui/src/screens/TopologyView/utils/helpers__RTL.test.js
Normal file
@ -0,0 +1,84 @@
|
||||
import {
|
||||
renderStateColor,
|
||||
renderLabelText,
|
||||
renderNodeType,
|
||||
renderNodeIcon,
|
||||
redirectToDetailsPage,
|
||||
getHeight,
|
||||
getWidth,
|
||||
} from './helpers';
|
||||
|
||||
describe('renderStateColor', () => {
|
||||
test('returns correct node state color', () => {
|
||||
expect(renderStateColor('healthy')).toBe('#3E8635');
|
||||
});
|
||||
test('returns empty string if state is not found', () => {
|
||||
expect(renderStateColor('foo')).toBe('');
|
||||
});
|
||||
test('returns empty string if state is null', () => {
|
||||
expect(renderStateColor(null)).toBe('');
|
||||
});
|
||||
test('returns empty string if state is zero/integer', () => {
|
||||
expect(renderStateColor(0)).toBe('');
|
||||
});
|
||||
});
|
||||
describe('renderNodeType', () => {
|
||||
test('returns correct node type', () => {
|
||||
expect(renderNodeType('control')).toBe('C');
|
||||
});
|
||||
test('returns empty string if state is not found', () => {
|
||||
expect(renderNodeType('foo')).toBe('');
|
||||
});
|
||||
test('returns empty string if state is null', () => {
|
||||
expect(renderNodeType(null)).toBe('');
|
||||
});
|
||||
test('returns empty string if state is zero/integer', () => {
|
||||
expect(renderNodeType(0)).toBe('');
|
||||
});
|
||||
});
|
||||
describe('renderNodeIcon', () => {
|
||||
test('returns correct node icon', () => {
|
||||
expect(renderNodeIcon({ node_type: 'control' })).toBe('C');
|
||||
});
|
||||
test('returns empty string if state is not found', () => {
|
||||
expect(renderNodeIcon('foo')).toBe('');
|
||||
});
|
||||
test('returns empty string if state is null', () => {
|
||||
expect(renderNodeIcon(null)).toBe(false);
|
||||
});
|
||||
test('returns empty string if state is zero/integer', () => {
|
||||
expect(renderNodeIcon(0)).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('getWidth', () => {
|
||||
test('returns 700 if selector is null', () => {
|
||||
expect(getWidth(null)).toBe(700);
|
||||
});
|
||||
test('returns 700 if selector is zero/integer', () => {
|
||||
expect(getWidth(0)).toBe(700);
|
||||
});
|
||||
});
|
||||
describe('getHeight', () => {
|
||||
test('returns 600 if selector is null', () => {
|
||||
expect(getHeight(null)).toBe(600);
|
||||
});
|
||||
test('returns 600 if selector is zero/integer', () => {
|
||||
expect(getHeight(0)).toBe(600);
|
||||
});
|
||||
});
|
||||
describe('renderLabelText', () => {
|
||||
test('returns label text correctly', () => {
|
||||
expect(renderLabelText('error', 'foo')).toBe('! foo');
|
||||
});
|
||||
test('returns label text if invalid node state is passed', () => {
|
||||
expect(renderLabelText('foo', 'bar')).toBe(' bar');
|
||||
});
|
||||
test('returns empty string if non string params are passed', () => {
|
||||
expect(renderLabelText(0, null)).toBe('');
|
||||
});
|
||||
});
|
||||
describe('redirectToDetailsPage', () => {
|
||||
test('returns false if incorrect params are passed', () => {
|
||||
expect(redirectToDetailsPage(null, 0)).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -20,6 +20,9 @@ import { getWidth, getHeight } from './helpers';
|
||||
*/
|
||||
|
||||
export default function useZoom(parentSelector, childSelector) {
|
||||
if (typeof parentSelector !== 'string' && typeof childSelector !== 'string') {
|
||||
return false;
|
||||
}
|
||||
const zoom = d3.zoom().on('zoom', ({ transform }) => {
|
||||
d3.select(childSelector).attr('transform', transform);
|
||||
});
|
||||
|
||||
134
awx/ui/src/screens/TopologyView/utils/useZoom__RTL.test.js
Normal file
134
awx/ui/src/screens/TopologyView/utils/useZoom__RTL.test.js
Normal file
@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent, waitFor, screen } from '@testing-library/react';
|
||||
import '@testing-library/jest-dom';
|
||||
import useZoom from './useZoom';
|
||||
import Header from '../Header';
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('useZoom', () => {
|
||||
test('hook returns a set of zoom functions', async () => {
|
||||
render(
|
||||
<svg className="parent" width="700" height="500">
|
||||
<g className="child"></g>
|
||||
</svg>
|
||||
);
|
||||
const hook = useZoom('.parent', '.child');
|
||||
expect(hook).toMatchObject({
|
||||
zoom: expect.any(Function),
|
||||
zoomFit: expect.any(Function),
|
||||
zoomIn: expect.any(Function),
|
||||
zoomOut: expect.any(Function),
|
||||
resetZoom: expect.any(Function),
|
||||
});
|
||||
});
|
||||
test('user can zoom in', async () => {
|
||||
const hook = useZoom('.parent', '.child');
|
||||
jest.spyOn(hook, 'zoomIn').mockReturnValueOnce(jest.fn());
|
||||
render(
|
||||
<>
|
||||
<Header
|
||||
title={`Topology View`}
|
||||
handleSwitchToggle={jest.fn()}
|
||||
toggleState={true}
|
||||
zoomIn={hook.zoomIn}
|
||||
zoomOut={hook.zoomOut}
|
||||
zoomFit={hook.zoomFit}
|
||||
resetZoom={hook.resetZoom}
|
||||
showZoomControls={true}
|
||||
/>
|
||||
<svg className="parent" width="700" height="500">
|
||||
<g className="child"></g>
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
await waitFor(() => screen.getByRole('heading', { level: 2 }));
|
||||
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(
|
||||
'Topology View'
|
||||
);
|
||||
fireEvent.click(screen.getByLabelText(/zoom in/i));
|
||||
expect(hook.zoomIn).toBeCalledTimes(1);
|
||||
});
|
||||
test('user can zoom out', async () => {
|
||||
const hook = useZoom('.parent', '.child');
|
||||
jest.spyOn(hook, 'zoomOut').mockReturnValueOnce(jest.fn());
|
||||
render(
|
||||
<>
|
||||
<Header
|
||||
title={`Topology View`}
|
||||
handleSwitchToggle={jest.fn()}
|
||||
toggleState={true}
|
||||
zoomIn={hook.zoomIn}
|
||||
zoomOut={hook.zoomOut}
|
||||
zoomFit={hook.zoomFit}
|
||||
resetZoom={hook.resetZoom}
|
||||
showZoomControls={true}
|
||||
/>
|
||||
<svg className="parent" width="700" height="500">
|
||||
<g className="child"></g>
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
await waitFor(() => screen.getByRole('heading', { level: 2 }));
|
||||
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(
|
||||
'Topology View'
|
||||
);
|
||||
fireEvent.click(screen.getByLabelText(/zoom out/i));
|
||||
expect(hook.zoomOut).toBeCalledTimes(1);
|
||||
});
|
||||
test('user can zoom fit', async () => {
|
||||
const hook = useZoom('.parent', '.child');
|
||||
jest.spyOn(hook, 'zoomFit').mockReturnValueOnce(jest.fn());
|
||||
render(
|
||||
<>
|
||||
<Header
|
||||
title={`Topology View`}
|
||||
handleSwitchToggle={jest.fn()}
|
||||
toggleState={true}
|
||||
zoomIn={hook.zoomIn}
|
||||
zoomOut={hook.zoomOut}
|
||||
zoomFit={hook.zoomFit}
|
||||
resetZoom={hook.resetZoom}
|
||||
showZoomControls={true}
|
||||
/>
|
||||
<svg className="parent" width="700" height="500">
|
||||
<g className="child"></g>
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
await waitFor(() => screen.getByRole('heading', { level: 2 }));
|
||||
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(
|
||||
'Topology View'
|
||||
);
|
||||
fireEvent.click(screen.getByLabelText(/fit to screen/i));
|
||||
expect(hook.zoomFit).toBeCalledTimes(1);
|
||||
});
|
||||
test('user can reset zoom', async () => {
|
||||
const hook = useZoom('.parent', '.child');
|
||||
jest.spyOn(hook, 'resetZoom').mockReturnValueOnce(jest.fn());
|
||||
render(
|
||||
<>
|
||||
<Header
|
||||
title={`Topology View`}
|
||||
handleSwitchToggle={jest.fn()}
|
||||
toggleState={true}
|
||||
zoomIn={hook.zoomIn}
|
||||
zoomOut={hook.zoomOut}
|
||||
zoomFit={hook.zoomFit}
|
||||
resetZoom={hook.resetZoom}
|
||||
showZoomControls={true}
|
||||
/>
|
||||
<svg className="parent" width="700" height="500">
|
||||
<g className="child"></g>
|
||||
</svg>
|
||||
</>
|
||||
);
|
||||
await waitFor(() => screen.getByRole('heading', { level: 2 }));
|
||||
expect(screen.getByRole('heading', { level: 2 })).toHaveTextContent(
|
||||
'Topology View'
|
||||
);
|
||||
fireEvent.click(screen.getByLabelText(/reset zoom/i));
|
||||
expect(hook.resetZoom).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user