NodeGraph.test.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import { render, screen, fireEvent, waitFor, getByText } from '@testing-library/react';
  2. import userEvent from '@testing-library/user-event';
  3. import React from 'react';
  4. import { NodeGraph } from './NodeGraph';
  5. import { makeEdgesDataFrame, makeNodesDataFrame } from './utils';
  6. jest.mock('react-use/lib/useMeasure', () => {
  7. return {
  8. __esModule: true,
  9. default: () => {
  10. return [() => {}, { width: 500, height: 200 }];
  11. },
  12. };
  13. });
  14. describe('NodeGraph', () => {
  15. const origError = console.error;
  16. const consoleErrorMock = jest.fn();
  17. afterEach(() => (console.error = origError));
  18. beforeEach(() => (console.error = consoleErrorMock));
  19. it('shows no data message without any data', async () => {
  20. render(<NodeGraph dataFrames={[]} getLinks={() => []} />);
  21. await screen.findByText('No data');
  22. });
  23. it('can zoom in and out', async () => {
  24. render(<NodeGraph dataFrames={[makeNodesDataFrame(2), makeEdgesDataFrame([[0, 1]])]} getLinks={() => []} />);
  25. const zoomIn = await screen.findByTitle(/Zoom in/);
  26. const zoomOut = await screen.findByTitle(/Zoom out/);
  27. expect(getScale()).toBe(1);
  28. await userEvent.click(zoomIn);
  29. expect(getScale()).toBe(1.5);
  30. await userEvent.click(zoomOut);
  31. expect(getScale()).toBe(1);
  32. });
  33. it('can pan the graph', async () => {
  34. render(
  35. <NodeGraph
  36. dataFrames={[
  37. makeNodesDataFrame(3),
  38. makeEdgesDataFrame([
  39. [0, 1],
  40. [1, 2],
  41. ]),
  42. ]}
  43. getLinks={() => []}
  44. />
  45. );
  46. await screen.findByLabelText('Node: service:1');
  47. panView({ x: 10, y: 10 });
  48. // Though we try to pan down 10px we are rendering in straight line 3 nodes so there are bounds preventing
  49. // as panning vertically
  50. await waitFor(() => expect(getTranslate()).toEqual({ x: 10, y: 0 }));
  51. });
  52. it('renders with single node', async () => {
  53. render(<NodeGraph dataFrames={[makeNodesDataFrame(1)]} getLinks={() => []} />);
  54. const circle = await screen.findByText('', { selector: 'circle' });
  55. await screen.findByText(/service:0/);
  56. expect(getXY(circle)).toEqual({ x: 0, y: 0 });
  57. });
  58. it('shows context menu when clicking on node or edge', async () => {
  59. render(
  60. <NodeGraph
  61. dataFrames={[makeNodesDataFrame(2), makeEdgesDataFrame([[0, 1]])]}
  62. getLinks={(dataFrame) => {
  63. return [
  64. {
  65. title: dataFrame.fields.find((f) => f.name === 'source') ? 'Edge traces' : 'Node traces',
  66. href: '',
  67. origin: null,
  68. target: '_self',
  69. },
  70. ];
  71. }}
  72. />
  73. );
  74. const node = await screen.findByLabelText(/Node: service:0/);
  75. await userEvent.click(node);
  76. await screen.findByText(/Node traces/);
  77. const edge = await screen.findByLabelText(/Edge from/);
  78. await userEvent.click(edge);
  79. await screen.findByText(/Edge traces/);
  80. });
  81. it('lays out 3 nodes in single line', async () => {
  82. render(
  83. <NodeGraph
  84. dataFrames={[
  85. makeNodesDataFrame(3),
  86. makeEdgesDataFrame([
  87. [0, 1],
  88. [1, 2],
  89. ]),
  90. ]}
  91. getLinks={() => []}
  92. />
  93. );
  94. await expectNodePositionCloseTo('service:0', { x: -221, y: 0 });
  95. await expectNodePositionCloseTo('service:1', { x: -21, y: 0 });
  96. await expectNodePositionCloseTo('service:2', { x: 221, y: 0 });
  97. });
  98. it('lays out first children on one vertical line', async () => {
  99. render(
  100. <NodeGraph
  101. dataFrames={[
  102. makeNodesDataFrame(3),
  103. makeEdgesDataFrame([
  104. [0, 1],
  105. [0, 2],
  106. ]),
  107. ]}
  108. getLinks={() => []}
  109. />
  110. );
  111. // Should basically look like <
  112. await expectNodePositionCloseTo('service:0', { x: -100, y: 0 });
  113. await expectNodePositionCloseTo('service:1', { x: 100, y: -100 });
  114. await expectNodePositionCloseTo('service:2', { x: 100, y: 100 });
  115. });
  116. it('limits the number of nodes shown and shows a warning', async () => {
  117. render(
  118. <NodeGraph
  119. dataFrames={[
  120. makeNodesDataFrame(5),
  121. makeEdgesDataFrame([
  122. [0, 1],
  123. [0, 2],
  124. [2, 3],
  125. [3, 4],
  126. ]),
  127. ]}
  128. getLinks={() => []}
  129. nodeLimit={2}
  130. />
  131. );
  132. const nodes = await screen.findAllByLabelText(/Node: service:\d/);
  133. expect(nodes.length).toBe(2);
  134. screen.getByLabelText(/Nodes hidden warning/);
  135. const markers = await screen.findAllByLabelText(/Hidden nodes marker: \d/);
  136. expect(markers.length).toBe(1);
  137. });
  138. it('allows expanding the nodes when limiting visible nodes', async () => {
  139. render(
  140. <NodeGraph
  141. dataFrames={[
  142. makeNodesDataFrame(5),
  143. makeEdgesDataFrame([
  144. [0, 1],
  145. [1, 2],
  146. [2, 3],
  147. [3, 4],
  148. ]),
  149. ]}
  150. getLinks={() => []}
  151. nodeLimit={3}
  152. />
  153. );
  154. const node = await screen.findByLabelText(/Node: service:0/);
  155. expect(node).toBeInTheDocument();
  156. const marker = await screen.findByLabelText(/Hidden nodes marker: 3/);
  157. await userEvent.click(marker);
  158. expect(screen.queryByLabelText(/Node: service:0/)).not.toBeInTheDocument();
  159. expect(screen.getByLabelText(/Node: service:4/)).toBeInTheDocument();
  160. const nodes = await screen.findAllByLabelText(/Node: service:\d/);
  161. expect(nodes.length).toBe(3);
  162. });
  163. it('can switch to grid layout', async () => {
  164. render(
  165. <NodeGraph
  166. dataFrames={[
  167. makeNodesDataFrame(3),
  168. makeEdgesDataFrame([
  169. [0, 1],
  170. [1, 2],
  171. ]),
  172. ]}
  173. getLinks={() => []}
  174. nodeLimit={3}
  175. />
  176. );
  177. const button = await screen.findByTitle(/Grid layout/);
  178. await userEvent.click(button);
  179. await expectNodePositionCloseTo('service:0', { x: -60, y: -60 });
  180. await expectNodePositionCloseTo('service:1', { x: 60, y: -60 });
  181. await expectNodePositionCloseTo('service:2', { x: -60, y: 80 });
  182. });
  183. });
  184. async function expectNodePositionCloseTo(node: string, pos: { x: number; y: number }) {
  185. const nodePos = await getNodeXY(node);
  186. expect(nodePos.x).toBeCloseTo(pos.x, -1);
  187. expect(nodePos.y).toBeCloseTo(pos.y, -1);
  188. }
  189. async function getNodeXY(node: string) {
  190. const group = await screen.findByLabelText(new RegExp(`Node: ${node}`));
  191. const circle = getByText(group, '', { selector: 'circle' });
  192. return getXY(circle);
  193. }
  194. function panView(toPos: { x: number; y: number }) {
  195. const svg = getSvg();
  196. fireEvent(svg, new MouseEvent('mousedown', { clientX: 0, clientY: 0 }));
  197. fireEvent(document, new MouseEvent('mousemove', { clientX: toPos.x, clientY: toPos.y }));
  198. fireEvent(document, new MouseEvent('mouseup'));
  199. }
  200. function getSvg() {
  201. return screen.getAllByText('', { selector: 'svg' })[0];
  202. }
  203. function getTransform() {
  204. const svg = getSvg();
  205. const group = svg.children[0] as SVGElement;
  206. return group.style.getPropertyValue('transform');
  207. }
  208. function getScale() {
  209. const scale = getTransform().match(/scale\(([\d\.]+)\)/)![1];
  210. return parseFloat(scale);
  211. }
  212. function getTranslate() {
  213. const matches = getTransform().match(/translate\((\d+)px, (\d+)px\)/);
  214. return {
  215. x: parseFloat(matches![1]),
  216. y: parseFloat(matches![2]),
  217. };
  218. }
  219. function getXY(e: Element) {
  220. return {
  221. x: parseFloat(e.attributes.getNamedItem('cx')?.value || ''),
  222. y: parseFloat(e.attributes.getNamedItem('cy')?.value || ''),
  223. };
  224. }