@truedat/core 8.11.5 → 8.11.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,15 @@
1
- import { fireEvent, screen, waitFor } from "@testing-library/react";
1
+ import { act, fireEvent, screen, waitFor } from "@testing-library/react";
2
2
  import { render } from "@truedat/test/render";
3
3
  import { Graph, getMinZoomForDiagram } from "../Graph";
4
4
 
5
5
  const mockGetNodesBounds = jest.fn();
6
+ const mockGetFlowNodesBounds = jest.fn();
6
7
  const mockGetViewportForBounds = jest.fn();
8
+ const mockGetNodes = jest.fn().mockReturnValue([]);
9
+ const mockGetViewport = jest.fn().mockReturnValue({ x: 0, y: 0, zoom: 1 });
10
+ const mockFitView = jest.fn();
11
+ const mockSetCenter = jest.fn();
12
+ const mockSetViewport = jest.fn();
7
13
 
8
14
  jest.mock("elkjs/lib/elk.bundled.js", () =>
9
15
  jest.fn().mockImplementation(() => ({
@@ -12,15 +18,25 @@ jest.mock("elkjs/lib/elk.bundled.js", () =>
12
18
  );
13
19
 
14
20
  jest.mock("@xyflow/react", () => ({
15
- ReactFlow: ({ children }) => <div data-testid="react-flow">{children}</div>,
21
+ ReactFlow: ({ children, onInit }) => {
22
+ const React = require("react");
23
+
24
+ React.useEffect(() => onInit?.(), [onInit]);
25
+
26
+ return <div data-testid="react-flow">{children}</div>;
27
+ },
16
28
  ReactFlowProvider: ({ children }) => <>{children}</>,
17
29
  useNodesState: (initialNodes) => [initialNodes, jest.fn(), jest.fn()],
18
30
  useEdgesState: (initialEdges) => [initialEdges, jest.fn(), jest.fn()],
19
31
  useReactFlow: () => ({
20
- fitView: jest.fn(),
32
+ fitView: mockFitView,
21
33
  zoomIn: jest.fn(),
22
34
  zoomOut: jest.fn(),
23
- getNodes: jest.fn().mockReturnValue([]),
35
+ getNodes: mockGetNodes,
36
+ getNodesBounds: mockGetFlowNodesBounds,
37
+ getViewport: mockGetViewport,
38
+ setCenter: mockSetCenter,
39
+ setViewport: mockSetViewport,
24
40
  }),
25
41
  Position: {
26
42
  Right: "right",
@@ -31,7 +47,15 @@ jest.mock("@xyflow/react", () => ({
31
47
  MarkerType: {
32
48
  ArrowClosed: "arrowclosed",
33
49
  },
34
- Controls: ({ children }) => <div>{children}</div>,
50
+ Controls: ({ children, className, orientation }) => (
51
+ <div
52
+ className={className}
53
+ data-orientation={orientation}
54
+ data-testid="graph-controls"
55
+ >
56
+ {children}
57
+ </div>
58
+ ),
35
59
  ControlButton: ({ children, ...props }) => (
36
60
  <button type="button" {...props}>
37
61
  {children}
@@ -57,39 +81,186 @@ describe("<Graph />", () => {
57
81
 
58
82
  beforeEach(() => {
59
83
  mockGetNodesBounds.mockReturnValue({ x: 0, y: 0, width: 100, height: 50 });
84
+ mockGetFlowNodesBounds.mockReturnValue({
85
+ x: 0,
86
+ y: 0,
87
+ width: 100,
88
+ height: 50,
89
+ });
60
90
  mockGetViewportForBounds.mockReturnValue({ x: 0, y: 0, zoom: 0.75 });
91
+ mockGetNodes.mockReturnValue([]);
92
+ mockGetViewport.mockReturnValue({ x: 0, y: 0, zoom: 1 });
93
+ mockFitView.mockClear();
94
+ mockSetCenter.mockClear();
95
+ mockSetViewport.mockClear();
61
96
  });
62
97
 
63
98
  afterEach(() => {
64
99
  mockGetNodesBounds.mockReset();
100
+ mockGetFlowNodesBounds.mockReset();
65
101
  mockGetViewportForBounds.mockReset();
102
+ mockFitView.mockReset();
103
+ mockSetCenter.mockReset();
104
+ mockSetViewport.mockReset();
66
105
  });
67
106
 
68
- it("uses the fit view zoom as the minimum zoom level", () => {
69
- expect(getMinZoomForDiagram(props.nodes, 600, 300)).toBe(0.75);
107
+ it("keeps the configured floor when the full diagram fits above it", () => {
108
+ expect(getMinZoomForDiagram(props.nodes, 600, 300)).toBe(0.03);
70
109
  expect(mockGetViewportForBounds).toHaveBeenCalledWith(
71
110
  { x: 0, y: 0, width: 100, height: 50 },
72
111
  600,
73
112
  300,
74
- 0.01,
75
- 0.8,
76
- 0.2,
113
+ 0.03,
114
+ 2,
115
+ 0.15,
77
116
  );
78
117
  });
79
118
 
119
+ it("accepts instance-aware bounds for graphs with subflows", () => {
120
+ expect(
121
+ getMinZoomForDiagram(
122
+ props.nodes,
123
+ 600,
124
+ 300,
125
+ 0.15,
126
+ 0.03,
127
+ 2,
128
+ mockGetFlowNodesBounds,
129
+ ),
130
+ ).toBe(0.03);
131
+ expect(mockGetFlowNodesBounds).toHaveBeenCalledWith(props.nodes);
132
+ });
133
+
80
134
  it("opens an expanded modal from the controls bar", async () => {
81
135
  render(<Graph {...props} />);
82
136
 
83
- expect(screen.queryByText("graph.expanded.header")).not.toBeInTheDocument();
137
+ expect(screen.getAllByTestId("react-flow")).toHaveLength(1);
84
138
 
85
139
  fireEvent.click(screen.getByLabelText("graph.controls.open_expanded"));
86
140
 
87
141
  await waitFor(() => {
88
- expect(screen.getByText("graph.expanded.header")).toBeInTheDocument();
89
142
  expect(screen.getAllByTestId("react-flow")).toHaveLength(2);
143
+ screen
144
+ .getAllByTestId("graph-controls")
145
+ .forEach((controls) =>
146
+ expect(controls).toHaveAttribute("data-orientation", "horizontal"),
147
+ );
90
148
  expect(
91
149
  screen.getAllByLabelText("graph.controls.open_expanded"),
92
150
  ).toHaveLength(1);
93
151
  });
94
152
  });
153
+
154
+ it("uses horizontal controls by default and keeps vertical controls in fullscreen", async () => {
155
+ render(
156
+ <Graph {...props} graphOptions={{ controlsOrientation: "vertical" }} />,
157
+ );
158
+
159
+ expect(screen.getByTestId("graph-controls")).toHaveAttribute(
160
+ "data-orientation",
161
+ "vertical",
162
+ );
163
+
164
+ fireEvent.click(screen.getByLabelText("graph.controls.open_expanded"));
165
+
166
+ await waitFor(() => {
167
+ expect(screen.getAllByTestId("graph-controls")).toHaveLength(2);
168
+ screen
169
+ .getAllByTestId("graph-controls")
170
+ .forEach((controls) =>
171
+ expect(controls).toHaveAttribute("data-orientation", "vertical"),
172
+ );
173
+ });
174
+ });
175
+
176
+ it("centers the same node again when its focus request key changes", async () => {
177
+ const focusNode = {
178
+ ...props.nodes[0],
179
+ width: 120,
180
+ height: 60,
181
+ position: { x: 80, y: 40 },
182
+ };
183
+
184
+ mockGetNodes.mockReturnValue([focusNode]);
185
+ const { rerender } = render(
186
+ <Graph
187
+ {...props}
188
+ graphOptions={{
189
+ focusNodeId: focusNode.id,
190
+ focusNodeRequestKey: 1,
191
+ layoutMode: "fixed",
192
+ }}
193
+ />,
194
+ );
195
+
196
+ await act(async () => {
197
+ jest.advanceTimersByTime(50);
198
+ });
199
+ const initialFocusCalls = mockSetCenter.mock.calls.length;
200
+
201
+ expect(initialFocusCalls).toBeGreaterThan(0);
202
+
203
+ rerender(
204
+ <Graph
205
+ {...props}
206
+ graphOptions={{
207
+ focusNodeId: focusNode.id,
208
+ focusNodeRequestKey: 2,
209
+ layoutMode: "fixed",
210
+ }}
211
+ />,
212
+ );
213
+ await act(async () => {
214
+ jest.advanceTimersByTime(50);
215
+ });
216
+
217
+ expect(mockSetCenter.mock.calls.length).toBeGreaterThan(initialFocusCalls);
218
+ expect(mockSetCenter).toHaveBeenLastCalledWith(140, 70, {
219
+ duration: 300,
220
+ zoom: expect.any(Number),
221
+ });
222
+ });
223
+
224
+ it("preserves x, y and zoom when a fixed layout changes internally", async () => {
225
+ const viewport = { x: -240, y: 96, zoom: 0.72 };
226
+
227
+ mockGetViewport.mockReturnValue(viewport);
228
+ const { rerender } = render(
229
+ <Graph
230
+ {...props}
231
+ allowExpandedView={false}
232
+ graphOptions={{
233
+ layoutMode: "fixed",
234
+ preserveUserViewportOnLayoutChange: true,
235
+ }}
236
+ />,
237
+ );
238
+
239
+ await act(async () => {
240
+ jest.advanceTimersByTime(50);
241
+ });
242
+ mockFitView.mockClear();
243
+ mockSetCenter.mockClear();
244
+ mockSetViewport.mockClear();
245
+
246
+ rerender(
247
+ <Graph
248
+ {...props}
249
+ allowExpandedView={false}
250
+ nodes={props.nodes.map((node) => ({ ...node, height: 240 }))}
251
+ graphOptions={{
252
+ layoutMode: "fixed",
253
+ preserveUserViewportOnLayoutChange: true,
254
+ }}
255
+ />,
256
+ );
257
+ await act(async () => {
258
+ jest.advanceTimersByTime(50);
259
+ });
260
+
261
+ expect(mockGetViewport()).toEqual(viewport);
262
+ expect(mockFitView).not.toHaveBeenCalled();
263
+ expect(mockSetCenter).not.toHaveBeenCalled();
264
+ expect(mockSetViewport).not.toHaveBeenCalled();
265
+ });
95
266
  });
@@ -0,0 +1,45 @@
1
+ import { fireEvent } from "@testing-library/react";
2
+ import { render } from "@truedat/test/render";
3
+ import ColoredEdge from "@truedat/core/components/graph/ColoredEdge";
4
+ import { redesignedLineageEdges } from "@truedat/dd/components/LineageGraph";
5
+
6
+ jest.mock("@xyflow/react", () => ({
7
+ BaseEdge: ({ className, id, path, style }) => (
8
+ <path className={className} d={path} id={id} style={style} />
9
+ ),
10
+ EdgeLabelRenderer: ({ children }) => children,
11
+ getBezierPath: () => ["M 0,0 L 400,0", 200, 0],
12
+ }));
13
+
14
+ it("lets the rendered edge respond to hover after lineage style normalization", () => {
15
+ const [edge] = redesignedLineageEdges([
16
+ {
17
+ id: "edge",
18
+ source: "a",
19
+ target: "b",
20
+ style: { stroke: "#b0b8c8", strokeWidth: 2, strokeOpacity: 0.95 },
21
+ data: {},
22
+ },
23
+ ]);
24
+ const { container } = render(
25
+ <svg>
26
+ <ColoredEdge
27
+ {...edge}
28
+ sourceX={0}
29
+ sourceY={0}
30
+ targetX={400}
31
+ targetY={0}
32
+ sourcePosition="right"
33
+ targetPosition="left"
34
+ />
35
+ </svg>,
36
+ );
37
+ const path = container.querySelector(".td-graph-edge");
38
+ expect(path.style.strokeWidth).toBe("");
39
+ expect(path.style.strokeOpacity).toBe("");
40
+ expect(path.style.stroke).toContain("var(--td-graph-edge-stroke");
41
+ fireEvent.mouseEnter(container.querySelector(".td-graph-edge-hit-area"));
42
+ expect(path).toHaveClass("td-graph-edge--hovered");
43
+ fireEvent.mouseLeave(container.querySelector(".td-graph-edge-hit-area"));
44
+ expect(path).not.toHaveClass("td-graph-edge--hovered");
45
+ });
@@ -0,0 +1,308 @@
1
+ import { fireEvent } from "@testing-library/react";
2
+ import { render } from "@truedat/test/render";
3
+ import LineageGroupNode from "../graph/LineageGroupNode";
4
+ import { EdgeHoverContext } from "../graph/ColoredEdge";
5
+ import { lineageNodeIcon } from "../graph/lineageNodeIcon";
6
+
7
+ jest.mock("@xyflow/react", () => ({
8
+ // eslint-disable-next-line react/prop-types
9
+ Handle: ({ id }) => <span data-handle-id={id} />,
10
+ Position: {
11
+ Bottom: "bottom",
12
+ Left: "left",
13
+ Right: "right",
14
+ Top: "top",
15
+ },
16
+ }));
17
+
18
+ describe("<LineageGroupNode />", () => {
19
+ it("renders the redesigned system hierarchy header", () => {
20
+ const rendered = render(
21
+ <LineageGroupNode
22
+ id="system-1"
23
+ data={{
24
+ designVariant: "redesigned",
25
+ label: "Teradata",
26
+ systemTypeLabel: "Warehouse",
27
+ variant: "absolute",
28
+ z: 1,
29
+ }}
30
+ />,
31
+ );
32
+
33
+ expect(rendered.getByText("Teradata")).toBeInTheDocument();
34
+ expect(rendered.getByText("Warehouse")).toBeInTheDocument();
35
+ expect(
36
+ rendered.container.querySelector(
37
+ ".td-lineage-group-node--design-redesigned",
38
+ ),
39
+ ).toBeInTheDocument();
40
+ expect(
41
+ rendered.container.querySelector(
42
+ ".td-lineage-group-node__system-indicator",
43
+ ),
44
+ ).not.toBeInTheDocument();
45
+ expect(
46
+ rendered.container.querySelectorAll("[data-handle-id]"),
47
+ ).toHaveLength(4);
48
+ expect(
49
+ rendered.container.querySelector(
50
+ ".td-lineage-group-node__type-icon.server.icon",
51
+ ),
52
+ ).toBeInTheDocument();
53
+ });
54
+
55
+ it("keeps collapse controls semantic and keyboard reachable", () => {
56
+ const onToggleCollapse = jest.fn();
57
+ const rendered = render(
58
+ <LineageGroupNode
59
+ id="database-1"
60
+ data={{
61
+ canCollapse: true,
62
+ collapsed: false,
63
+ designVariant: "redesigned",
64
+ label: "Analytics",
65
+ onToggleCollapse,
66
+ variant: "absolute",
67
+ z: 2,
68
+ }}
69
+ />,
70
+ );
71
+
72
+ expect(
73
+ rendered.getByRole("button", { name: "Collapse group" }),
74
+ ).toBeInTheDocument();
75
+ expect(
76
+ rendered.container.querySelector(
77
+ ".td-lineage-group-node__type-icon.database.icon",
78
+ ),
79
+ ).toBeInTheDocument();
80
+ });
81
+
82
+ it("omits a redundant single-node summary when the leaf group is open", () => {
83
+ const rendered = render(
84
+ <LineageGroupNode
85
+ id="open-leaf-group"
86
+ data={{
87
+ collapsed: false,
88
+ designVariant: "redesigned",
89
+ groupCount: 0,
90
+ label: "Leaf group",
91
+ nodeCount: 1,
92
+ variant: "absolute",
93
+ }}
94
+ />,
95
+ );
96
+
97
+ expect(rendered.queryByText("1 nodo")).not.toBeInTheDocument();
98
+ expect(
99
+ rendered.container.querySelector(".td-lineage-group-node__footer"),
100
+ ).not.toBeInTheDocument();
101
+ });
102
+
103
+ it("keeps the single-node summary while the leaf group is collapsed", () => {
104
+ const rendered = render(
105
+ <LineageGroupNode
106
+ id="collapsed-leaf-group"
107
+ data={{
108
+ collapsed: true,
109
+ designVariant: "redesigned",
110
+ groupCount: 0,
111
+ label: "Leaf group",
112
+ nodeCount: 1,
113
+ variant: "absolute",
114
+ }}
115
+ />,
116
+ );
117
+
118
+ expect(rendered.getByText("1 nodo")).toBeInTheDocument();
119
+ });
120
+
121
+ it("retains an explicit classic modifier", () => {
122
+ const rendered = render(
123
+ <LineageGroupNode
124
+ id="legacy-group"
125
+ data={{
126
+ designVariant: "classic",
127
+ label: "Legacy",
128
+ variant: "absolute",
129
+ }}
130
+ />,
131
+ );
132
+
133
+ expect(
134
+ rendered.container.querySelector(
135
+ ".td-lineage-group-node--design-classic",
136
+ ),
137
+ ).toBeInTheDocument();
138
+ });
139
+
140
+ it("keeps the active modifier used by the selected emphasis state", () => {
141
+ const rendered = render(
142
+ <LineageGroupNode
143
+ id="selected-group"
144
+ data={{
145
+ designVariant: "redesigned",
146
+ isActive: true,
147
+ label: "Selected group",
148
+ variant: "absolute",
149
+ }}
150
+ />,
151
+ );
152
+
153
+ expect(
154
+ rendered.container.querySelector(
155
+ ".td-lineage-group-node--design-redesigned.td-lineage-group-node--active",
156
+ ),
157
+ ).toBeInTheDocument();
158
+ });
159
+
160
+ it("marks the group under the pointer independently from related nodes", () => {
161
+ const rendered = render(
162
+ <EdgeHoverContext.Provider value={{ hoveredNodeId: "hovered-group" }}>
163
+ <LineageGroupNode
164
+ id="hovered-group"
165
+ data={{
166
+ designVariant: "redesigned",
167
+ label: "Hovered group",
168
+ variant: "absolute",
169
+ }}
170
+ />
171
+ </EdgeHoverContext.Provider>,
172
+ );
173
+
174
+ expect(
175
+ rendered.container.querySelector(
176
+ ".td-lineage-group-node--hovered:not(.td-lineage-group-node--connected)",
177
+ ),
178
+ ).toBeInTheDocument();
179
+ });
180
+
181
+ it("renders the oversized warning badge only for oversized groups", () => {
182
+ const rendered = render(
183
+ <LineageGroupNode
184
+ id="oversized-group"
185
+ data={{
186
+ designVariant: "redesigned",
187
+ label: "Oversized group",
188
+ oversized: true,
189
+ variant: "absolute",
190
+ }}
191
+ />,
192
+ );
193
+
194
+ expect(
195
+ rendered.container.querySelector(
196
+ ".td-lineage-group-node__oversized-warning",
197
+ ),
198
+ ).toBeInTheDocument();
199
+ });
200
+
201
+ it("renders the oversized warning title band and message", () => {
202
+ const rendered = render(
203
+ <LineageGroupNode
204
+ id="oversized-group"
205
+ data={{
206
+ designVariant: "redesigned",
207
+ label: "Oversized group",
208
+ oversized: true,
209
+ oversizedWarningOpen: true,
210
+ variant: "absolute",
211
+ }}
212
+ />,
213
+ );
214
+
215
+ expect(rendered.getByText("Warning")).toBeInTheDocument();
216
+ expect(rendered.getByText(/more than 20 nodes/)).toBeInTheDocument();
217
+ });
218
+
219
+ it("omits the oversized badge for regular groups", () => {
220
+ const rendered = render(
221
+ <LineageGroupNode
222
+ id="regular-group"
223
+ data={{
224
+ designVariant: "redesigned",
225
+ label: "Regular group",
226
+ variant: "absolute",
227
+ }}
228
+ />,
229
+ );
230
+
231
+ expect(
232
+ rendered.container.querySelector(
233
+ ".td-lineage-group-node__oversized-warning",
234
+ ),
235
+ ).not.toBeInTheDocument();
236
+ });
237
+
238
+ it("offers the Open link when the oversized warning was triggered", () => {
239
+ const onConfirmOversizedExpand = jest.fn();
240
+ const rendered = render(
241
+ <LineageGroupNode
242
+ id="blocked-group"
243
+ data={{
244
+ designVariant: "redesigned",
245
+ label: "Blocked group",
246
+ onConfirmOversizedExpand,
247
+ oversized: true,
248
+ oversizedWarningOpen: true,
249
+ variant: "absolute",
250
+ }}
251
+ />,
252
+ );
253
+
254
+ const openButton = rendered.getByRole("button", { name: "Open" });
255
+ expect(openButton).toBeInTheDocument();
256
+ fireEvent.click(openButton);
257
+ expect(onConfirmOversizedExpand).toHaveBeenCalled();
258
+ });
259
+ });
260
+
261
+ describe("lineageNodeIcon", () => {
262
+ it.each([
263
+ [{ kind: "resource" }, "table"],
264
+ [{ kind: "process" }, "random"],
265
+ [{ kind: "group" }, "database"],
266
+ [{ kind: "group", z: 1 }, "server"],
267
+ [{ kind: "group", z: 2 }, "database"],
268
+ [{ kind: "group", resourceContainer: true, z: 1 }, "table"],
269
+ ])("resolves %p as %s", (node, icon) => {
270
+ expect(lineageNodeIcon(node)).toBe(icon);
271
+ });
272
+ });
273
+
274
+ it.each(["direct", "contained"])(
275
+ "distinguishes %s query origins in collapsed groups",
276
+ (queryOrigin) => {
277
+ const rendered = render(
278
+ <EdgeHoverContext.Provider value={{ hoveredNodeId: "group" }}>
279
+ <LineageGroupNode
280
+ id="group"
281
+ data={{
282
+ label: "Warehouse",
283
+ variant: "absolute",
284
+ designVariant: "redesigned",
285
+ collapsed: true,
286
+ isActive: true,
287
+ queryOrigin,
288
+ }}
289
+ />
290
+ </EdgeHoverContext.Provider>,
291
+ { locale: "es" },
292
+ );
293
+ expect(
294
+ rendered.getByText(
295
+ queryOrigin === "direct"
296
+ ? "Origen de consulta"
297
+ : "Contiene origen de consulta",
298
+ ),
299
+ ).toBeInTheDocument();
300
+ expect(
301
+ rendered.container.querySelector(".td-lineage-group-node"),
302
+ ).toHaveClass(
303
+ "td-lineage-group-node--query-origin",
304
+ "td-lineage-group-node--active",
305
+ "td-lineage-group-node--hovered",
306
+ );
307
+ },
308
+ );
@@ -0,0 +1,23 @@
1
+ // Jest Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`<ConceptNode /> matches the snapshot 1`] = `
4
+ <div>
5
+ <div
6
+ class="td-concept-node td-concept-node--active"
7
+ >
8
+ <span
9
+ data-position="left"
10
+ data-testid="handle-target"
11
+ />
12
+ <span
13
+ class="td-concept-node__label"
14
+ >
15
+ Snapshot
16
+ </span>
17
+ <span
18
+ data-position="right"
19
+ data-testid="handle-source"
20
+ />
21
+ </div>
22
+ </div>
23
+ `;