@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.
Files changed (43) hide show
  1. package/package.json +4 -3
  2. package/src/components/Graphs.js +30 -4
  3. package/src/components/LineageContextMenu.js +185 -58
  4. package/src/components/LineageDownloadButton.js +16 -6
  5. package/src/components/LineageEventsPanel.js +80 -27
  6. package/src/components/LineageEventsTable.js +7 -3
  7. package/src/components/LineageGraph.js +5166 -306
  8. package/src/components/LineageGraphToolbar.js +896 -0
  9. package/src/components/LineageInspectorPanel.js +748 -0
  10. package/src/components/LineageResourceNode.js +501 -0
  11. package/src/components/LineageRoutes.js +1 -2
  12. package/src/components/LineageSelectionPanel.js +28 -12
  13. package/src/components/StructureGraph.js +8 -1
  14. package/src/components/StructureGraphView.js +14 -0
  15. package/src/components/StructureImpact.js +2 -22
  16. package/src/components/StructureLineage.js +2 -19
  17. package/src/components/StructuresUploadEventsTable.js +0 -3
  18. package/src/components/__tests__/LineageContextMenu.spec.js +42 -0
  19. package/src/components/__tests__/LineageGraph.spec.js +896 -0
  20. package/src/components/__tests__/LineageGraphLayout.spec.js +639 -0
  21. package/src/components/__tests__/LineageGraphToolbar.spec.js +366 -0
  22. package/src/components/__tests__/LineageHierarchy.spec.js +116 -0
  23. package/src/components/__tests__/LineageInspectorPanel.spec.js +364 -0
  24. package/src/components/__tests__/LineageResourceNode.spec.js +338 -0
  25. package/src/components/__tests__/LineageRoutes.spec.js +27 -9
  26. package/src/components/__tests__/__snapshots__/LineageDownloadButton.spec.js.snap +23 -3
  27. package/src/components/__tests__/__snapshots__/LineageForm.spec.js.snap +23 -3
  28. package/src/components/__tests__/lineageEventHelpers.spec.js +60 -0
  29. package/src/components/lineageEventHelpers.js +53 -17
  30. package/src/reducers/__tests__/lineageEvents.spec.js +164 -0
  31. package/src/reducers/__tests__/lineageQuery.spec.js +11 -0
  32. package/src/reducers/lineageEvents.js +68 -2
  33. package/src/reducers/lineageQuery.js +2 -6
  34. package/src/styles/LineageGraph.less +4094 -0
  35. package/src/styles/LineageGraphColors.less +105 -0
  36. package/src/styles/LineageGraphCore.less +20 -0
  37. package/src/styles/LineageResourceNode.less +711 -0
  38. package/src/utils/__tests__/lineageGraphIndex.spec.js +52 -0
  39. package/src/utils/__tests__/lineageLayout.spec.js +252 -0
  40. package/src/utils/__tests__/lineageTopology.spec.js +256 -0
  41. package/src/utils/lineageGraphIndex.js +296 -0
  42. package/src/utils/lineageLayout.js +1050 -0
  43. package/src/styles/lineageGraph.less +0 -1236
@@ -0,0 +1,364 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { act, fireEvent, screen } from "@testing-library/react";
4
+ import { render } from "@truedat/test/render";
5
+ import {
6
+ formatMetadataValue,
7
+ HierarchyPath,
8
+ LineageInspectorPanel,
9
+ } from "../LineageInspectorPanel";
10
+
11
+ describe("<LineageInspectorPanel />", () => {
12
+ const createGraph = jest.fn();
13
+ const excludeNode = jest.fn();
14
+ const onClose = jest.fn();
15
+ const onCenterNode = jest.fn();
16
+ const onMinimize = jest.fn();
17
+ const onTogglePin = jest.fn();
18
+
19
+ beforeEach(() => {
20
+ jest.useFakeTimers();
21
+ jest.clearAllMocks();
22
+ });
23
+
24
+ afterEach(() => {
25
+ jest.useRealTimers();
26
+ });
27
+
28
+ it("shows real node facts, quality and conditional resource actions", () => {
29
+ render(
30
+ <LineageInspectorPanel
31
+ context={{
32
+ type: "node",
33
+ node: {
34
+ id: "resource-1",
35
+ graphNodeId: "resource-1",
36
+ lineageKind: "resource",
37
+ iconName: "table",
38
+ name: "Orders",
39
+ path: ["Warehouse", "Sales"],
40
+ quality: "88",
41
+ status: "published",
42
+ structure_id: "structure-1",
43
+ systemTypeLabel: "Oracle",
44
+ fieldsKnown: true,
45
+ totalFields: 12,
46
+ type: "Table",
47
+ },
48
+ }}
49
+ createGraph={createGraph}
50
+ excludeNode={excludeNode}
51
+ levels={3}
52
+ onClose={onClose}
53
+ onCenterNode={onCenterNode}
54
+ onMinimize={onMinimize}
55
+ onTogglePin={onTogglePin}
56
+ pinned
57
+ />,
58
+ );
59
+
60
+ expect(screen.getByText("Orders")).toBeInTheDocument();
61
+ expect(
62
+ screen
63
+ .getByLabelText("Lineage inspector")
64
+ .querySelector(".lineage-inspector__header-icon .table.icon"),
65
+ ).toBeInTheDocument();
66
+ expect(screen.getAllByText("Warehouse › Sales › Orders")).toHaveLength(2);
67
+ expect(screen.getByText("Oracle")).toBeInTheDocument();
68
+ expect(screen.getAllByText("Table")).toHaveLength(2);
69
+ expect(screen.getByText("88")).toBeInTheDocument();
70
+ expect(screen.queryByText(/completeness/i)).not.toBeInTheDocument();
71
+ expect(screen.getByRole("button", { name: "Unpin focus" })).toHaveAttribute(
72
+ "aria-pressed",
73
+ "true",
74
+ );
75
+ expect(
76
+ screen
77
+ .getByRole("button", { name: "Unpin focus" })
78
+ .querySelector(".lineage-unpin-icon"),
79
+ ).toBeInTheDocument();
80
+ expect(
81
+ screen.getByRole("button", { name: "Minimize inspector" }),
82
+ ).toHaveClass("lineage-inspector__minimize--pinned");
83
+ expect(screen.getByRole("button", { name: "Lineage" })).toBeInTheDocument();
84
+ expect(screen.getByRole("button", { name: "Impact" })).toBeInTheDocument();
85
+ expect(screen.getByRole("button", { name: "Exclude" })).toBeInTheDocument();
86
+ expect(
87
+ screen.getByRole("link", { name: "View Quality Rules" }),
88
+ ).toBeInTheDocument();
89
+ expect(
90
+ screen.getByRole("link", { name: "View in Data Catalog" }),
91
+ ).toBeInTheDocument();
92
+ expect(
93
+ screen.queryByRole("button", { name: "Close inspector" }),
94
+ ).not.toBeInTheDocument();
95
+
96
+ const actions = screen
97
+ .getAllByRole("button")
98
+ .map((button) => button.getAttribute("aria-label"));
99
+ expect(actions).toEqual([
100
+ "Minimize inspector",
101
+ "Center in graph",
102
+ "Unpin focus",
103
+ "Exclude",
104
+ "Lineage",
105
+ "Impact",
106
+ ]);
107
+ expect(
108
+ screen
109
+ .getByLabelText("Lineage inspector")
110
+ .querySelector(".lineage-inspector__action-divider"),
111
+ ).toBeInTheDocument();
112
+
113
+ fireEvent.click(screen.getByRole("button", { name: "Center in graph" }));
114
+ fireEvent.click(screen.getByRole("button", { name: "Minimize inspector" }));
115
+ fireEvent.click(screen.getByRole("button", { name: "Unpin focus" }));
116
+ fireEvent.click(screen.getByRole("button", { name: "Lineage" }));
117
+
118
+ expect(onCenterNode).toHaveBeenCalledWith("resource-1");
119
+ expect(onMinimize).toHaveBeenCalledTimes(1);
120
+ expect(onTogglePin).toHaveBeenCalledTimes(1);
121
+ expect(createGraph).toHaveBeenCalledWith({
122
+ ids: ["resource-1"],
123
+ isRedirected: true,
124
+ levels: 3,
125
+ type: "lineage",
126
+ });
127
+ expect(onClose).toHaveBeenCalledTimes(1);
128
+ });
129
+
130
+ it("does not show a Without System Group placeholder as a system fact", () => {
131
+ render(
132
+ <LineageInspectorPanel
133
+ context={{
134
+ type: "node",
135
+ node: {
136
+ id: "resource-2",
137
+ graphNodeId: "resource-2",
138
+ lineageKind: "resource",
139
+ name: "Orphan table",
140
+ systemTypeLabel: "Without System Group",
141
+ type: "Table",
142
+ },
143
+ }}
144
+ createGraph={createGraph}
145
+ excludeNode={excludeNode}
146
+ onClose={onClose}
147
+ onCenterNode={onCenterNode}
148
+ onMinimize={onMinimize}
149
+ />,
150
+ );
151
+
152
+ expect(screen.queryByText("Without System Group")).not.toBeInTheDocument();
153
+ expect(screen.getAllByText("Table")).toHaveLength(2);
154
+ });
155
+
156
+ it("keeps each grouped original available for catalogue and lineage actions", () => {
157
+ render(
158
+ <LineageInspectorPanel
159
+ context={{
160
+ type: "node",
161
+ node: {
162
+ id: "glue:/Glue/db/table/field",
163
+ name: "field",
164
+ lineageKind: "resource",
165
+ originals: [
166
+ { id: "glue:/Glue/db/table/field", structure_id: 11 },
167
+ {
168
+ id: "https://glue.region.amazonaws.com/#/account/db/table/field",
169
+ structure_id: 22,
170
+ },
171
+ ],
172
+ },
173
+ }}
174
+ createGraph={createGraph}
175
+ excludeNode={excludeNode}
176
+ onClose={onClose}
177
+ onMinimize={onMinimize}
178
+ />,
179
+ );
180
+ const selector = screen.getByRole("combobox", {
181
+ name: "Original resource",
182
+ });
183
+ expect(selector.options).toHaveLength(2);
184
+ expect(
185
+ screen.getByRole("link", { name: "View in Data Catalog" }).href,
186
+ ).toContain("11");
187
+ fireEvent.change(selector, {
188
+ target: { value: selector.options[1].value },
189
+ });
190
+ expect(
191
+ screen.getByRole("link", { name: "View in Data Catalog" }).href,
192
+ ).toContain("22");
193
+ fireEvent.click(screen.getByRole("button", { name: "Impact" }));
194
+ expect(createGraph).toHaveBeenCalledWith(
195
+ expect.objectContaining({
196
+ ids: [selector.options[1].value],
197
+ type: "impact",
198
+ }),
199
+ );
200
+ });
201
+
202
+ it("renders every edge metadata value, including nested and null values", () => {
203
+ render(
204
+ <LineageInspectorPanel
205
+ context={{
206
+ type: "edge",
207
+ edge: {
208
+ metadata: {
209
+ columns: ["customer_id", "id"],
210
+ nullable: null,
211
+ rule: { mode: "strict", nested: { active: true } },
212
+ },
213
+ relationType: "depends",
214
+ source: "source-id",
215
+ sourceName: "Orders",
216
+ target: "target-id",
217
+ targetName: "Customers",
218
+ },
219
+ }}
220
+ createGraph={createGraph}
221
+ excludeNode={excludeNode}
222
+ onClose={onClose}
223
+ onMinimize={onMinimize}
224
+ />,
225
+ );
226
+
227
+ expect(screen.getByText("Orders → Customers")).toBeInTheDocument();
228
+ expect(screen.getByText("DEPENDS")).toBeInTheDocument();
229
+ expect(screen.getByText("columns")).toBeInTheDocument();
230
+ expect(screen.getByText("nullable")).toBeInTheDocument();
231
+ expect(screen.getByText("—")).toBeInTheDocument();
232
+ expect(screen.getByText(/"nested": \{/)).toBeInTheDocument();
233
+ expect(screen.getByText(/"active": true/)).toBeInTheDocument();
234
+ });
235
+
236
+ it.each([
237
+ ["a column", { fieldsKnown: true, totalFields: 0, type: "Column" }, false],
238
+ [
239
+ "unknown fields",
240
+ { fieldsKnown: false, totalFields: 0, type: "Table" },
241
+ false,
242
+ ],
243
+ ["an explicit empty collection", { fields: [], type: "Table" }, true],
244
+ [
245
+ "an explicit populated collection",
246
+ { fields: [{}, {}], type: "Table" },
247
+ true,
248
+ ],
249
+ ])("handles Fields for %s", (_label, node, visible) => {
250
+ render(
251
+ <LineageInspectorPanel
252
+ context={{
253
+ type: "node",
254
+ node: {
255
+ graphNodeId: "node-1",
256
+ id: "node-1",
257
+ lineageKind: "resource",
258
+ name: "Node",
259
+ ...node,
260
+ },
261
+ }}
262
+ createGraph={createGraph}
263
+ excludeNode={excludeNode}
264
+ onClose={onClose}
265
+ onMinimize={onMinimize}
266
+ />,
267
+ );
268
+
269
+ if (visible) expect(screen.getByText("Fields")).toBeInTheDocument();
270
+ else expect(screen.queryByText("Fields")).not.toBeInTheDocument();
271
+ });
272
+
273
+ it("summarizes an overflowing path and exposes the full value on focus", () => {
274
+ const originalScrollHeight = Object.getOwnPropertyDescriptor(
275
+ HTMLElement.prototype,
276
+ "scrollHeight",
277
+ );
278
+ let resize;
279
+ let height = 60;
280
+
281
+ Object.defineProperty(HTMLElement.prototype, "scrollHeight", {
282
+ configurable: true,
283
+ get() {
284
+ return this.classList.contains("lineage-inspector__path--measure")
285
+ ? height
286
+ : 0;
287
+ },
288
+ });
289
+ global.ResizeObserver = class ResizeObserver {
290
+ constructor(callback) {
291
+ resize = callback;
292
+ }
293
+
294
+ disconnect() {}
295
+
296
+ observe() {}
297
+ };
298
+
299
+ const rendered = render(
300
+ <HierarchyPath parts={["Root", "Domain", "Schema", "Node"]} />,
301
+ );
302
+ const trigger = rendered.container.querySelector(
303
+ ".lineage-inspector__path--truncated",
304
+ );
305
+
306
+ expect(trigger).toHaveTextContent("Root › … › Node");
307
+ expect(trigger).toHaveAttribute("tabindex", "0");
308
+
309
+ fireEvent.focus(trigger);
310
+ act(() => jest.runOnlyPendingTimers());
311
+ expect(screen.getAllByText("Root › Domain › Schema › Node")).toHaveLength(
312
+ 2,
313
+ );
314
+
315
+ height = 20;
316
+ act(() => resize());
317
+ expect(
318
+ rendered.container.querySelector(".lineage-inspector__path--truncated"),
319
+ ).not.toBeInTheDocument();
320
+
321
+ if (originalScrollHeight) {
322
+ Object.defineProperty(
323
+ HTMLElement.prototype,
324
+ "scrollHeight",
325
+ originalScrollHeight,
326
+ );
327
+ } else {
328
+ delete HTMLElement.prototype.scrollHeight;
329
+ }
330
+ delete global.ResizeObserver;
331
+ });
332
+
333
+ it("uses one motion token, reduced motion and the neutral detail hover", () => {
334
+ const styles = fs.readFileSync(
335
+ path.resolve(__dirname, "../../styles/LineageGraph.less"),
336
+ "utf8",
337
+ );
338
+
339
+ expect(styles).toContain("--td-lineage-inspector-motion-duration: 180ms;");
340
+ expect(styles).toContain("@media (prefers-reduced-motion: reduce)");
341
+ expect(styles).toContain(
342
+ ".lineage-view__header-icon > .icon,\n.lineage-inspector__header-icon > .icon",
343
+ );
344
+ expect(styles).toContain(
345
+ "justify-content: center;\n margin: 0;\n line-height: 1;",
346
+ );
347
+ expect(styles).toContain(
348
+ ".lineage-inspector__minimize--pinned:hover {\n background: var(--td-graph-accent, #ed5c17);\n color: #ffffff;",
349
+ );
350
+ expect(styles).toContain(".lineage-inspector__action--view-detail:hover");
351
+ expect(styles).toContain("background: #667085;");
352
+ });
353
+ });
354
+
355
+ describe("formatMetadataValue", () => {
356
+ it("formats primitives, nulls and nested objects safely", () => {
357
+ expect(formatMetadataValue(null)).toBe("—");
358
+ expect(formatMetadataValue(false)).toBe("false");
359
+ expect(formatMetadataValue(["a", "b"])).toBe('[\n "a",\n "b"\n]');
360
+ expect(formatMetadataValue({ nested: { score: 1 } })).toContain(
361
+ '"score": 1',
362
+ );
363
+ });
364
+ });
@@ -0,0 +1,338 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fireEvent } from "@testing-library/react";
4
+ import { render } from "@truedat/test/render";
5
+ import { EdgeHoverContext } from "@truedat/core/components/graph/ColoredEdge";
6
+ import LineageResourceNode, {
7
+ lineageQualityLevel,
8
+ } from "../LineageResourceNode";
9
+
10
+ jest.mock("@truedat/core/components/graph/FlowPorts", () => ({
11
+ // eslint-disable-next-line react/prop-types
12
+ Handle: ({ className, id, position, type }) => (
13
+ <span
14
+ className={className}
15
+ data-handle-id={id}
16
+ data-position={position}
17
+ data-type={type}
18
+ />
19
+ ),
20
+ Position: {
21
+ Left: "left",
22
+ Right: "right",
23
+ },
24
+ }));
25
+
26
+ describe("<LineageResourceNode />", () => {
27
+ const fields = Array.from({ length: 12 }, (_, index) => ({
28
+ name: `field_${index + 1}`,
29
+ primary_key: index === 0,
30
+ type: index % 2 ? "STRING" : "INT",
31
+ }));
32
+
33
+ it("renders six fields and an accessible disclosure by default", () => {
34
+ const onToggleFields = jest.fn();
35
+ const rendered = render(
36
+ <LineageResourceNode
37
+ id="fact-orders"
38
+ data={{
39
+ fieldHandleLimit: 6,
40
+ fields: fields.slice(0, 6),
41
+ hiddenFieldsCount: 6,
42
+ kind: "resource",
43
+ label: "fact_orders",
44
+ systemTypeLabel: "Table",
45
+ onToggleFields,
46
+ quality: "98%",
47
+ status: "warning",
48
+ totalFields: fields.length,
49
+ variant: "lineage-redesigned",
50
+ }}
51
+ />,
52
+ );
53
+
54
+ expect(rendered.getByText("fact_orders")).toBeInTheDocument();
55
+ expect(rendered.getByText("Table")).toBeInTheDocument();
56
+ expect(rendered.getByText("field_1")).toBeInTheDocument();
57
+ expect(rendered.queryByText("field_7")).not.toBeInTheDocument();
58
+
59
+ const disclosure = rendered.getByRole("button", {
60
+ name: "+ 6 elements",
61
+ });
62
+
63
+ expect(disclosure).toHaveAttribute("aria-expanded", "false");
64
+ fireEvent.click(disclosure);
65
+ expect(onToggleFields).toHaveBeenCalledTimes(1);
66
+ expect(
67
+ rendered.container.querySelector('[data-handle-id="field-source-0"]'),
68
+ ).toBeInTheDocument();
69
+ expect(
70
+ rendered.container.querySelector('[data-handle-id="field-target-5"]'),
71
+ ).toBeInTheDocument();
72
+ });
73
+
74
+ it("renders all expanded fields in a ten-row scroll window", () => {
75
+ const rendered = render(
76
+ <LineageResourceNode
77
+ id="fact-orders"
78
+ data={{
79
+ fieldHandleLimit: 10,
80
+ fields,
81
+ fieldsExpanded: true,
82
+ hiddenFieldsCount: 6,
83
+ kind: "resource",
84
+ label: "fact_orders",
85
+ onToggleFields: jest.fn(),
86
+ totalFields: fields.length,
87
+ variant: "lineage-redesigned",
88
+ }}
89
+ />,
90
+ );
91
+
92
+ expect(rendered.getByText("field_12")).toBeInTheDocument();
93
+ expect(rendered.getByRole("button", { name: "Show less" })).toHaveAttribute(
94
+ "aria-expanded",
95
+ "true",
96
+ );
97
+ expect(
98
+ rendered.container.querySelector(
99
+ ".td-lineage-resource-node__fields--scrollable",
100
+ ),
101
+ ).toBeInTheDocument();
102
+ expect(
103
+ rendered.container.querySelector('[data-handle-id="field-target-9"]'),
104
+ ).toBeInTheDocument();
105
+ expect(
106
+ rendered.container.querySelector('[data-handle-id="field-target-10"]'),
107
+ ).not.toBeInTheDocument();
108
+ });
109
+
110
+ it("does not invent quality or status values", () => {
111
+ const rendered = render(
112
+ <LineageResourceNode
113
+ id="without-metadata"
114
+ data={{ kind: "resource", label: "raw_table", meta: "Table" }}
115
+ />,
116
+ );
117
+
118
+ expect(rendered.queryByText(/QC/)).not.toBeInTheDocument();
119
+ expect(rendered.queryByTitle("Ready")).not.toBeInTheDocument();
120
+ });
121
+
122
+ it("renders entity type badge when entityType is provided", () => {
123
+ const rendered = render(
124
+ <LineageResourceNode
125
+ id="process"
126
+ data={{ entityType: "process", kind: "process", label: "Process" }}
127
+ />,
128
+ );
129
+
130
+ expect(
131
+ rendered.container.querySelector(
132
+ ".td-lineage-resource-node__entity-badge",
133
+ ),
134
+ ).toBeInTheDocument();
135
+ });
136
+
137
+ it("keeps the selected modifier used by the orange emphasis state", () => {
138
+ const rendered = render(
139
+ <LineageResourceNode
140
+ id="selected-resource"
141
+ data={{
142
+ isActive: true,
143
+ kind: "resource",
144
+ label: "selected_table",
145
+ variant: "lineage-redesigned",
146
+ }}
147
+ />,
148
+ );
149
+
150
+ expect(
151
+ rendered.container.querySelector(
152
+ ".td-lineage-resource-node--redesigned.td-lineage-resource-node--active",
153
+ ),
154
+ ).toBeInTheDocument();
155
+ });
156
+
157
+ it("marks the node under the pointer independently from related nodes", () => {
158
+ const rendered = render(
159
+ <EdgeHoverContext.Provider value={{ hoveredNodeId: "hovered-resource" }}>
160
+ <LineageResourceNode
161
+ id="hovered-resource"
162
+ data={{
163
+ kind: "resource",
164
+ label: "hovered_table",
165
+ variant: "lineage-redesigned",
166
+ }}
167
+ />
168
+ </EdgeHoverContext.Provider>,
169
+ );
170
+
171
+ expect(
172
+ rendered.container.querySelector(
173
+ ".td-lineage-resource-node--hovered:not(.td-lineage-resource-node--connected)",
174
+ ),
175
+ ).toBeInTheDocument();
176
+ });
177
+
178
+ it("hides the system badge when the system type is missing", () => {
179
+ const rendered = render(
180
+ <LineageResourceNode
181
+ id="orphan-table"
182
+ data={{
183
+ kind: "resource",
184
+ label: "orphan_table",
185
+ systemTypeLabel: "Table",
186
+ systemTypeLabel: "Without System Group",
187
+ systemTypeMissing: true,
188
+ variant: "lineage-redesigned",
189
+ }}
190
+ />,
191
+ );
192
+
193
+ expect(rendered.getByText("orphan_table")).toBeInTheDocument();
194
+ expect(
195
+ rendered.queryByText("Without System Group"),
196
+ ).not.toBeInTheDocument();
197
+ });
198
+
199
+ it("keeps a real SYSTEM badge visible", () => {
200
+ const rendered = render(
201
+ <LineageResourceNode
202
+ id="system-table"
203
+ data={{
204
+ kind: "resource",
205
+ label: "system_table",
206
+ systemTypeLabel: "Table",
207
+ systemTypeLabel: "SYSTEM",
208
+ systemTypeMissing: false,
209
+ variant: "lineage-redesigned",
210
+ }}
211
+ />,
212
+ );
213
+
214
+ expect(rendered.getByText("SYSTEM")).toBeInTheDocument();
215
+ });
216
+
217
+ it("keeps the card geometry and scroll affordance in CSS", () => {
218
+ const styles = fs.readFileSync(
219
+ path.resolve(__dirname, "../../styles/LineageResourceNode.less"),
220
+ "utf8",
221
+ );
222
+
223
+ expect(styles).toContain("width: var(--td-lineage-node-width, 288px);");
224
+ expect(styles).toContain("height: 42px;");
225
+ expect(styles).toContain("max-height: 320px;");
226
+ expect(styles).toContain("box-sizing: border-box;");
227
+ expect(styles).toContain("scrollbar-color:");
228
+ expect(styles).toContain("field-type-badge");
229
+ expect(styles).toContain("entity-badge");
230
+ });
231
+ });
232
+
233
+ describe("lineageQualityLevel", () => {
234
+ it("maps high quality scores to good", () => {
235
+ expect(lineageQualityLevel("99.9%")).toBe("good");
236
+ expect(lineageQualityLevel(95)).toBe("good");
237
+ expect(lineageQualityLevel(90)).toBe("good");
238
+ });
239
+
240
+ it("maps mid quality scores to warning", () => {
241
+ expect(lineageQualityLevel(80)).toBe("warning");
242
+ expect(lineageQualityLevel("75%")).toBe("warning");
243
+ });
244
+
245
+ it("maps low quality scores to poor", () => {
246
+ expect(lineageQualityLevel(50)).toBe("poor");
247
+ expect(lineageQualityLevel("0.3")).toBe("poor");
248
+ });
249
+
250
+ it("returns undefined when no quality data is present", () => {
251
+ expect(lineageQualityLevel()).toBeUndefined();
252
+ expect(lineageQualityLevel(null)).toBeUndefined();
253
+ });
254
+ });
255
+
256
+ describe("field type badge and PK key icon", () => {
257
+ it("renders PK badge for primary key fields", () => {
258
+ const rendered = render(
259
+ <LineageResourceNode
260
+ id="table-with-pk"
261
+ data={{
262
+ fields: [{ name: "id", primary_key: true, type: "BIGINT" }],
263
+ kind: "resource",
264
+ label: "orders",
265
+ variant: "lineage-redesigned",
266
+ }}
267
+ />,
268
+ );
269
+
270
+ expect(rendered.getByText("id")).toBeInTheDocument();
271
+ expect(rendered.getByText("PK")).toBeInTheDocument();
272
+ expect(rendered.getByText("BIGINT")).toBeInTheDocument();
273
+ expect(
274
+ rendered.container.querySelector(
275
+ ".td-lineage-resource-node__field-type-badge",
276
+ ),
277
+ ).toBeInTheDocument();
278
+ });
279
+
280
+ it("renders type badge without PK for non-PK fields", () => {
281
+ const rendered = render(
282
+ <LineageResourceNode
283
+ id="table-no-pk"
284
+ data={{
285
+ fields: [{ name: "name", type: "VARCHAR" }],
286
+ kind: "resource",
287
+ label: "users",
288
+ variant: "lineage-redesigned",
289
+ }}
290
+ />,
291
+ );
292
+
293
+ expect(rendered.getByText("name")).toBeInTheDocument();
294
+ expect(rendered.queryByText("PK")).not.toBeInTheDocument();
295
+ expect(rendered.getByText("VARCHAR")).toBeInTheDocument();
296
+ expect(
297
+ rendered.container.querySelector(
298
+ ".td-lineage-resource-node__field-type-badge",
299
+ ),
300
+ ).toBeInTheDocument();
301
+ });
302
+ });
303
+
304
+ describe("query origin presentation", () => {
305
+ it.each(["en", "es"])(
306
+ "keeps the origin visible while selected and hovered in %s",
307
+ (locale) => {
308
+ const rendered = render(
309
+ <EdgeHoverContext.Provider value={{ hoveredNodeId: "origin" }}>
310
+ <LineageResourceNode
311
+ id="origin"
312
+ data={{
313
+ label: "Orders",
314
+ queryOrigin: "direct",
315
+ focusSelected: true,
316
+ variant: "lineage-redesigned",
317
+ }}
318
+ />
319
+ </EdgeHoverContext.Provider>,
320
+ { locale },
321
+ );
322
+ expect(
323
+ rendered.getByText(
324
+ locale === "es" ? "Origen de consulta" : "Query origin",
325
+ ),
326
+ ).toBeInTheDocument();
327
+ const card = rendered.container.querySelector(
328
+ ".td-lineage-resource-node",
329
+ );
330
+ expect(card).toHaveClass(
331
+ "td-lineage-resource-node--query-origin",
332
+ "td-lineage-resource-node--focus-selected",
333
+ "td-lineage-resource-node--hovered",
334
+ );
335
+ expect(rendered.getByText("Orders")).toBeInTheDocument();
336
+ },
337
+ );
338
+ });