import { describe, it, expect } from "vitest" import type { KpWithRelations } from "./types" import { computeGraphLayout } from "./graph-layout" describe("textbooks/graph-layout", () => { const makeKp = ( id: string, parentId: string | null = null, prerequisiteIds: string[] = [], ): KpWithRelations => ({ id, name: `KP-${id}`, description: null, parentId, chapterId: "c1", level: 1, order: 0, chapterTitle: "Chapter 1", questionCount: 0, prerequisiteIds, }) describe("computeGraphLayout", () => { it("should return empty layout for empty input", () => { const layout = computeGraphLayout([]) expect(layout.nodes).toEqual([]) expect(layout.edges).toEqual([]) }) it("should place single node", () => { const layout = computeGraphLayout([makeKp("1")]) expect(layout.nodes).toHaveLength(1) expect(layout.nodes[0].id).toBe("1") }) it("should generate parent-child edge", () => { const layout = computeGraphLayout([makeKp("1"), makeKp("2", "1")]) expect(layout.nodes).toHaveLength(2) const parentEdge = layout.edges.find((e) => e.id === "parent-1-2") expect(parentEdge).toBeDefined() }) it("should generate prerequisite edge", () => { const layout = computeGraphLayout([ makeKp("1"), makeKp("2", null, ["1"]), ]) const prereqEdge = layout.edges.find((e) => e.id === "prereq-1-2") expect(prereqEdge).toBeDefined() }) it("should assign positions to all nodes", () => { const layout = computeGraphLayout([ makeKp("1"), makeKp("2", "1"), makeKp("3", "1"), ]) for (const node of layout.nodes) { expect(node.position.x).toBeGreaterThanOrEqual(0) expect(node.position.y).toBeGreaterThanOrEqual(0) } }) it("should not create edge for non-existent parentId", () => { const layout = computeGraphLayout([makeKp("1", "nonexistent")]) expect(layout.nodes).toHaveLength(1) expect(layout.edges).toHaveLength(0) }) it("should not create edge for non-existent prerequisiteIds", () => { const layout = computeGraphLayout([makeKp("1", null, ["nonexistent"])]) expect(layout.nodes).toHaveLength(1) expect(layout.edges).toHaveLength(0) }) it("should generate both parent and prerequisite edges for one node", () => { const layout = computeGraphLayout([ makeKp("1"), makeKp("2"), makeKp("3", "1", ["2"]), ]) const parentEdge = layout.edges.find((e) => e.id === "parent-1-3") const prereqEdge = layout.edges.find((e) => e.id === "prereq-2-3") expect(parentEdge).toBeDefined() expect(prereqEdge).toBeDefined() expect(layout.edges).toHaveLength(2) }) it("should return positive width/height for non-empty graph", () => { const layout = computeGraphLayout([ makeKp("1"), makeKp("2", "1"), makeKp("3", "1"), ]) expect(layout.width).toBeGreaterThan(0) expect(layout.height).toBeGreaterThan(0) }) it("should handle deeply nested parent chain (boundary)", () => { const depth = 20 const kps: KpWithRelations[] = [makeKp("0")] for (let i = 1; i < depth; i++) { kps.push(makeKp(String(i), String(i - 1))) } const layout = computeGraphLayout(kps) expect(layout.nodes).toHaveLength(depth) // 应生成 depth-1 条 parent 边 const parentEdges = layout.edges.filter((e) => e.id.startsWith("parent-")) expect(parentEdges).toHaveLength(depth - 1) }) it("should handle many nodes (boundary, 50 nodes)", () => { const count = 50 const kps: KpWithRelations[] = [] for (let i = 0; i < count; i++) { kps.push(makeKp(`n${i}`)) } const layout = computeGraphLayout(kps) expect(layout.nodes).toHaveLength(count) expect(layout.edges).toHaveLength(0) // 所有节点都应有有效位置 for (const node of layout.nodes) { expect(Number.isFinite(node.position.x)).toBe(true) expect(Number.isFinite(node.position.y)).toBe(true) } }) }) })