diff --git a/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.highlighting.test.tsx b/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.highlighting.test.tsx index cb164b21df..472f8fe265 100644 --- a/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.highlighting.test.tsx +++ b/plugins/fusion-plugin-dependency-graph/src/__tests__/DependencyGraph.highlighting.test.tsx @@ -41,8 +41,22 @@ function createTask(id: string, dependencies: string[] = []): Task { } as Task; } +function setViewportDimensions(width: number, height: number) { + Object.defineProperty(window, "innerWidth", { configurable: true, value: width }); + Object.defineProperty(window, "innerHeight", { configurable: true, value: height }); +} + +function expectEdgePaintsViaResolvableStyle(edge: SVGElement, expectedStroke = "var(--border)", expectedStrokeWidth = "var(--btn-border-width)") { + expect(edge.getAttribute("stroke")).not.toBe(expectedStroke); + expect(edge.getAttribute("stroke-width")).not.toBe(expectedStrokeWidth); + expect(edge.getAttribute("strokeWidth")).not.toBe(expectedStrokeWidth); + expect(edge.style.stroke).toBe(expectedStroke); + expect(edge.style.strokeWidth).toBe(expectedStrokeWidth); +} + afterEach(() => { cleanup(); + setViewportDimensions(1024, 768); }); describe("DependencyGraph highlighting", () => { @@ -97,14 +111,39 @@ describe("DependencyGraph highlighting", () => { const edgeAB = edges.find((edge) => edge.getAttribute("data-edge-id") === "B->A"); const edgeCB = edges.find((edge) => edge.getAttribute("data-edge-id") === "C->B"); + expect(edgeAB).toBeDefined(); expect(edgeAB?.className.baseVal || edgeAB?.className).toContain("graph-edge--highlighted"); + expectEdgePaintsViaResolvableStyle(edgeAB as SVGElement, "var(--todo)", "var(--space-xs)"); + expect(edgeCB).toBeDefined(); expect(edgeCB?.className.baseVal || edgeCB?.className).toContain("graph-edge--highlighted"); + expectEdgePaintsViaResolvableStyle(edgeCB as SVGElement, "var(--todo)", "var(--space-xs)"); fireEvent.doubleClick(screen.getByTestId("graph-task-node-C")); expect(onOpenDetail).toHaveBeenCalledTimes(1); expect(onOpenDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "C" })); }); + it.each([ + ["vertical desktop", 1200, 800], + ["horizontal mobile", 390, 844], + ] as const)("renders dependency chain edges with resolvable stroke in %s orientation", (_label, width, height) => { + setViewportDimensions(width, height); + render(); + + const edges = screen.getAllByTestId("dependency-edge"); + expect(edges.map((edge) => edge.getAttribute("data-edge-id")).sort()).toEqual(["B->A", "C->B"]); + for (const edge of edges) { + expectEdgePaintsViaResolvableStyle(edge); + } + }); + + it("does not render an edge when a dependency target is filtered out", () => { + const doneDependency = { ...createTask("Done"), column: "done" as const }; + render(); + + expect(screen.queryAllByTestId("dependency-edge")).toHaveLength(0); + }); + it("highlights only isolated node with no dependencies", () => { render(); fireEvent.mouseEnter(screen.getByTestId("graph-task-node-X")); diff --git a/plugins/fusion-plugin-dependency-graph/src/__tests__/edges.test.tsx b/plugins/fusion-plugin-dependency-graph/src/__tests__/edges.test.tsx index b2e655e16e..d8a1eccfb9 100644 --- a/plugins/fusion-plugin-dependency-graph/src/__tests__/edges.test.tsx +++ b/plugins/fusion-plugin-dependency-graph/src/__tests__/edges.test.tsx @@ -19,6 +19,14 @@ function renderEdges(edges: GraphEdge[], highlightedEdgeIds?: Set) { ); } +function expectEdgePaintsViaResolvableStyle(edge: SVGElement, expectedStroke: string, expectedStrokeWidth: string) { + expect(edge.getAttribute("stroke")).not.toBe(expectedStroke); + expect(edge.getAttribute("stroke-width")).not.toBe(expectedStrokeWidth); + expect(edge.getAttribute("strokeWidth")).not.toBe(expectedStrokeWidth); + expect(edge.style.stroke).toBe(expectedStroke); + expect(edge.style.strokeWidth).toBe(expectedStrokeWidth); +} + describe("GraphEdges", () => { afterEach(() => { cleanup(); @@ -26,8 +34,22 @@ describe("GraphEdges", () => { it("renders single edge", () => { renderEdges([{ source: "A", target: "B" }]); const edge = screen.getAllByTestId("dependency-edge")[0]; - expect(edge.getAttribute("opacity")).toBe("1"); - expect(edge.getAttribute("stroke")).toBe("var(--border)"); + expect(edge.getAttribute("opacity")).toBeNull(); + expectEdgePaintsViaResolvableStyle(edge, "var(--border)", "var(--btn-border-width)"); + }); + + it("renders no edge paths when there are zero edges", () => { + renderEdges([]); + expect(screen.queryAllByTestId("dependency-edge")).toHaveLength(0); + }); + + it("renders arrowhead fill through resolvable style instead of a presentation attribute", () => { + const { container } = renderEdges([{ source: "A", target: "B" }]); + const arrowheadPath = container.querySelector("#dependency-graph-arrowhead path") as SVGPathElement | null; + + expect(arrowheadPath).not.toBeNull(); + expect(arrowheadPath?.getAttribute("fill")).not.toBe("var(--border)"); + expect(arrowheadPath?.style.fill).toBe("var(--border)"); }); it("renders multiple edges", () => { @@ -67,9 +89,13 @@ describe("GraphEdges", () => { const highlighted = all.find((edge) => edge.getAttribute("data-edge-id") === "A->B"); const dimmed = all.find((edge) => edge.getAttribute("data-edge-id") === "A->C"); - expect(highlighted?.getAttribute("opacity")).toBe("1"); + expect(highlighted).toBeDefined(); + expect(highlighted?.style.opacity).toBe("1"); + expectEdgePaintsViaResolvableStyle(highlighted as SVGElement, "var(--todo)", "var(--space-xs)"); expect(highlighted?.getAttribute("class") ?? "").toContain("graph-edge--highlighted"); - expect(dimmed?.getAttribute("opacity")).toBe("0.15"); + expect(dimmed).toBeDefined(); + expect(dimmed?.style.opacity).toBe("0.15"); + expectEdgePaintsViaResolvableStyle(dimmed as SVGElement, "var(--border)", "var(--btn-border-width)"); expect(dimmed?.getAttribute("class") ?? "").toContain("graph-edge--dimmed"); }); }); diff --git a/plugins/fusion-plugin-dependency-graph/src/edges.tsx b/plugins/fusion-plugin-dependency-graph/src/edges.tsx index fafaa97551..c260abb9dc 100644 --- a/plugins/fusion-plugin-dependency-graph/src/edges.tsx +++ b/plugins/fusion-plugin-dependency-graph/src/edges.tsx @@ -13,6 +13,11 @@ interface GraphEdgesProps { const DEFAULT_NODE_WIDTH = 280; const DEFAULT_NODE_HEIGHT = 100; +/** + * FNXC:DependencyGraphEdges 2026-06-19-08:59: + * Browser SVG presentation attributes do not resolve CSS custom properties, so dependency edge theme paint and widths must travel through real CSS via inline style or classes. + * Keep only literal SVG-safe values such as fill="none" as presentation attributes so dependency connector strokes and arrowheads remain visible across default, highlighted, and dimmed states. + */ export function GraphEdges({ edges, positions, @@ -35,7 +40,7 @@ export function GraphEdges({ orient="auto" markerUnits="strokeWidth" > - + {edges.map((edge) => { @@ -59,11 +64,13 @@ export function GraphEdges({ className={`dependency-graph-edge${isActiveHighlight ? " graph-edge--highlighted" : ""}${hasHighlights && !isActiveHighlight ? " graph-edge--dimmed" : ""}`} d={`M ${x1} ${y1} C ${x1} ${controlY}, ${x2} ${controlY}, ${x2} ${y2}`} fill="none" - stroke={isActiveHighlight ? "var(--todo)" : "var(--border)"} - strokeWidth={isActiveHighlight ? "var(--space-xs)" : "var(--btn-border-width)"} - opacity={hasHighlights && !isActiveHighlight ? 0.15 : 1} markerEnd="url(#dependency-graph-arrowhead)" - style={{ transition: "opacity var(--transition-fast), stroke var(--transition-fast), stroke-width var(--transition-fast)" }} + style={{ + opacity: hasHighlights && !isActiveHighlight ? 0.15 : 1, + stroke: isActiveHighlight ? "var(--todo)" : "var(--border)", + strokeWidth: isActiveHighlight ? "var(--space-xs)" : "var(--btn-border-width)", + transition: "opacity var(--transition-fast), stroke var(--transition-fast), stroke-width var(--transition-fast)", + }} /> ); })}