123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260 |
- import { render, screen, fireEvent, waitFor, getByText } from '@testing-library/react';
- import userEvent from '@testing-library/user-event';
- import React from 'react';
- import { NodeGraph } from './NodeGraph';
- import { makeEdgesDataFrame, makeNodesDataFrame } from './utils';
- jest.mock('react-use/lib/useMeasure', () => {
- return {
- __esModule: true,
- default: () => {
- return [() => {}, { width: 500, height: 200 }];
- },
- };
- });
- describe('NodeGraph', () => {
- const origError = console.error;
- const consoleErrorMock = jest.fn();
- afterEach(() => (console.error = origError));
- beforeEach(() => (console.error = consoleErrorMock));
- it('shows no data message without any data', async () => {
- render(<NodeGraph dataFrames={[]} getLinks={() => []} />);
- await screen.findByText('No data');
- });
- it('can zoom in and out', async () => {
- render(<NodeGraph dataFrames={[makeNodesDataFrame(2), makeEdgesDataFrame([[0, 1]])]} getLinks={() => []} />);
- const zoomIn = await screen.findByTitle(/Zoom in/);
- const zoomOut = await screen.findByTitle(/Zoom out/);
- expect(getScale()).toBe(1);
- await userEvent.click(zoomIn);
- expect(getScale()).toBe(1.5);
- await userEvent.click(zoomOut);
- expect(getScale()).toBe(1);
- });
- it('can pan the graph', async () => {
- render(
- <NodeGraph
- dataFrames={[
- makeNodesDataFrame(3),
- makeEdgesDataFrame([
- [0, 1],
- [1, 2],
- ]),
- ]}
- getLinks={() => []}
- />
- );
- await screen.findByLabelText('Node: service:1');
- panView({ x: 10, y: 10 });
- // Though we try to pan down 10px we are rendering in straight line 3 nodes so there are bounds preventing
- // as panning vertically
- await waitFor(() => expect(getTranslate()).toEqual({ x: 10, y: 0 }));
- });
- it('renders with single node', async () => {
- render(<NodeGraph dataFrames={[makeNodesDataFrame(1)]} getLinks={() => []} />);
- const circle = await screen.findByText('', { selector: 'circle' });
- await screen.findByText(/service:0/);
- expect(getXY(circle)).toEqual({ x: 0, y: 0 });
- });
- it('shows context menu when clicking on node or edge', async () => {
- render(
- <NodeGraph
- dataFrames={[makeNodesDataFrame(2), makeEdgesDataFrame([[0, 1]])]}
- getLinks={(dataFrame) => {
- return [
- {
- title: dataFrame.fields.find((f) => f.name === 'source') ? 'Edge traces' : 'Node traces',
- href: '',
- origin: null,
- target: '_self',
- },
- ];
- }}
- />
- );
- const node = await screen.findByLabelText(/Node: service:0/);
- await userEvent.click(node);
- await screen.findByText(/Node traces/);
- const edge = await screen.findByLabelText(/Edge from/);
- await userEvent.click(edge);
- await screen.findByText(/Edge traces/);
- });
- it('lays out 3 nodes in single line', async () => {
- render(
- <NodeGraph
- dataFrames={[
- makeNodesDataFrame(3),
- makeEdgesDataFrame([
- [0, 1],
- [1, 2],
- ]),
- ]}
- getLinks={() => []}
- />
- );
- await expectNodePositionCloseTo('service:0', { x: -221, y: 0 });
- await expectNodePositionCloseTo('service:1', { x: -21, y: 0 });
- await expectNodePositionCloseTo('service:2', { x: 221, y: 0 });
- });
- it('lays out first children on one vertical line', async () => {
- render(
- <NodeGraph
- dataFrames={[
- makeNodesDataFrame(3),
- makeEdgesDataFrame([
- [0, 1],
- [0, 2],
- ]),
- ]}
- getLinks={() => []}
- />
- );
- // Should basically look like <
- await expectNodePositionCloseTo('service:0', { x: -100, y: 0 });
- await expectNodePositionCloseTo('service:1', { x: 100, y: -100 });
- await expectNodePositionCloseTo('service:2', { x: 100, y: 100 });
- });
- it('limits the number of nodes shown and shows a warning', async () => {
- render(
- <NodeGraph
- dataFrames={[
- makeNodesDataFrame(5),
- makeEdgesDataFrame([
- [0, 1],
- [0, 2],
- [2, 3],
- [3, 4],
- ]),
- ]}
- getLinks={() => []}
- nodeLimit={2}
- />
- );
- const nodes = await screen.findAllByLabelText(/Node: service:\d/);
- expect(nodes.length).toBe(2);
- screen.getByLabelText(/Nodes hidden warning/);
- const markers = await screen.findAllByLabelText(/Hidden nodes marker: \d/);
- expect(markers.length).toBe(1);
- });
- it('allows expanding the nodes when limiting visible nodes', async () => {
- render(
- <NodeGraph
- dataFrames={[
- makeNodesDataFrame(5),
- makeEdgesDataFrame([
- [0, 1],
- [1, 2],
- [2, 3],
- [3, 4],
- ]),
- ]}
- getLinks={() => []}
- nodeLimit={3}
- />
- );
- const node = await screen.findByLabelText(/Node: service:0/);
- expect(node).toBeInTheDocument();
- const marker = await screen.findByLabelText(/Hidden nodes marker: 3/);
- await userEvent.click(marker);
- expect(screen.queryByLabelText(/Node: service:0/)).not.toBeInTheDocument();
- expect(screen.getByLabelText(/Node: service:4/)).toBeInTheDocument();
- const nodes = await screen.findAllByLabelText(/Node: service:\d/);
- expect(nodes.length).toBe(3);
- });
- it('can switch to grid layout', async () => {
- render(
- <NodeGraph
- dataFrames={[
- makeNodesDataFrame(3),
- makeEdgesDataFrame([
- [0, 1],
- [1, 2],
- ]),
- ]}
- getLinks={() => []}
- nodeLimit={3}
- />
- );
- const button = await screen.findByTitle(/Grid layout/);
- await userEvent.click(button);
- await expectNodePositionCloseTo('service:0', { x: -60, y: -60 });
- await expectNodePositionCloseTo('service:1', { x: 60, y: -60 });
- await expectNodePositionCloseTo('service:2', { x: -60, y: 80 });
- });
- });
- async function expectNodePositionCloseTo(node: string, pos: { x: number; y: number }) {
- const nodePos = await getNodeXY(node);
- expect(nodePos.x).toBeCloseTo(pos.x, -1);
- expect(nodePos.y).toBeCloseTo(pos.y, -1);
- }
- async function getNodeXY(node: string) {
- const group = await screen.findByLabelText(new RegExp(`Node: ${node}`));
- const circle = getByText(group, '', { selector: 'circle' });
- return getXY(circle);
- }
- function panView(toPos: { x: number; y: number }) {
- const svg = getSvg();
- fireEvent(svg, new MouseEvent('mousedown', { clientX: 0, clientY: 0 }));
- fireEvent(document, new MouseEvent('mousemove', { clientX: toPos.x, clientY: toPos.y }));
- fireEvent(document, new MouseEvent('mouseup'));
- }
- function getSvg() {
- return screen.getAllByText('', { selector: 'svg' })[0];
- }
- function getTransform() {
- const svg = getSvg();
- const group = svg.children[0] as SVGElement;
- return group.style.getPropertyValue('transform');
- }
- function getScale() {
- const scale = getTransform().match(/scale\(([\d\.]+)\)/)![1];
- return parseFloat(scale);
- }
- function getTranslate() {
- const matches = getTransform().match(/translate\((\d+)px, (\d+)px\)/);
- return {
- x: parseFloat(matches![1]),
- y: parseFloat(matches![2]),
- };
- }
- function getXY(e: Element) {
- return {
- x: parseFloat(e.attributes.getNamedItem('cx')?.value || ''),
- y: parseFloat(e.attributes.getNamedItem('cy')?.value || ''),
- };
- }
|