import { afterEach, beforeEach, 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"; const fitToGraph = vi.fn(); vi.mock("@fusion/dashboard/app/components/TaskCard", () => ({ TaskCard: ({ task, onOpenDetail }: { task: Task; onOpenDetail: (task: Task) => void }) => ( ), })); vi.mock("../useGraphInteraction", () => ({ useGraphInteraction: () => ({ transform: "translate(0px, 0px) scale(1)", zoom: 1, transitioning: false, zoomIn: vi.fn(), zoomOut: vi.fn(), resetView: vi.fn(), fitToGraph, onPointerDown: vi.fn(), onPointerMove: vi.fn(), onPointerUp: vi.fn(), onWheelZoom: vi.fn(), handleKeyDown: vi.fn(), }), })); vi.mock("../layout", () => ({ computeAutoLayout: ({ nodes }: { nodes: Array<{ task: { id: string } }> }) => { const map = new Map(); for (const node of nodes) { if (node.task.id === "A") map.set("A", { x: 0, y: 0 }); if (node.task.id === "B") map.set("B", { x: 200, y: 0 }); } return map; }, })); function createStorage() { const store = new Map(); return { getItem: (key: string) => store.get(key) ?? null, setItem: (key: string, value: string) => { store.set(key, value); }, removeItem: (key: string) => { store.delete(key); }, }; } function createTask(id: string, column: Task["column"] = "todo"): Task { return { id, description: id, column, dependencies: [], steps: [], currentStep: 0, log: [] } as Task; } describe("DependencyGraph persistence", () => { afterEach(() => { cleanup(); }); beforeEach(() => { Object.defineProperty(window, "localStorage", { value: createStorage(), configurable: true }); fitToGraph.mockReset(); }); it("persists dragged node position across remount", () => { const { unmount } = render(); const node = screen.getByTestId("graph-task-node-A"); fireEvent.click(node); fireEvent.pointerDown(node, { pointerId: 1, isPrimary: true, clientX: 10, clientY: 10 }); fireEvent.pointerMove(node, { pointerId: 1, isPrimary: true, clientX: 30, clientY: 40 }); fireEvent.pointerUp(node, { pointerId: 1, isPrimary: true, clientX: 30, clientY: 40 }); expect(window.localStorage.getItem("kb:p1:fusion-plugin-dependency-graph:positions")).toContain('"A":{"x":20,"y":30}'); unmount(); render(); expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("left: 20px"); expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("top: 30px"); }); it("merges saved positions with auto-layout for new tasks", () => { window.localStorage.setItem("kb:p1:fusion-plugin-dependency-graph:positions", JSON.stringify({ A: { x: 25, y: 35 } })); render(); expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("left: 25px"); expect(screen.getByTestId("graph-task-node-B").getAttribute("style")).toContain("left: 200px"); }); it("fit to graph clears saved positions and reapplies auto-layout", () => { window.localStorage.setItem("kb:p1:fusion-plugin-dependency-graph:positions", JSON.stringify({ A: { x: 25, y: 35 } })); render(); fireEvent.click(screen.getByRole("button", { name: "Fit to graph" })); expect(window.localStorage.getItem("kb:p1:fusion-plugin-dependency-graph:positions")).toBeNull(); expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("left: 0px"); }); it("switching projects loads project-scoped positions", () => { window.localStorage.setItem("kb:p1:fusion-plugin-dependency-graph:positions", JSON.stringify({ A: { x: 11, y: 22 } })); window.localStorage.setItem("kb:p2:fusion-plugin-dependency-graph:positions", JSON.stringify({ A: { x: 33, y: 44 } })); const { rerender } = render(); expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("left: 11px"); rerender(); expect(screen.getByTestId("graph-task-node-A").getAttribute("style")).toContain("left: 33px"); }); });