feat(FN-3088): add dependency graph highlighting with hover and selection s
The merge adds visual highlighting to the dependency graph plugin. A new `useDependencyChain` hook tracks dependency relationships, while `GraphTaskNode` and `DependencyGraph` gained hover, selection, and highlight visual states backed by new CSS. Tests cover the hook logic, highlighting behavior, a Fusion-Task-Id: FN-3088
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import type { Task } from "@fusion/core";
|
||||
import { DependencyGraph } from "../DependencyGraph";
|
||||
|
||||
vi.mock("@fusion/dashboard/app/components/TaskCard", () => ({
|
||||
TaskCard: ({ task, onOpenDetail }: { task: Task; onOpenDetail: (task: Task) => void }) => (
|
||||
<button data-testid={`task-${task.id}`} onClick={() => onOpenDetail(task)}>{task.id}</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("../useGraphInteraction", () => ({
|
||||
useGraphInteraction: () => ({
|
||||
transform: "translate(0px, 0px) scale(1)",
|
||||
zoom: 1,
|
||||
transitioning: false,
|
||||
zoomIn: vi.fn(),
|
||||
zoomOut: vi.fn(),
|
||||
resetView: vi.fn(),
|
||||
fitToGraph: vi.fn(),
|
||||
onPointerDown: vi.fn(),
|
||||
onPointerMove: vi.fn(),
|
||||
onPointerUp: vi.fn(),
|
||||
onWheelZoom: vi.fn(),
|
||||
handleKeyDown: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
function createTask(id: string, dependencies: string[] = []): Task {
|
||||
return {
|
||||
id,
|
||||
description: id,
|
||||
column: "todo",
|
||||
dependencies,
|
||||
steps: [],
|
||||
currentStep: 0,
|
||||
log: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as Task;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe("DependencyGraph highlighting", () => {
|
||||
const tasks = [createTask("A"), createTask("B", ["A"]), createTask("C", ["B"]), createTask("D")];
|
||||
|
||||
it("highlights chain on hover and returns to neutral on mouse leave", () => {
|
||||
render(<DependencyGraph tasks={tasks} onOpenDetail={vi.fn()} />);
|
||||
|
||||
fireEvent.mouseEnter(screen.getByTestId("graph-task-node-C"));
|
||||
expect(screen.getByTestId("graph-task-node-A").className).toContain("graph-task-node--highlighted");
|
||||
expect(screen.getByTestId("graph-task-node-B").className).toContain("graph-task-node--highlighted");
|
||||
expect(screen.getByTestId("graph-task-node-C").className).toContain("graph-task-node--highlighted");
|
||||
expect(screen.getByTestId("graph-task-node-D").className).toContain("graph-task-node--dimmed");
|
||||
|
||||
fireEvent.mouseLeave(screen.getByTestId("graph-task-node-C"));
|
||||
expect(screen.getByTestId("graph-task-node-A").className).not.toContain("graph-task-node--highlighted");
|
||||
expect(screen.getByTestId("graph-task-node-D").className).not.toContain("graph-task-node--dimmed");
|
||||
});
|
||||
|
||||
it("keeps selection until toggled or pane clicked", () => {
|
||||
render(<DependencyGraph tasks={tasks} onOpenDetail={vi.fn()} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("graph-task-node-B"));
|
||||
expect(screen.getByTestId("graph-task-node-A").className).toContain("graph-task-node--highlighted");
|
||||
|
||||
fireEvent.click(screen.getByTestId("graph-task-node-B"));
|
||||
expect(screen.getByTestId("graph-task-node-A").className).not.toContain("graph-task-node--highlighted");
|
||||
|
||||
fireEvent.click(screen.getByTestId("graph-task-node-B"));
|
||||
fireEvent.click(document.querySelector(".dependency-graph__viewport") as Element);
|
||||
expect(screen.getByTestId("graph-task-node-B").className).not.toContain("graph-task-node--highlighted");
|
||||
});
|
||||
|
||||
it("hover overrides selection and reverts when hover leaves", () => {
|
||||
render(<DependencyGraph tasks={tasks} onOpenDetail={vi.fn()} />);
|
||||
|
||||
fireEvent.click(screen.getByTestId("graph-task-node-B"));
|
||||
fireEvent.mouseEnter(screen.getByTestId("graph-task-node-D"));
|
||||
expect(screen.getByTestId("graph-task-node-D").className).toContain("graph-task-node--highlighted");
|
||||
expect(screen.getByTestId("graph-task-node-A").className).toContain("graph-task-node--dimmed");
|
||||
|
||||
fireEvent.mouseLeave(screen.getByTestId("graph-task-node-D"));
|
||||
expect(screen.getByTestId("graph-task-node-A").className).toContain("graph-task-node--highlighted");
|
||||
});
|
||||
|
||||
it("applies edge dimming/highlighting and preserves click-to-detail", () => {
|
||||
const onOpenDetail = vi.fn();
|
||||
render(<DependencyGraph tasks={tasks} onOpenDetail={onOpenDetail} />);
|
||||
|
||||
fireEvent.mouseEnter(screen.getByTestId("graph-task-node-C"));
|
||||
const edges = screen.getAllByTestId("dependency-edge");
|
||||
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?.className.baseVal || edgeAB?.className).toContain("graph-edge--highlighted");
|
||||
expect(edgeCB?.className.baseVal || edgeCB?.className).toContain("graph-edge--highlighted");
|
||||
|
||||
fireEvent.click(screen.getByTestId("task-C"));
|
||||
expect(onOpenDetail).toHaveBeenCalledWith(expect.objectContaining({ id: "C" }));
|
||||
});
|
||||
|
||||
it("highlights only isolated node with no dependencies", () => {
|
||||
render(<DependencyGraph tasks={[createTask("X")]} onOpenDetail={vi.fn()} />);
|
||||
fireEvent.mouseEnter(screen.getByTestId("graph-task-node-X"));
|
||||
expect(screen.getByTestId("graph-task-node-X").className).toContain("graph-task-node--highlighted");
|
||||
});
|
||||
});
|
||||
@@ -68,6 +68,8 @@ describe("GraphEdges", () => {
|
||||
const dimmed = all.find((edge) => edge.getAttribute("data-edge-id") === "A->C");
|
||||
|
||||
expect(highlighted?.getAttribute("opacity")).toBe("1");
|
||||
expect(dimmed?.getAttribute("opacity")).toBe("0.2");
|
||||
expect(highlighted?.getAttribute("class") ?? "").toContain("graph-edge--highlighted");
|
||||
expect(dimmed?.getAttribute("opacity")).toBe("0.15");
|
||||
expect(dimmed?.getAttribute("class") ?? "").toContain("graph-edge--dimmed");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user