Files
2026-03-23 16:40:53 -05:00

224 lines
7.6 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { parseLayoutToken, tokenToAlgorithm } from "../../src/layout/tokens";
import { createHorizontalAlgorithm } from "../../src/layout/algorithms/horizontal";
import { createVerticalAlgorithm } from "../../src/layout/algorithms/vertical";
import type { LayoutNode, LayoutInput } from "../../src/layout/types";
function node(id: string, width = 100, height = 60): LayoutNode {
return { id, width, height };
}
function input(nodes: LayoutNode[]): LayoutInput {
return { nodes, edges: [] };
}
// ---------------------------------------------------------------------------
// parseLayoutToken
// ---------------------------------------------------------------------------
describe("parseLayoutToken", () => {
it("parses [HORIZONTAL] from group title", () => {
const result = parseLayoutToken("My Group [HORIZONTAL]");
expect(result).toEqual({ mode: "horizontal" });
});
it("parses [vertical] case-insensitively", () => {
const result = parseLayoutToken("stuff [vertical] more");
expect(result).toEqual({ mode: "vertical" });
});
it("parses [2ROW] as grid with 2 rows", () => {
const result = parseLayoutToken("[2ROW]");
expect(result).toEqual({ mode: "grid", count: 2, dimension: "row" });
});
it("parses [3col] case-insensitively as grid with 3 columns", () => {
const result = parseLayoutToken("Title [3col]");
expect(result).toEqual({ mode: "grid", count: 3, dimension: "col" });
});
it("treats [1ROW] as horizontal alias", () => {
const result = parseLayoutToken("[1ROW]");
expect(result).toEqual({ mode: "horizontal" });
});
it("treats [1COL] as vertical alias", () => {
const result = parseLayoutToken("[1COL]");
expect(result).toEqual({ mode: "vertical" });
});
it("returns null when no token present", () => {
const result = parseLayoutToken("No token here");
expect(result).toBeNull();
});
it("returns null for [10ROW] (only 1-9 supported)", () => {
const result = parseLayoutToken("[10ROW]");
expect(result).toBeNull();
});
it("returns null for [0ROW] (0 not valid)", () => {
const result = parseLayoutToken("[0ROW]");
expect(result).toBeNull();
});
});
// ---------------------------------------------------------------------------
// Algorithm behavior: horizontal
// ---------------------------------------------------------------------------
describe("horizontal algorithm", () => {
it("places 3 nodes left-to-right with Y centered", () => {
const nodes = [node("A", 100, 40), node("B", 80, 60), node("C", 120, 20)];
const algo = createHorizontalAlgorithm(40);
const result = algo.layout(input(nodes));
const posA = result.positions.get("A")!;
const posB = result.positions.get("B")!;
const posC = result.positions.get("C")!;
// Max height is 60 (node B)
// A: y = 60/2 - 40/2 = 10
// B: y = 60/2 - 60/2 = 0
// C: y = 60/2 - 20/2 = 20
expect(posA).toEqual({ x: 0, y: 10 });
expect(posB).toEqual({ x: 140, y: 0 }); // 0 + 100 + 40
expect(posC).toEqual({ x: 260, y: 20 }); // 140 + 80 + 40
});
it("returns empty positions for empty input", () => {
const algo = createHorizontalAlgorithm();
const result = algo.layout(input([]));
expect(result.positions.size).toBe(0);
});
it("places single node at origin", () => {
const algo = createHorizontalAlgorithm();
const result = algo.layout(input([node("A", 100, 60)]));
expect(result.positions.get("A")).toEqual({ x: 0, y: 0 });
});
});
// ---------------------------------------------------------------------------
// Algorithm behavior: vertical
// ---------------------------------------------------------------------------
describe("vertical algorithm", () => {
it("places 3 nodes top-to-bottom with X centered", () => {
const nodes = [node("A", 80, 40), node("B", 120, 60), node("C", 60, 30)];
const algo = createVerticalAlgorithm(40);
const result = algo.layout(input(nodes));
const posA = result.positions.get("A")!;
const posB = result.positions.get("B")!;
const posC = result.positions.get("C")!;
// Max width is 120 (node B)
// A: x = 120/2 - 80/2 = 20
// B: x = 120/2 - 120/2 = 0
// C: x = 120/2 - 60/2 = 30
expect(posA).toEqual({ x: 20, y: 0 });
expect(posB).toEqual({ x: 0, y: 80 }); // 0 + 40 + 40
expect(posC).toEqual({ x: 30, y: 180 }); // 80 + 60 + 40
});
it("returns empty positions for empty input", () => {
const algo = createVerticalAlgorithm();
const result = algo.layout(input([]));
expect(result.positions.size).toBe(0);
});
it("places single node at origin", () => {
const algo = createVerticalAlgorithm();
const result = algo.layout(input([node("A", 100, 60)]));
expect(result.positions.get("A")).toEqual({ x: 0, y: 0 });
});
});
// ---------------------------------------------------------------------------
// Algorithm behavior: grid
// ---------------------------------------------------------------------------
describe("grid algorithm via tokenToAlgorithm", () => {
it("[2ROW] with 6 nodes: 2 rows x 3 columns", () => {
const token = parseLayoutToken("[2ROW]")!;
const algo = tokenToAlgorithm(token, 40);
const nodes = [
node("A", 100, 60), node("B", 100, 60), node("C", 100, 60),
node("D", 100, 60), node("E", 100, 60), node("F", 100, 60),
];
const result = algo.layout(input(nodes));
expect(result.positions.size).toBe(6);
// Row 0: A, B, C
// Row 1: D, E, F
const posA = result.positions.get("A")!;
const posB = result.positions.get("B")!;
const posD = result.positions.get("D")!;
// Same-size nodes, gap=40: cellWidth=140, cellHeight=100
expect(posA).toEqual({ x: 0, y: 0 });
expect(posB).toEqual({ x: 140, y: 0 });
expect(posD).toEqual({ x: 0, y: 100 });
});
it("[3COL] with 6 nodes: 2 rows x 3 columns", () => {
const token = parseLayoutToken("[3COL]")!;
const algo = tokenToAlgorithm(token, 40);
const nodes = [
node("A", 100, 60), node("B", 100, 60), node("C", 100, 60),
node("D", 100, 60), node("E", 100, 60), node("F", 100, 60),
];
const result = algo.layout(input(nodes));
expect(result.positions.size).toBe(6);
// Row 0: A, B, C
// Row 1: D, E, F
const posA = result.positions.get("A")!;
const posD = result.positions.get("D")!;
expect(posA).toEqual({ x: 0, y: 0 });
expect(posD).toEqual({ x: 0, y: 100 });
});
it("[2ROW] with 5 nodes: row 0 has 3, row 1 has 2", () => {
const token = parseLayoutToken("[2ROW]")!;
const algo = tokenToAlgorithm(token, 40);
const nodes = [
node("A", 100, 60), node("B", 100, 60), node("C", 100, 60),
node("D", 100, 60), node("E", 100, 60),
];
const result = algo.layout(input(nodes));
expect(result.positions.size).toBe(5);
// cols = ceil(5/2) = 3
// Row 0: A(0,0), B(0,1), C(0,2)
// Row 1: D(1,0), E(1,1)
const posC = result.positions.get("C")!;
const posD = result.positions.get("D")!;
const posE = result.positions.get("E")!;
expect(posC.y).toBe(0); // row 0
expect(posD.y).toBe(100); // row 1
expect(posE.y).toBe(100); // row 1
expect(posE.x).toBe(140); // col 1
});
it("empty input returns empty positions", () => {
const token = parseLayoutToken("[2ROW]")!;
const algo = tokenToAlgorithm(token, 40);
const result = algo.layout(input([]));
expect(result.positions.size).toBe(0);
});
it("single node placed at origin", () => {
const token = parseLayoutToken("[3COL]")!;
const algo = tokenToAlgorithm(token, 40);
const result = algo.layout(input([node("A", 100, 60)]));
expect(result.positions.get("A")).toEqual({ x: 0, y: 0 });
});
});