Files
turbostarter/apps/web/src/modules/diagram/stores/useGraphStore.test.ts
Alejandro Gutiérrez 5033109656 feat: implement Story 2.1 — canvas workspace with @xyflow/react and unified graph model
Replace the placeholder diagram editor with a professional Studio layout featuring
an interactive @xyflow/react canvas, unified graph data model with bidirectional
converters, Zustand state management, and oklch design tokens. Includes 25 unit
tests for converters and store, with all code review fixes applied.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 02:07:59 +00:00

125 lines
3.7 KiB
TypeScript

import { describe, it, expect, beforeEach } from "vitest";
import { useGraphStore } from "./useGraphStore";
import type { Node, Edge } from "@xyflow/react";
const makeNode = (id: string, label = "Node"): Node => ({
id,
type: "default",
position: { x: 0, y: 0 },
data: { label },
});
const makeEdge = (id: string, source: string, target: string): Edge => ({
id,
source,
target,
});
describe("useGraphStore", () => {
beforeEach(() => {
useGraphStore.setState({
nodes: [],
edges: [],
viewport: { x: 0, y: 0, zoom: 1 },
nodeCount: 0,
zoomLevel: 100,
});
});
describe("initializeFromGraphData", () => {
it("should set nodes, edges, and nodeCount", () => {
const nodes = [makeNode("n1"), makeNode("n2")];
const edges = [makeEdge("e1", "n1", "n2")];
useGraphStore.getState().initializeFromGraphData(nodes, edges);
expect(useGraphStore.getState().nodes).toHaveLength(2);
expect(useGraphStore.getState().edges).toHaveLength(1);
expect(useGraphStore.getState().nodeCount).toBe(2);
});
});
describe("setNodes", () => {
it("should replace nodes and update nodeCount", () => {
useGraphStore.getState().setNodes([makeNode("a"), makeNode("b"), makeNode("c")]);
expect(useGraphStore.getState().nodes).toHaveLength(3);
expect(useGraphStore.getState().nodeCount).toBe(3);
});
it("should handle empty array", () => {
useGraphStore.getState().setNodes([makeNode("x")]);
useGraphStore.getState().setNodes([]);
expect(useGraphStore.getState().nodes).toEqual([]);
expect(useGraphStore.getState().nodeCount).toBe(0);
});
});
describe("setEdges", () => {
it("should replace edges", () => {
const edges = [makeEdge("e1", "a", "b"), makeEdge("e2", "b", "c")];
useGraphStore.getState().setEdges(edges);
expect(useGraphStore.getState().edges).toHaveLength(2);
});
});
describe("onNodesChange", () => {
it("should apply node position changes", () => {
useGraphStore.getState().setNodes([makeNode("n1")]);
useGraphStore.getState().onNodesChange([
{
type: "position",
id: "n1",
position: { x: 100, y: 200 },
},
]);
const updatedNode = useGraphStore.getState().nodes[0];
expect(updatedNode!.position).toEqual({ x: 100, y: 200 });
});
it("should update nodeCount when nodes are removed", () => {
useGraphStore.getState().setNodes([makeNode("n1"), makeNode("n2")]);
expect(useGraphStore.getState().nodeCount).toBe(2);
useGraphStore.getState().onNodesChange([
{ type: "remove", id: "n1" },
]);
expect(useGraphStore.getState().nodeCount).toBe(1);
});
});
describe("onEdgesChange", () => {
it("should apply edge removal", () => {
useGraphStore.getState().setEdges([makeEdge("e1", "a", "b")]);
useGraphStore.getState().onEdgesChange([
{ type: "remove", id: "e1" },
]);
expect(useGraphStore.getState().edges).toHaveLength(0);
});
});
describe("onViewportChange", () => {
it("should update viewport and zoomLevel", () => {
useGraphStore.getState().onViewportChange({ x: 50, y: 50, zoom: 1.5 });
expect(useGraphStore.getState().viewport).toEqual({ x: 50, y: 50, zoom: 1.5 });
expect(useGraphStore.getState().zoomLevel).toBe(150);
});
it("should round zoomLevel to nearest integer", () => {
useGraphStore.getState().onViewportChange({ x: 0, y: 0, zoom: 0.333 });
expect(useGraphStore.getState().zoomLevel).toBe(33);
});
it("should handle zoom at 100%", () => {
useGraphStore.getState().onViewportChange({ x: 0, y: 0, zoom: 1.0 });
expect(useGraphStore.getState().zoomLevel).toBe(100);
});
});
});