@truedat/core 8.11.6 → 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.
- package/package.json +4 -3
- package/src/components/CardGroupsAccordion.js +0 -2
- package/src/components/Graph.js +1410 -158
- package/src/components/__tests__/ColoredEdge.spec.js +372 -0
- package/src/components/__tests__/ConceptNode.spec.js +181 -0
- package/src/components/__tests__/Graph.spec.js +183 -12
- package/src/components/__tests__/LineageEdgeRendering.spec.js +45 -0
- package/src/components/__tests__/LineageGroupNode.spec.js +308 -0
- package/src/components/__tests__/__snapshots__/ConceptNode.spec.js.snap +23 -0
- package/src/components/graph/ColoredEdge.js +715 -79
- package/src/components/graph/ConceptNode.js +21 -4
- package/src/components/graph/FlowPorts.js +1 -0
- package/src/components/graph/LineageGroupNode.js +320 -0
- package/src/components/graph/LineageQueryOrigin.js +27 -0
- package/src/components/graph/edgeLayout.js +7 -4
- package/src/components/graph/lineageNodeIcon.js +17 -0
- package/src/components/graph/lineageQueryOriginMessages.js +10 -0
- package/src/styles/ColoredEdge.less +3 -0
- package/src/styles/CommonGraphs.less +76 -0
- package/src/styles/ConceptGraph.less +6 -0
- package/src/styles/LineageGroupNode.less +563 -0
- package/src/styles/graph.less +20 -25
- package/src/styles/lineageNodeStates.less +111 -0
- /package/src/{components/graph → styles}/ConceptNode.less +0 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { fireEvent, waitFor } from "@testing-library/react";
|
|
2
|
+
import { render } from "@truedat/test/render";
|
|
3
|
+
import ColoredEdge, {
|
|
4
|
+
EdgeHoverContext,
|
|
5
|
+
edgeAnimationDuration,
|
|
6
|
+
getClassicArrowPoints,
|
|
7
|
+
getFlexibleEdgePath,
|
|
8
|
+
} from "../graph/ColoredEdge";
|
|
9
|
+
|
|
10
|
+
jest.mock("@xyflow/react", () => ({
|
|
11
|
+
// eslint-disable-next-line react/prop-types
|
|
12
|
+
BaseEdge: ({ className, id, path, style }) => (
|
|
13
|
+
<path className={className} d={path} id={id} style={style} />
|
|
14
|
+
),
|
|
15
|
+
EdgeLabelRenderer: ({ children }) => children,
|
|
16
|
+
getBezierPath: ({ sourceX, sourceY, targetX, targetY }) => [
|
|
17
|
+
`M ${sourceX},${sourceY} C ${sourceX},${sourceY} ${targetX},${targetY} ${targetX},${targetY}`,
|
|
18
|
+
(sourceX + targetX) / 2,
|
|
19
|
+
(sourceY + targetY) / 2,
|
|
20
|
+
],
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
describe("<ColoredEdge /> flow animation", () => {
|
|
24
|
+
it("uses the measured path length for the motion duration", async () => {
|
|
25
|
+
const originalGetTotalLength = SVGElement.prototype.getTotalLength;
|
|
26
|
+
|
|
27
|
+
SVGElement.prototype.getTotalLength = jest.fn(() => 400);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const rendered = render(
|
|
31
|
+
<svg>
|
|
32
|
+
<ColoredEdge
|
|
33
|
+
id="animated-edge"
|
|
34
|
+
source="a"
|
|
35
|
+
target="b"
|
|
36
|
+
sourceX={0}
|
|
37
|
+
sourceY={100}
|
|
38
|
+
targetX={400}
|
|
39
|
+
targetY={100}
|
|
40
|
+
sourcePosition="right"
|
|
41
|
+
targetPosition="left"
|
|
42
|
+
data={{ animated: true, edgePath: "orthogonal" }}
|
|
43
|
+
/>
|
|
44
|
+
</svg>,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
await waitFor(() => {
|
|
48
|
+
const motion = rendered.container.querySelector("animateMotion");
|
|
49
|
+
|
|
50
|
+
expect(motion).not.toBeNull();
|
|
51
|
+
expect(motion.getAttribute("dur")).toBe("10s");
|
|
52
|
+
expect(motion.getAttribute("calcMode")).toBe("paced");
|
|
53
|
+
});
|
|
54
|
+
} finally {
|
|
55
|
+
if (originalGetTotalLength) {
|
|
56
|
+
SVGElement.prototype.getTotalLength = originalGetTotalLength;
|
|
57
|
+
} else {
|
|
58
|
+
delete SVGElement.prototype.getTotalLength;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("keeps the circle immediately visible on very long paths", async () => {
|
|
64
|
+
const originalGetTotalLength = SVGElement.prototype.getTotalLength;
|
|
65
|
+
|
|
66
|
+
SVGElement.prototype.getTotalLength = jest.fn(() => 4000);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const rendered = render(
|
|
70
|
+
<svg>
|
|
71
|
+
<ColoredEdge
|
|
72
|
+
id="long-animated-edge"
|
|
73
|
+
source="a"
|
|
74
|
+
target="b"
|
|
75
|
+
sourceX={0}
|
|
76
|
+
sourceY={100}
|
|
77
|
+
targetX={4000}
|
|
78
|
+
targetY={100}
|
|
79
|
+
sourcePosition="right"
|
|
80
|
+
targetPosition="left"
|
|
81
|
+
data={{ animated: true, edgePath: "orthogonal" }}
|
|
82
|
+
/>
|
|
83
|
+
</svg>,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
await waitFor(() => {
|
|
87
|
+
const circle = rendered.container.querySelector(
|
|
88
|
+
".td-graph-edge-flow-dot",
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(circle).toHaveAttribute("opacity", "1");
|
|
92
|
+
expect(circle.querySelector("animateMotion")).toHaveAttribute(
|
|
93
|
+
"dur",
|
|
94
|
+
"100s",
|
|
95
|
+
);
|
|
96
|
+
expect(
|
|
97
|
+
circle.querySelector('animate[attributeName="opacity"]'),
|
|
98
|
+
).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
} finally {
|
|
101
|
+
if (originalGetTotalLength) {
|
|
102
|
+
SVGElement.prototype.getTotalLength = originalGetTotalLength;
|
|
103
|
+
} else {
|
|
104
|
+
delete SVGElement.prototype.getTotalLength;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("<ColoredEdge /> metadata badge", () => {
|
|
111
|
+
const baseProps = {
|
|
112
|
+
id: "edge-1",
|
|
113
|
+
source: "a",
|
|
114
|
+
target: "b",
|
|
115
|
+
sourceX: 0,
|
|
116
|
+
sourceY: 100,
|
|
117
|
+
targetX: 400,
|
|
118
|
+
targetY: 100,
|
|
119
|
+
sourcePosition: "right",
|
|
120
|
+
targetPosition: "left",
|
|
121
|
+
style: {},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
it("opens the details handler when the metadata badge is clicked", () => {
|
|
125
|
+
const onClick = jest.fn();
|
|
126
|
+
const metadata = { source: "sales", relation: "derives" };
|
|
127
|
+
const rendered = render(
|
|
128
|
+
<ColoredEdge {...baseProps} data={{ metadata, onClick }} />,
|
|
129
|
+
);
|
|
130
|
+
const badge = rendered.container.querySelector(
|
|
131
|
+
".td-graph-edge-metadata-badge",
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(badge).not.toBeNull();
|
|
135
|
+
fireEvent.click(badge);
|
|
136
|
+
expect(onClick).toHaveBeenCalledWith(metadata);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("does nothing when the edge has metadata but no click handler", () => {
|
|
140
|
+
const onClick = jest.fn();
|
|
141
|
+
const rendered = render(
|
|
142
|
+
<ColoredEdge {...baseProps} data={{ metadata: { source: "sales" } }} />,
|
|
143
|
+
);
|
|
144
|
+
const badge = rendered.container.querySelector(
|
|
145
|
+
".td-graph-edge-metadata-badge",
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
fireEvent.click(badge);
|
|
149
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("<ColoredEdge /> arrow markers", () => {
|
|
154
|
+
const arrowProps = {
|
|
155
|
+
id: "edge-arrow",
|
|
156
|
+
source: "a",
|
|
157
|
+
target: "b",
|
|
158
|
+
sourceX: 0,
|
|
159
|
+
sourceY: 100,
|
|
160
|
+
targetX: 400,
|
|
161
|
+
targetY: 100,
|
|
162
|
+
sourcePosition: "right",
|
|
163
|
+
targetPosition: "left",
|
|
164
|
+
markerEnd: { type: "arrow" },
|
|
165
|
+
style: {},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
it("draws the filled arrow at the target for impact edges", () => {
|
|
169
|
+
const rendered = render(
|
|
170
|
+
<ColoredEdge
|
|
171
|
+
{...arrowProps}
|
|
172
|
+
data={{ edgePath: "orthogonal", markerPosition: "target" }}
|
|
173
|
+
/>,
|
|
174
|
+
);
|
|
175
|
+
const arrow = rendered.container.querySelector(".td-graph-edge-arrow");
|
|
176
|
+
|
|
177
|
+
expect(arrow).not.toBeNull();
|
|
178
|
+
expect(arrow.getAttribute("points")).toMatch(/^400,100/);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("draws the filled arrow at the source for traceability edges", () => {
|
|
182
|
+
const rendered = render(
|
|
183
|
+
<ColoredEdge
|
|
184
|
+
{...arrowProps}
|
|
185
|
+
data={{ edgePath: "orthogonal", markerPosition: "source" }}
|
|
186
|
+
/>,
|
|
187
|
+
);
|
|
188
|
+
const arrow = rendered.container.querySelector(".td-graph-edge-arrow");
|
|
189
|
+
|
|
190
|
+
expect(arrow).not.toBeNull();
|
|
191
|
+
expect(arrow.getAttribute("points")).toMatch(/^0,100/);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("colors target arrows that share the hovered source bundle", () => {
|
|
195
|
+
const rendered = render(
|
|
196
|
+
<EdgeHoverContext.Provider
|
|
197
|
+
value={{
|
|
198
|
+
hoveredEdgeEndpoints: {
|
|
199
|
+
edgeId: "hovered-edge",
|
|
200
|
+
source: "a",
|
|
201
|
+
target: "first-target",
|
|
202
|
+
},
|
|
203
|
+
}}
|
|
204
|
+
>
|
|
205
|
+
<svg>
|
|
206
|
+
<ColoredEdge
|
|
207
|
+
{...arrowProps}
|
|
208
|
+
id="sibling-edge"
|
|
209
|
+
target="second-target"
|
|
210
|
+
data={{ edgePath: "bezier", bundleSource: true }}
|
|
211
|
+
/>
|
|
212
|
+
</svg>
|
|
213
|
+
</EdgeHoverContext.Provider>,
|
|
214
|
+
);
|
|
215
|
+
const edge = rendered.container.querySelector(".td-graph-edge");
|
|
216
|
+
const arrow = rendered.container.querySelector(".td-graph-edge-arrow");
|
|
217
|
+
|
|
218
|
+
expect(edge).toHaveClass("td-graph-edge--source-bundle-hovered");
|
|
219
|
+
expect(arrow).toHaveAttribute(
|
|
220
|
+
"fill",
|
|
221
|
+
"var(--td-graph-edge-active, var(--td-graph-accent, #ed5c17))",
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("keeps arrows from another source field out of the hovered bundle", () => {
|
|
226
|
+
const rendered = render(
|
|
227
|
+
<EdgeHoverContext.Provider
|
|
228
|
+
value={{
|
|
229
|
+
hoveredEdgeEndpoints: {
|
|
230
|
+
edgeId: "hovered-edge",
|
|
231
|
+
source: "a",
|
|
232
|
+
sourceFieldIndex: 0,
|
|
233
|
+
target: "first-target",
|
|
234
|
+
},
|
|
235
|
+
}}
|
|
236
|
+
>
|
|
237
|
+
<svg>
|
|
238
|
+
<ColoredEdge
|
|
239
|
+
{...arrowProps}
|
|
240
|
+
id="other-field-edge"
|
|
241
|
+
target="second-target"
|
|
242
|
+
data={{
|
|
243
|
+
edgePath: "bezier",
|
|
244
|
+
bundleSource: true,
|
|
245
|
+
sourceFieldIndex: 1,
|
|
246
|
+
}}
|
|
247
|
+
/>
|
|
248
|
+
</svg>
|
|
249
|
+
</EdgeHoverContext.Provider>,
|
|
250
|
+
);
|
|
251
|
+
const edge = rendered.container.querySelector(".td-graph-edge");
|
|
252
|
+
const arrow = rendered.container.querySelector(".td-graph-edge-arrow");
|
|
253
|
+
|
|
254
|
+
expect(edge).not.toHaveClass("td-graph-edge--source-bundle-hovered");
|
|
255
|
+
expect(arrow).toHaveAttribute(
|
|
256
|
+
"fill",
|
|
257
|
+
"var(--td-graph-edge-stroke, #b0b8c8)",
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("does not clear a newer edge hover when leaving an overlapping branch", () => {
|
|
262
|
+
const setHoveredEdgeEndpoints = jest.fn();
|
|
263
|
+
const rendered = render(
|
|
264
|
+
<EdgeHoverContext.Provider value={{ setHoveredEdgeEndpoints }}>
|
|
265
|
+
<svg>
|
|
266
|
+
<ColoredEdge
|
|
267
|
+
{...arrowProps}
|
|
268
|
+
data={{ edgePath: "bezier", bundleSource: true }}
|
|
269
|
+
/>
|
|
270
|
+
</svg>
|
|
271
|
+
</EdgeHoverContext.Provider>,
|
|
272
|
+
);
|
|
273
|
+
const hitArea = rendered.container.querySelector(".td-graph-edge-hit-area");
|
|
274
|
+
|
|
275
|
+
fireEvent.mouseLeave(hitArea);
|
|
276
|
+
const clearHoveredEdge = setHoveredEdgeEndpoints.mock.calls[0][0];
|
|
277
|
+
const newerHover = { edgeId: "newer-edge" };
|
|
278
|
+
|
|
279
|
+
expect(clearHoveredEdge(newerHover)).toBe(newerHover);
|
|
280
|
+
expect(clearHoveredEdge({ edgeId: "edge-arrow" })).toBeNull();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("getFlexibleEdgePath", () => {
|
|
285
|
+
it("gives N-to-1 curves an identical final trunk", () => {
|
|
286
|
+
const first = getFlexibleEdgePath({
|
|
287
|
+
sourceX: 0,
|
|
288
|
+
sourceY: 20,
|
|
289
|
+
targetX: 400,
|
|
290
|
+
targetY: 100,
|
|
291
|
+
bundleTarget: true,
|
|
292
|
+
});
|
|
293
|
+
const second = getFlexibleEdgePath({
|
|
294
|
+
sourceX: 0,
|
|
295
|
+
sourceY: 180,
|
|
296
|
+
targetX: 400,
|
|
297
|
+
targetY: 100,
|
|
298
|
+
bundleTarget: true,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
expect(first.path).toContain("M 360,100 L 400,100");
|
|
302
|
+
expect(second.path).toContain("M 360,100 L 400,100");
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("gives 1-to-N curves an identical initial trunk", () => {
|
|
306
|
+
const edge = getFlexibleEdgePath({
|
|
307
|
+
sourceX: 0,
|
|
308
|
+
sourceY: 100,
|
|
309
|
+
targetX: 400,
|
|
310
|
+
targetY: 20,
|
|
311
|
+
bundleSource: true,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(edge.path.startsWith("M 0,100 L 40,100")).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("keeps shared trunks attached when the relation points left", () => {
|
|
318
|
+
const edge = getFlexibleEdgePath({
|
|
319
|
+
sourceX: 400,
|
|
320
|
+
sourceY: 100,
|
|
321
|
+
sourcePosition: "left",
|
|
322
|
+
targetX: 0,
|
|
323
|
+
targetY: 180,
|
|
324
|
+
targetPosition: "right",
|
|
325
|
+
bundleSource: true,
|
|
326
|
+
bundleTarget: true,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
expect(edge.path.startsWith("M 400,100 L 360,100")).toBe(true);
|
|
330
|
+
expect(edge.path).toContain("M 40,180 L 0,180");
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe("getClassicArrowPoints", () => {
|
|
335
|
+
it("points a triangle at the target for a rightward edge", () => {
|
|
336
|
+
const points = getClassicArrowPoints({
|
|
337
|
+
targetX: 400,
|
|
338
|
+
targetY: 100,
|
|
339
|
+
targetTangent: { x: 1, y: 0 },
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
expect(points).toBe("400,100 390,104 390,96");
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("points a triangle at the target for a leftward edge", () => {
|
|
346
|
+
const points = getClassicArrowPoints({
|
|
347
|
+
targetX: 0,
|
|
348
|
+
targetY: 100,
|
|
349
|
+
targetTangent: { x: -1, y: 0 },
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
expect(points).toBe("0,100 10,96 10,104");
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe("edgeAnimationDuration", () => {
|
|
357
|
+
it("keeps the circle at the same slow speed for every path length", () => {
|
|
358
|
+
const shortPathDuration = edgeAnimationDuration(100);
|
|
359
|
+
const longPathDuration = edgeAnimationDuration(400);
|
|
360
|
+
|
|
361
|
+
expect(shortPathDuration).toBe(2.5);
|
|
362
|
+
expect(longPathDuration).toBe(10);
|
|
363
|
+
expect(100 / shortPathDuration).toBe(400 / longPathDuration);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it.each([0, -1, Number.NaN, Number.POSITIVE_INFINITY])(
|
|
367
|
+
"ignores an invalid path length (%p)",
|
|
368
|
+
(pathLength) => {
|
|
369
|
+
expect(edgeAnimationDuration(pathLength)).toBeNull();
|
|
370
|
+
},
|
|
371
|
+
);
|
|
372
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { fireEvent } from "@testing-library/react";
|
|
2
|
+
import { render } from "@truedat/test/render";
|
|
3
|
+
import ConceptNode from "../graph/ConceptNode";
|
|
4
|
+
import { EdgeHoverContext } from "../graph/ColoredEdge";
|
|
5
|
+
|
|
6
|
+
jest.mock("@xyflow/react", () => ({
|
|
7
|
+
Handle: ({ type, position }) => (
|
|
8
|
+
<span data-testid={`handle-${type}`} data-position={position} />
|
|
9
|
+
),
|
|
10
|
+
Position: {
|
|
11
|
+
Left: "left",
|
|
12
|
+
Right: "right",
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe("<ConceptNode />", () => {
|
|
17
|
+
it("renders the label text", () => {
|
|
18
|
+
const rendered = render(
|
|
19
|
+
<ConceptNode id="node-1" data={{ label: "My Concept" }} />,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
expect(rendered.getByText("My Concept")).toBeInTheDocument();
|
|
23
|
+
expect(
|
|
24
|
+
rendered.container.querySelector(".td-concept-node__label"),
|
|
25
|
+
).toHaveTextContent("My Concept");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("renders target and source handles", () => {
|
|
29
|
+
const rendered = render(
|
|
30
|
+
<ConceptNode id="node-1" data={{ label: "Test" }} />,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(rendered.getByTestId("handle-target")).toHaveAttribute(
|
|
34
|
+
"data-position",
|
|
35
|
+
"left",
|
|
36
|
+
);
|
|
37
|
+
expect(rendered.getByTestId("handle-source")).toHaveAttribute(
|
|
38
|
+
"data-position",
|
|
39
|
+
"right",
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("applies the active modifier when isActive is true", () => {
|
|
44
|
+
const rendered = render(
|
|
45
|
+
<ConceptNode id="node-1" data={{ label: "Active", isActive: true }} />,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(rendered.container.querySelector(".td-concept-node")).toHaveClass(
|
|
49
|
+
"td-concept-node--active",
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("does not apply the active modifier when isActive is false", () => {
|
|
54
|
+
const rendered = render(
|
|
55
|
+
<ConceptNode id="node-1" data={{ label: "Inactive", isActive: false }} />,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
expect(
|
|
59
|
+
rendered.container.querySelector(".td-concept-node"),
|
|
60
|
+
).not.toHaveClass("td-concept-node--active");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("marks the node as connected when its id is in connectedNodeIds", () => {
|
|
64
|
+
const connectedNodeIds = new Set(["node-1", "node-2"]);
|
|
65
|
+
const rendered = render(
|
|
66
|
+
<EdgeHoverContext.Provider value={{ connectedNodeIds }}>
|
|
67
|
+
<ConceptNode id="node-1" data={{ label: "Connected" }} />
|
|
68
|
+
</EdgeHoverContext.Provider>,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(rendered.container.querySelector(".td-concept-node")).toHaveClass(
|
|
72
|
+
"td-concept-node--connected",
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("marks the node as connected when it is the source of a hovered edge", () => {
|
|
77
|
+
const rendered = render(
|
|
78
|
+
<EdgeHoverContext.Provider
|
|
79
|
+
value={{
|
|
80
|
+
hoveredEdgeEndpoints: { source: "node-1", target: "other" },
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<ConceptNode id="node-1" data={{ label: "Source" }} />
|
|
84
|
+
</EdgeHoverContext.Provider>,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(rendered.container.querySelector(".td-concept-node")).toHaveClass(
|
|
88
|
+
"td-concept-node--connected",
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("marks the node as connected when it is the target of a hovered edge", () => {
|
|
93
|
+
const rendered = render(
|
|
94
|
+
<EdgeHoverContext.Provider
|
|
95
|
+
value={{
|
|
96
|
+
hoveredEdgeEndpoints: { source: "other", target: "node-1" },
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
<ConceptNode id="node-1" data={{ label: "Target" }} />
|
|
100
|
+
</EdgeHoverContext.Provider>,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(rendered.container.querySelector(".td-concept-node")).toHaveClass(
|
|
104
|
+
"td-concept-node--connected",
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("does not mark the node as connected when it is not in the hover context", () => {
|
|
109
|
+
const rendered = render(
|
|
110
|
+
<EdgeHoverContext.Provider
|
|
111
|
+
value={{
|
|
112
|
+
connectedNodeIds: new Set(["other"]),
|
|
113
|
+
hoveredEdgeEndpoints: { source: "a", target: "b" },
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
<ConceptNode id="node-1" data={{ label: "Isolated" }} />
|
|
117
|
+
</EdgeHoverContext.Provider>,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(
|
|
121
|
+
rendered.container.querySelector(".td-concept-node"),
|
|
122
|
+
).not.toHaveClass("td-concept-node--connected");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("does not mark the node as connected when there is no hover context", () => {
|
|
126
|
+
const rendered = render(
|
|
127
|
+
<ConceptNode id="node-1" data={{ label: "No context" }} />,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect(
|
|
131
|
+
rendered.container.querySelector(".td-concept-node"),
|
|
132
|
+
).not.toHaveClass("td-concept-node--connected");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("calls setHoveredNodeId with the node id on mouse enter", () => {
|
|
136
|
+
const setHoveredNodeId = jest.fn();
|
|
137
|
+
const rendered = render(
|
|
138
|
+
<EdgeHoverContext.Provider value={{ setHoveredNodeId }}>
|
|
139
|
+
<ConceptNode id="node-1" data={{ label: "Hover me" }} />
|
|
140
|
+
</EdgeHoverContext.Provider>,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
fireEvent.mouseEnter(rendered.container.querySelector(".td-concept-node"));
|
|
144
|
+
expect(setHoveredNodeId).toHaveBeenCalledWith("node-1");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("calls setHoveredNodeId with null on mouse leave", () => {
|
|
148
|
+
const setHoveredNodeId = jest.fn();
|
|
149
|
+
const rendered = render(
|
|
150
|
+
<EdgeHoverContext.Provider value={{ setHoveredNodeId }}>
|
|
151
|
+
<ConceptNode id="node-1" data={{ label: "Leave me" }} />
|
|
152
|
+
</EdgeHoverContext.Provider>,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
fireEvent.mouseLeave(rendered.container.querySelector(".td-concept-node"));
|
|
156
|
+
expect(setHoveredNodeId).toHaveBeenCalledWith(null);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("does not throw when hovering without a hover context", () => {
|
|
160
|
+
const rendered = render(
|
|
161
|
+
<ConceptNode id="node-1" data={{ label: "No context" }} />,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
expect(() => {
|
|
165
|
+
fireEvent.mouseEnter(
|
|
166
|
+
rendered.container.querySelector(".td-concept-node"),
|
|
167
|
+
);
|
|
168
|
+
fireEvent.mouseLeave(
|
|
169
|
+
rendered.container.querySelector(".td-concept-node"),
|
|
170
|
+
);
|
|
171
|
+
}).not.toThrow();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("matches the snapshot", () => {
|
|
175
|
+
const rendered = render(
|
|
176
|
+
<ConceptNode id="node-1" data={{ label: "Snapshot", isActive: true }} />,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
expect(rendered.container).toMatchSnapshot();
|
|
180
|
+
});
|
|
181
|
+
});
|