@truedat/dd 8.11.6 → 8.11.9
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.
- package/package.json +4 -3
- package/src/components/Graphs.js +30 -4
- package/src/components/LineageContextMenu.js +185 -58
- package/src/components/LineageDownloadButton.js +16 -6
- package/src/components/LineageEventsPanel.js +80 -27
- package/src/components/LineageEventsTable.js +7 -3
- package/src/components/LineageGraph.js +5166 -306
- package/src/components/LineageGraphToolbar.js +896 -0
- package/src/components/LineageInspectorPanel.js +748 -0
- package/src/components/LineageResourceNode.js +501 -0
- package/src/components/LineageRoutes.js +1 -2
- package/src/components/LineageSelectionPanel.js +28 -12
- package/src/components/StructureGraph.js +8 -1
- package/src/components/StructureGraphView.js +14 -0
- package/src/components/StructureImpact.js +2 -22
- package/src/components/StructureLineage.js +2 -19
- package/src/components/StructuresUploadEventsTable.js +0 -3
- package/src/components/__tests__/LineageContextMenu.spec.js +42 -0
- package/src/components/__tests__/LineageGraph.spec.js +896 -0
- package/src/components/__tests__/LineageGraphLayout.spec.js +639 -0
- package/src/components/__tests__/LineageGraphToolbar.spec.js +366 -0
- package/src/components/__tests__/LineageHierarchy.spec.js +116 -0
- package/src/components/__tests__/LineageInspectorPanel.spec.js +364 -0
- package/src/components/__tests__/LineageResourceNode.spec.js +338 -0
- package/src/components/__tests__/LineageRoutes.spec.js +27 -9
- package/src/components/__tests__/__snapshots__/LineageDownloadButton.spec.js.snap +23 -3
- package/src/components/__tests__/__snapshots__/LineageForm.spec.js.snap +23 -3
- package/src/components/__tests__/lineageEventHelpers.spec.js +60 -0
- package/src/components/lineageEventHelpers.js +53 -17
- package/src/reducers/__tests__/lineageEvents.spec.js +164 -0
- package/src/reducers/__tests__/lineageQuery.spec.js +11 -0
- package/src/reducers/lineageEvents.js +68 -2
- package/src/reducers/lineageQuery.js +2 -6
- package/src/styles/LineageGraph.less +4094 -0
- package/src/styles/LineageGraphColors.less +105 -0
- package/src/styles/LineageGraphCore.less +20 -0
- package/src/styles/LineageResourceNode.less +711 -0
- package/src/utils/__tests__/lineageGraphIndex.spec.js +52 -0
- package/src/utils/__tests__/lineageLayout.spec.js +252 -0
- package/src/utils/__tests__/lineageTopology.spec.js +256 -0
- package/src/utils/lineageGraphIndex.js +296 -0
- package/src/utils/lineageLayout.js +1050 -0
- package/src/styles/lineageGraph.less +0 -1236
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { fireEvent, screen } from "@testing-library/react";
|
|
2
|
+
import { render } from "@truedat/test/render";
|
|
3
|
+
import { LineageGraphToolbar, visibleLegendKeys } from "../LineageGraphToolbar";
|
|
4
|
+
|
|
5
|
+
const defaultProps = {
|
|
6
|
+
activePanel: null,
|
|
7
|
+
visibleLegend: ["deleted", "PK"],
|
|
8
|
+
collapsible: true,
|
|
9
|
+
context: {
|
|
10
|
+
type: "node",
|
|
11
|
+
node: {
|
|
12
|
+
graphNodeId: "orders",
|
|
13
|
+
id: "orders",
|
|
14
|
+
lineageKind: "resource",
|
|
15
|
+
iconName: "table",
|
|
16
|
+
name: "Orders",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
indicators: [{ icon: "key", key: "pk", label: "PK" }],
|
|
20
|
+
legendItems: [{ color: "#123456", key: "oracle", label: "Oracle" }],
|
|
21
|
+
onClosePanel: jest.fn(),
|
|
22
|
+
onClearContext: jest.fn(),
|
|
23
|
+
onCollapseAll: jest.fn(),
|
|
24
|
+
onExpandAll: jest.fn(),
|
|
25
|
+
onExpansionDepthChange: jest.fn(),
|
|
26
|
+
onMinimizeInspector: jest.fn(),
|
|
27
|
+
onPanelChange: jest.fn(),
|
|
28
|
+
onSearchInputChange: jest.fn(),
|
|
29
|
+
onSearchResultSelect: jest.fn(),
|
|
30
|
+
onSystemTypeFilterChange: jest.fn(),
|
|
31
|
+
onTogglePin: jest.fn(),
|
|
32
|
+
onUnpin: jest.fn(),
|
|
33
|
+
pinned: false,
|
|
34
|
+
relationTypes: ["depends_on"],
|
|
35
|
+
searchInput: "",
|
|
36
|
+
searchResults: [],
|
|
37
|
+
searchResultsTotal: 0,
|
|
38
|
+
selectedGraphNodeId: "orders",
|
|
39
|
+
systemTypeFilter: "",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const renderToolbar = (props = {}, state = { graph: { id: "graph-1" } }) =>
|
|
43
|
+
render(<LineageGraphToolbar {...defaultProps} {...props} />, { state });
|
|
44
|
+
|
|
45
|
+
describe("<LineageGraphToolbar />", () => {
|
|
46
|
+
beforeEach(() => jest.clearAllMocks());
|
|
47
|
+
|
|
48
|
+
it("opens the legend from the empty context slot", () => {
|
|
49
|
+
renderToolbar({ context: null });
|
|
50
|
+
const button = screen.getByRole("button", { name: "Lineage legend" });
|
|
51
|
+
expect(button).toBeEnabled();
|
|
52
|
+
expect(button).toHaveAttribute("aria-expanded", "false");
|
|
53
|
+
fireEvent.click(button);
|
|
54
|
+
expect(defaultProps.onPanelChange).toHaveBeenCalledWith("legend");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("shows the basic legend in simplified views and hides it for a selection", () => {
|
|
58
|
+
const rendered = renderToolbar({
|
|
59
|
+
context: null,
|
|
60
|
+
activePanel: "legend",
|
|
61
|
+
simplified: true,
|
|
62
|
+
});
|
|
63
|
+
expect(
|
|
64
|
+
screen.getByRole("region", { name: "Lineage legend" }),
|
|
65
|
+
).toBeInTheDocument();
|
|
66
|
+
expect(screen.getByText("PK")).toBeInTheDocument();
|
|
67
|
+
expect(
|
|
68
|
+
screen.getByText("Deleted structure · red dashed border"),
|
|
69
|
+
).toBeInTheDocument();
|
|
70
|
+
fireEvent.click(screen.getByRole("button", { name: "Close panel" }));
|
|
71
|
+
expect(defaultProps.onClosePanel).toHaveBeenCalled();
|
|
72
|
+
rendered.unmount();
|
|
73
|
+
renderToolbar({ activePanel: "legend", simplified: true });
|
|
74
|
+
expect(
|
|
75
|
+
screen.queryByRole("region", { name: "Lineage legend" }),
|
|
76
|
+
).not.toBeInTheDocument();
|
|
77
|
+
expect(screen.getByText("Orders")).toBeInTheDocument();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("keeps the requested dock order and exposes accessible tooltips", () => {
|
|
81
|
+
renderToolbar();
|
|
82
|
+
|
|
83
|
+
const buttonNames = screen
|
|
84
|
+
.getAllByRole("button")
|
|
85
|
+
.map((button) => button.getAttribute("aria-label"));
|
|
86
|
+
|
|
87
|
+
expect(buttonNames).toEqual([
|
|
88
|
+
"Clear selection for Orders",
|
|
89
|
+
"Search nodes",
|
|
90
|
+
"graph.actions.download.tooltip",
|
|
91
|
+
"Graph settings",
|
|
92
|
+
"Visible levels",
|
|
93
|
+
"Collapse all",
|
|
94
|
+
"Expand all",
|
|
95
|
+
]);
|
|
96
|
+
expect(
|
|
97
|
+
screen.getByRole("button", { name: "Search nodes" }),
|
|
98
|
+
).toHaveAttribute("title", "Search nodes");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("changes visible depth independently through the accessible selector", () => {
|
|
102
|
+
renderToolbar();
|
|
103
|
+
|
|
104
|
+
const trigger = screen.getByRole("button", { name: "Visible levels" });
|
|
105
|
+
|
|
106
|
+
expect(trigger).toHaveAttribute("aria-expanded", "false");
|
|
107
|
+
expect(trigger).toHaveAttribute("aria-haspopup", "dialog");
|
|
108
|
+
expect(trigger).toHaveTextContent("4");
|
|
109
|
+
expect(trigger.querySelector(".lucide-layers")).toBeInTheDocument();
|
|
110
|
+
expect(
|
|
111
|
+
trigger.closest(".lineage-graph__dock-tooltip-trigger"),
|
|
112
|
+
).not.toBeInTheDocument();
|
|
113
|
+
|
|
114
|
+
fireEvent.click(trigger);
|
|
115
|
+
|
|
116
|
+
expect(trigger).toHaveAttribute("aria-expanded", "true");
|
|
117
|
+
expect(trigger).toHaveClass("lineage-graph__dock-button--active");
|
|
118
|
+
expect(
|
|
119
|
+
screen.getByRole("dialog", { name: "Visible levels" }),
|
|
120
|
+
).toBeInTheDocument();
|
|
121
|
+
expect(document.querySelector(".lineage-graph__depth-popup")).toHaveClass(
|
|
122
|
+
"left",
|
|
123
|
+
"center",
|
|
124
|
+
);
|
|
125
|
+
expect(
|
|
126
|
+
document.querySelector(".lineage-graph__depth-popover-scale"),
|
|
127
|
+
).toHaveAttribute("aria-hidden", "true");
|
|
128
|
+
expect(
|
|
129
|
+
document.querySelector(".lineage-graph__depth-popover-value"),
|
|
130
|
+
).not.toBeInTheDocument();
|
|
131
|
+
|
|
132
|
+
const slider = screen.getByRole("slider", { name: "Visible levels" });
|
|
133
|
+
expect(slider).toHaveAttribute("min", "0");
|
|
134
|
+
fireEvent.change(slider, {
|
|
135
|
+
target: { value: "0" },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
expect(defaultProps.onExpansionDepthChange).toHaveBeenCalledWith("0");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("shows the selected context summary and clear action without a reopen button", () => {
|
|
142
|
+
const rendered = renderToolbar();
|
|
143
|
+
const summary = rendered.container.querySelector(
|
|
144
|
+
".lineage-graph__dock-context-summary",
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(summary).toHaveTextContent("Orders");
|
|
148
|
+
expect(summary).toHaveTextContent("Data structure");
|
|
149
|
+
expect(summary.querySelector(".lucide-table-2")).toBeInTheDocument();
|
|
150
|
+
expect(
|
|
151
|
+
summary
|
|
152
|
+
.querySelector(".lucide-table-2")
|
|
153
|
+
.closest(".lineage-graph__dock-context-summary-icon"),
|
|
154
|
+
).toBeInTheDocument();
|
|
155
|
+
expect(
|
|
156
|
+
rendered.container.querySelector(".lineage-graph__dock-context-maximize"),
|
|
157
|
+
).not.toBeInTheDocument();
|
|
158
|
+
|
|
159
|
+
fireEvent.click(
|
|
160
|
+
screen.getByRole("button", { name: "Clear selection for Orders" }),
|
|
161
|
+
);
|
|
162
|
+
expect(defaultProps.onClearContext).toHaveBeenCalledTimes(1);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("renders only the active panel and marks its trigger as active", () => {
|
|
166
|
+
renderToolbar({ activePanel: "search" });
|
|
167
|
+
|
|
168
|
+
expect(
|
|
169
|
+
screen.getByRole("textbox", { name: "Search graph nodes" }),
|
|
170
|
+
).toBeInTheDocument();
|
|
171
|
+
expect(screen.queryByText("Lineage legend")).not.toBeInTheDocument();
|
|
172
|
+
expect(
|
|
173
|
+
screen.queryByLabelText("Lineage inspector"),
|
|
174
|
+
).not.toBeInTheDocument();
|
|
175
|
+
expect(
|
|
176
|
+
screen.getByRole("button", { name: "Search nodes" }),
|
|
177
|
+
).toHaveAttribute("aria-expanded", "true");
|
|
178
|
+
expect(screen.getByRole("button", { name: "Search nodes" })).toHaveClass(
|
|
179
|
+
"lineage-graph__dock-button--active",
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("keeps the selected name in the minimized banner in structure views", () => {
|
|
184
|
+
const rendered = renderToolbar({
|
|
185
|
+
activePanel: "inspector",
|
|
186
|
+
simplified: true,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const contextBar = rendered.container.querySelector(
|
|
190
|
+
".lineage-graph__dock-context-bar",
|
|
191
|
+
);
|
|
192
|
+
expect(contextBar).toHaveTextContent("Orders");
|
|
193
|
+
expect(contextBar).not.toHaveClass(
|
|
194
|
+
"lineage-graph__dock-context-bar--expanded",
|
|
195
|
+
);
|
|
196
|
+
expect(
|
|
197
|
+
screen.queryByLabelText("Lineage inspector"),
|
|
198
|
+
).not.toBeInTheDocument();
|
|
199
|
+
fireEvent.click(screen.getByText("Orders"));
|
|
200
|
+
expect(defaultProps.onPanelChange).not.toHaveBeenCalled();
|
|
201
|
+
expect(
|
|
202
|
+
screen.queryByLabelText("Lineage inspector"),
|
|
203
|
+
).not.toBeInTheDocument();
|
|
204
|
+
fireEvent.click(
|
|
205
|
+
screen.getByRole("button", { name: "Clear selection for Orders" }),
|
|
206
|
+
);
|
|
207
|
+
expect(defaultProps.onClearContext).toHaveBeenCalledTimes(1);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("reuses and expands the minimized context bar with the inspector", () => {
|
|
211
|
+
const rendered = renderToolbar({ activePanel: "inspector" });
|
|
212
|
+
|
|
213
|
+
const clearButton = screen.getByRole("button", {
|
|
214
|
+
name: "Clear selection for Orders",
|
|
215
|
+
});
|
|
216
|
+
const contextBar = rendered.container.querySelector(
|
|
217
|
+
".lineage-graph__dock-context-bar",
|
|
218
|
+
);
|
|
219
|
+
const summary = rendered.container.querySelector(
|
|
220
|
+
".lineage-graph__dock-context-summary",
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
expect(contextBar).toHaveClass("lineage-graph__dock-context-bar--expanded");
|
|
224
|
+
expect(summary).toHaveTextContent("Orders");
|
|
225
|
+
expect(summary).toHaveTextContent("Data structure");
|
|
226
|
+
expect(clearButton.querySelector(".lucide-x")).toBeInTheDocument();
|
|
227
|
+
expect(contextBar.closest(".lineage-graph__dock-context")).toHaveClass(
|
|
228
|
+
"lineage-graph__dock-context--inspector-open",
|
|
229
|
+
);
|
|
230
|
+
expect(screen.getByLabelText("Lineage inspector")).toBeInTheDocument();
|
|
231
|
+
expect(screen.getByLabelText("Lineage inspector")).toHaveClass(
|
|
232
|
+
"lineage-inspector--body-only",
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
fireEvent.click(clearButton);
|
|
236
|
+
expect(defaultProps.onClearContext).toHaveBeenCalledTimes(1);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("renders the pinned minimized bar with unpin and accent state", () => {
|
|
240
|
+
renderToolbar({ pinned: true });
|
|
241
|
+
|
|
242
|
+
const unpin = screen.getByRole("button", { name: "Unpin focus" });
|
|
243
|
+
|
|
244
|
+
expect(unpin).toHaveAttribute("aria-pressed", "true");
|
|
245
|
+
expect(unpin.querySelector(".lucide-pin")).toBeInTheDocument();
|
|
246
|
+
expect(screen.getByRole("button", { name: "Search nodes" })).toBeDisabled();
|
|
247
|
+
|
|
248
|
+
fireEvent.click(unpin);
|
|
249
|
+
expect(defaultProps.onUnpin).toHaveBeenCalledTimes(1);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("forwards search, collapse and expand interactions", () => {
|
|
253
|
+
renderToolbar({ activePanel: "search" });
|
|
254
|
+
|
|
255
|
+
fireEvent.change(
|
|
256
|
+
screen.getByRole("textbox", { name: "Search graph nodes" }),
|
|
257
|
+
{
|
|
258
|
+
target: { value: "orders" },
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
fireEvent.click(screen.getByRole("button", { name: "Collapse all" }));
|
|
262
|
+
fireEvent.click(screen.getByRole("button", { name: "Expand all" }));
|
|
263
|
+
|
|
264
|
+
expect(defaultProps.onSearchInputChange).toHaveBeenCalledWith("orders");
|
|
265
|
+
expect(defaultProps.onCollapseAll).toHaveBeenCalledTimes(1);
|
|
266
|
+
expect(defaultProps.onExpandAll).toHaveBeenCalledTimes(1);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("preserves the disabled download state when there is no graph", () => {
|
|
270
|
+
renderToolbar({}, {});
|
|
271
|
+
|
|
272
|
+
expect(
|
|
273
|
+
screen.getByRole("button", { name: "graph.actions.download.popup" }),
|
|
274
|
+
).toBeDisabled();
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("keeps the context button visible without a selection summary", () => {
|
|
278
|
+
const rendered = renderToolbar({ context: null });
|
|
279
|
+
|
|
280
|
+
expect(
|
|
281
|
+
screen.getByRole("button", { name: "Lineage legend" }),
|
|
282
|
+
).toBeEnabled();
|
|
283
|
+
expect(
|
|
284
|
+
screen.getByRole("button", { name: "Lineage legend" }),
|
|
285
|
+
).toHaveAttribute("title", "Lineage legend");
|
|
286
|
+
expect(
|
|
287
|
+
rendered.container.querySelector(".lineage-graph__dock-context-summary"),
|
|
288
|
+
).not.toBeInTheDocument();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("toggles whether a double click expands or collapses nodes", () => {
|
|
292
|
+
const onDoubleClickExpandsNodesChange = jest.fn();
|
|
293
|
+
renderToolbar({ onDoubleClickExpandsNodesChange });
|
|
294
|
+
|
|
295
|
+
const settingsButton = screen.getByRole("button", {
|
|
296
|
+
name: "Graph settings",
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
expect(
|
|
300
|
+
settingsButton.querySelector(".lucide-settings"),
|
|
301
|
+
).toBeInTheDocument();
|
|
302
|
+
fireEvent.click(settingsButton);
|
|
303
|
+
|
|
304
|
+
const toggle = screen.getByRole("checkbox", {
|
|
305
|
+
name: "Double-click expands or collapses nodes",
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
expect(toggle).not.toBeChecked();
|
|
309
|
+
fireEvent.click(toggle);
|
|
310
|
+
expect(onDoubleClickExpandsNodesChange).toHaveBeenCalledWith(true);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe("visibleLegendKeys", () => {
|
|
315
|
+
it("omits unloaded, hidden and collapsed field indicators", () => {
|
|
316
|
+
const nodes = [
|
|
317
|
+
{
|
|
318
|
+
className:
|
|
319
|
+
"td-lineage-catalog--present td-lineage-group-node--entity-system",
|
|
320
|
+
data: { entityType: "system" },
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
hidden: true,
|
|
324
|
+
className: "td-lineage-catalog--deleted",
|
|
325
|
+
data: { fields: [{ isPK: true }] },
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
data: {
|
|
329
|
+
fields: [{ isCDE: true }, { isFK: true }],
|
|
330
|
+
fieldHandleLimit: 1,
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
];
|
|
334
|
+
expect(visibleLegendKeys(nodes, [{ data: { containment: true } }])).toEqual(
|
|
335
|
+
["system", "CDE"],
|
|
336
|
+
);
|
|
337
|
+
expect(visibleLegendKeys([], [])).toEqual([]);
|
|
338
|
+
});
|
|
339
|
+
it("includes only the loaded dependency direction and visible node states", () => {
|
|
340
|
+
expect(
|
|
341
|
+
visibleLegendKeys(
|
|
342
|
+
[
|
|
343
|
+
{ className: "td-lineage-catalog--absent" },
|
|
344
|
+
{
|
|
345
|
+
className: "td-lineage-catalog--deleted",
|
|
346
|
+
data: { fieldLike: true, lineageNode: { isPK: true } },
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
[{ id: "edge" }],
|
|
350
|
+
"lineage",
|
|
351
|
+
),
|
|
352
|
+
).toEqual(["absent", "deleted", "lineage", "PK"]);
|
|
353
|
+
});
|
|
354
|
+
it("does not render entries absent from the current graph", () => {
|
|
355
|
+
renderToolbar({
|
|
356
|
+
context: null,
|
|
357
|
+
activePanel: "legend",
|
|
358
|
+
visibleLegend: ["impact"],
|
|
359
|
+
});
|
|
360
|
+
expect(screen.getByText("Impact dependencies")).toBeInTheDocument();
|
|
361
|
+
expect(screen.queryByText("PK")).not.toBeInTheDocument();
|
|
362
|
+
expect(
|
|
363
|
+
screen.queryByText("Deleted structure · red dashed border"),
|
|
364
|
+
).not.toBeInTheDocument();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
lineagePositionedLayoutNodes,
|
|
3
|
+
preparePositionedLineageHierarchy,
|
|
4
|
+
} from "../LineageGraph";
|
|
5
|
+
|
|
6
|
+
const group = (id, x, y, w, h, z = 0) => ({ id, name: id, x, y, w, h, z });
|
|
7
|
+
const resource = (id, x, y, z = 2) => ({
|
|
8
|
+
...group(id, x, y, 120, 50, z),
|
|
9
|
+
fields: Array.from({ length: 9 }, (_, index) => ({ name: `field-${index}` })),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("prepared lineage hierarchy", () => {
|
|
13
|
+
it("keeps the rendered card width equal to its layout width", () => {
|
|
14
|
+
const groups = [group("database", 0, 0, 700, 600)];
|
|
15
|
+
const resources = [resource("table", 30, 80)];
|
|
16
|
+
[240, 288, 360].forEach((nodeWidth) => {
|
|
17
|
+
const nodes = lineagePositionedLayoutNodes({
|
|
18
|
+
groups,
|
|
19
|
+
resources,
|
|
20
|
+
nodeWidth,
|
|
21
|
+
});
|
|
22
|
+
const table = nodes.find(({ id }) => id === "table");
|
|
23
|
+
expect(table.style["--td-lineage-node-width"]).toBe(`${nodeWidth}px`);
|
|
24
|
+
expect(table.data.absoluteGeometry.w).toBe(nodeWidth);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("chooses the deepest and then smallest parent, preserving input order on ties", () => {
|
|
29
|
+
const outer = group("outer", 0, 0, 900, 900);
|
|
30
|
+
const wide = group("wide", 10, 10, 600, 600, 1);
|
|
31
|
+
const first = group("first", 20, 20, 300, 300, 1);
|
|
32
|
+
const tied = group("tied", 20, 20, 300, 300, 1);
|
|
33
|
+
const leaf = resource("leaf", 50, 70);
|
|
34
|
+
const prepared = preparePositionedLineageHierarchy(
|
|
35
|
+
[outer, wide, first, tied],
|
|
36
|
+
[leaf],
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(prepared.parentByNode.get(leaf)).toBe(first);
|
|
40
|
+
expect(prepared.childrenByParent.get(first)).toContain(leaf);
|
|
41
|
+
expect(prepared.parentByNode.get(first)).toBe(outer);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("keeps duplicate canonical instances in their geometric parent", () => {
|
|
45
|
+
const left = group("left", 0, 0, 500, 500);
|
|
46
|
+
const right = group("right", 1000, 0, 500, 500);
|
|
47
|
+
const first = resource("shared", 30, 80);
|
|
48
|
+
const second = resource("shared", 1030, 80);
|
|
49
|
+
const groups = [left, right];
|
|
50
|
+
const resources = [first, second];
|
|
51
|
+
const preparedHierarchy = preparePositionedLineageHierarchy(
|
|
52
|
+
groups,
|
|
53
|
+
resources,
|
|
54
|
+
);
|
|
55
|
+
const nodes = lineagePositionedLayoutNodes({
|
|
56
|
+
groups,
|
|
57
|
+
resources,
|
|
58
|
+
preparedHierarchy,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(preparedHierarchy.parentByNode.get(first)).toBe(left);
|
|
62
|
+
expect(preparedHierarchy.parentByNode.get(second)).toBe(right);
|
|
63
|
+
const instances = nodes.filter(
|
|
64
|
+
(node) => node.data.canonicalId === "shared",
|
|
65
|
+
);
|
|
66
|
+
expect(new Set(instances.map((node) => node.id)).size).toBe(2);
|
|
67
|
+
expect(new Set(instances.map((node) => node.parentId))).toEqual(
|
|
68
|
+
new Set(["left", "right"]),
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("preserves anchored containment and the fallback for partial geometry", () => {
|
|
73
|
+
const parent = group("parent", 0, 0, 400, 400);
|
|
74
|
+
const anchored = resource("anchored", 350, 100);
|
|
75
|
+
const partial = { id: "partial", name: "partial", x: 50, y: 150 };
|
|
76
|
+
const outside = resource("outside", 900, 900);
|
|
77
|
+
const invalidGroup = { id: "invalid", x: 0, y: 0 };
|
|
78
|
+
const prepared = preparePositionedLineageHierarchy(
|
|
79
|
+
[parent, invalidGroup],
|
|
80
|
+
[anchored, partial, outside],
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(prepared.parentByNode.get(anchored)).toBe(parent);
|
|
84
|
+
expect(prepared.parentByNode.get(partial)).toBe(parent);
|
|
85
|
+
expect(prepared.parentByNode.get(outside)).toBeUndefined();
|
|
86
|
+
expect([...prepared.parentByNode.values()]).not.toContain(invalidGroup);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("reuses the same hierarchy across field expansion without changing parents or counters", () => {
|
|
90
|
+
const groups = [group("database", 0, 0, 700, 600)];
|
|
91
|
+
const resources = [resource("table", 30, 80)];
|
|
92
|
+
const preparedHierarchy = preparePositionedLineageHierarchy(
|
|
93
|
+
groups,
|
|
94
|
+
resources,
|
|
95
|
+
);
|
|
96
|
+
const parentLookup = jest.spyOn(preparedHierarchy.parentByNode, "get");
|
|
97
|
+
const base = { groups, resources, preparedHierarchy };
|
|
98
|
+
const initial = lineagePositionedLayoutNodes(base);
|
|
99
|
+
const expanded = lineagePositionedLayoutNodes({
|
|
100
|
+
...base,
|
|
101
|
+
expandedFieldNodeIds: new Set(["table"]),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(parentLookup).toHaveBeenCalledWith(resources[0]);
|
|
105
|
+
expect(initial.map(({ id, parentId }) => [id, parentId])).toEqual(
|
|
106
|
+
expanded.map(({ id, parentId }) => [id, parentId]),
|
|
107
|
+
);
|
|
108
|
+
expect(expanded.find(({ id }) => id === "table").height).toBeGreaterThan(
|
|
109
|
+
initial.find(({ id }) => id === "table").height,
|
|
110
|
+
);
|
|
111
|
+
expect(expanded.find(({ id }) => id === "database").data.nodeCount).toBe(1);
|
|
112
|
+
expect(lineagePositionedLayoutNodes({ groups, resources })).toEqual(
|
|
113
|
+
initial,
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
});
|