Files
grimoire/src/lib/layout-utils.test.ts
Alejandro Gómez a6650ff6e1 fix: type errors
2025-12-18 16:00:56 +01:00

460 lines
13 KiB
TypeScript

import { describe, it, expect } from "vitest";
import type { MosaicNode } from "react-mosaic-component";
import { analyzeLayoutStats, insertWindow } from "./layout-utils";
import type { LayoutConfig } from "@/types/app";
describe("analyzeLayoutStats", () => {
describe("empty and single window layouts", () => {
it("should return zeros for null layout", () => {
const result = analyzeLayoutStats(null);
expect(result).toEqual({
rowSplits: 0,
columnSplits: 0,
depth: 0,
windowCount: 0,
});
});
it("should return single window stats for leaf node", () => {
const result = analyzeLayoutStats("window-1");
expect(result).toEqual({
rowSplits: 0,
columnSplits: 0,
depth: 0,
windowCount: 1,
});
});
});
describe("horizontal splits", () => {
it("should count single horizontal split", () => {
const layout: MosaicNode<string> = {
direction: "row",
first: "window-1",
second: "window-2",
splitPercentage: 50,
};
const result = analyzeLayoutStats(layout);
expect(result).toEqual({
rowSplits: 1,
columnSplits: 0,
depth: 1,
windowCount: 2,
});
});
it("should count nested horizontal splits", () => {
const layout: MosaicNode<string> = {
direction: "row",
first: {
direction: "row",
first: "window-1",
second: "window-2",
splitPercentage: 50,
},
second: "window-3",
splitPercentage: 50,
};
const result = analyzeLayoutStats(layout);
expect(result).toEqual({
rowSplits: 2,
columnSplits: 0,
depth: 2,
windowCount: 3,
});
});
});
describe("vertical splits", () => {
it("should count single vertical split", () => {
const layout: MosaicNode<string> = {
direction: "column",
first: "window-1",
second: "window-2",
splitPercentage: 50,
};
const result = analyzeLayoutStats(layout);
expect(result).toEqual({
rowSplits: 0,
columnSplits: 1,
depth: 1,
windowCount: 2,
});
});
it("should count nested vertical splits", () => {
const layout: MosaicNode<string> = {
direction: "column",
first: {
direction: "column",
first: "window-1",
second: "window-2",
splitPercentage: 50,
},
second: "window-3",
splitPercentage: 50,
};
const result = analyzeLayoutStats(layout);
expect(result).toEqual({
rowSplits: 0,
columnSplits: 2,
depth: 2,
windowCount: 3,
});
});
});
describe("mixed splits", () => {
it("should count mixed horizontal and vertical splits", () => {
const layout: MosaicNode<string> = {
direction: "row",
first: {
direction: "column",
first: "window-1",
second: "window-2",
splitPercentage: 50,
},
second: "window-3",
splitPercentage: 50,
};
const result = analyzeLayoutStats(layout);
expect(result).toEqual({
rowSplits: 1,
columnSplits: 1,
depth: 2,
windowCount: 3,
});
});
it("should handle complex nested mixed splits", () => {
// Layout: row split with column split on left and row split on right
const layout: MosaicNode<string> = {
direction: "row",
first: {
direction: "column",
first: "window-1",
second: "window-2",
splitPercentage: 50,
},
second: {
direction: "row",
first: "window-3",
second: "window-4",
splitPercentage: 50,
},
splitPercentage: 50,
};
const result = analyzeLayoutStats(layout);
expect(result).toEqual({
rowSplits: 2,
columnSplits: 1,
depth: 2,
windowCount: 4,
});
});
it("should handle quad layout (2x2 grid)", () => {
// Quad layout: row split, each side has column split
const layout: MosaicNode<string> = {
direction: "row",
first: {
direction: "column",
first: "window-1",
second: "window-2",
splitPercentage: 50,
},
second: {
direction: "column",
first: "window-3",
second: "window-4",
splitPercentage: 50,
},
splitPercentage: 50,
};
const result = analyzeLayoutStats(layout);
expect(result).toEqual({
rowSplits: 1,
columnSplits: 2,
depth: 2,
windowCount: 4,
});
});
});
describe("depth calculation", () => {
it("should calculate correct depth for unbalanced tree", () => {
// Deep on one side, shallow on other
const layout: MosaicNode<string> = {
direction: "row",
first: {
direction: "row",
first: {
direction: "row",
first: "window-1",
second: "window-2",
splitPercentage: 50,
},
second: "window-3",
splitPercentage: 50,
},
second: "window-4",
splitPercentage: 50,
};
const result = analyzeLayoutStats(layout);
expect(result.depth).toBe(3);
expect(result.windowCount).toBe(4);
});
});
});
describe("insertWindow", () => {
describe("first window insertion", () => {
it("should return window ID for null layout", () => {
const config: LayoutConfig = {
insertionMode: "smart",
splitPercentage: 50,
insertionPosition: "second",
};
const result = insertWindow(null, "new-window", config);
expect(result).toBe("new-window");
});
});
describe("row mode insertion", () => {
it("should create horizontal split in row mode", () => {
const config: LayoutConfig = {
insertionMode: "row",
splitPercentage: 50,
insertionPosition: "second",
};
const result = insertWindow("window-1", "window-2", config);
expect(result).toEqual({
direction: "row",
first: "window-1",
second: "window-2",
splitPercentage: 50,
});
});
it("should respect custom split percentage in row mode", () => {
const config: LayoutConfig = {
insertionMode: "row",
splitPercentage: 70,
insertionPosition: "second",
};
const result = insertWindow("window-1", "window-2", config);
expect(result).toEqual({
direction: "row",
first: "window-1",
second: "window-2",
splitPercentage: 70,
});
});
it("should place new window on left when position is first", () => {
const config: LayoutConfig = {
insertionMode: "row",
splitPercentage: 50,
insertionPosition: "first",
};
const result = insertWindow("window-1", "window-2", config);
expect(result).toEqual({
direction: "row",
first: "window-2", // New window on left
second: "window-1",
splitPercentage: 50,
});
});
});
describe("column mode insertion", () => {
it("should create vertical split in column mode", () => {
const config: LayoutConfig = {
insertionMode: "column",
splitPercentage: 50,
insertionPosition: "second",
};
const result = insertWindow("window-1", "window-2", config);
expect(result).toEqual({
direction: "column",
first: "window-1",
second: "window-2",
splitPercentage: 50,
});
});
it("should respect custom split percentage in column mode", () => {
const config: LayoutConfig = {
insertionMode: "column",
splitPercentage: 30,
insertionPosition: "second",
};
const result = insertWindow("window-1", "window-2", config);
expect(result).toEqual({
direction: "column",
first: "window-1",
second: "window-2",
splitPercentage: 30,
});
});
it("should place new window on top when position is first", () => {
const config: LayoutConfig = {
insertionMode: "column",
splitPercentage: 50,
insertionPosition: "first",
};
const result = insertWindow("window-1", "window-2", config);
expect(result).toEqual({
direction: "column",
first: "window-2", // New window on top
second: "window-1",
splitPercentage: 50,
});
});
});
describe("smart mode insertion", () => {
it("should use smart direction for single window", () => {
const config: LayoutConfig = {
insertionMode: "smart",
splitPercentage: 50,
insertionPosition: "second",
};
const result = insertWindow("window-1", "window-2", config);
expect(result).toEqual({
direction: "row", // Smart defaults to row for first split
first: "window-1",
second: "window-2",
splitPercentage: 50,
});
});
it("should balance horizontal splits by adding vertical", () => {
// Existing layout: 2 horizontal splits
const existingLayout: MosaicNode<string> = {
direction: "row",
first: {
direction: "row",
first: "window-1",
second: "window-2",
splitPercentage: 50,
},
second: "window-3",
splitPercentage: 50,
};
const config: LayoutConfig = {
insertionMode: "smart",
splitPercentage: 50,
insertionPosition: "second",
};
const result = insertWindow(existingLayout, "window-4", config);
// NEW IMPROVED BEHAVIOR: Splits shallowest leaf (window-3 at depth 1)
// Parent direction is row, so rotates to column for the split
expect(result).toHaveProperty("direction", "row");
// window-3 should be replaced with [column: window-3 | window-4]
const secondNode = (result as any).second;
expect(secondNode).toHaveProperty("direction", "column");
expect(secondNode).toHaveProperty("first", "window-3");
expect(secondNode).toHaveProperty("second", "window-4");
});
it("should balance vertical splits by adding horizontal", () => {
// Existing layout: 2 vertical splits
const existingLayout: MosaicNode<string> = {
direction: "column",
first: {
direction: "column",
first: "window-1",
second: "window-2",
splitPercentage: 50,
},
second: "window-3",
splitPercentage: 50,
};
const config: LayoutConfig = {
insertionMode: "smart",
splitPercentage: 50,
insertionPosition: "second",
};
const result = insertWindow(existingLayout, "window-4", config);
// NEW IMPROVED BEHAVIOR: Splits shallowest leaf (window-3 at depth 1)
// Parent direction is column, so rotates to row for the split
expect(result).toHaveProperty("direction", "column");
// window-3 should be replaced with [row: window-3 | window-4]
const secondNode = (result as any).second;
expect(secondNode).toHaveProperty("direction", "row");
expect(secondNode).toHaveProperty("first", "window-3");
expect(secondNode).toHaveProperty("second", "window-4");
});
});
describe("complex layout insertion", () => {
it("should handle insertion into complex nested layout", () => {
// Quad layout (1 row, 2 column splits)
const quadLayout: MosaicNode<string> = {
direction: "row",
first: {
direction: "column",
first: "window-1",
second: "window-2",
splitPercentage: 50,
},
second: {
direction: "column",
first: "window-3",
second: "window-4",
splitPercentage: 50,
},
splitPercentage: 50,
};
const config: LayoutConfig = {
insertionMode: "smart",
splitPercentage: 50,
insertionPosition: "second",
};
const result = insertWindow(quadLayout, "window-5", config);
// NEW IMPROVED BEHAVIOR: All 4 windows at depth 2 (equal depth)
// Algorithm picks first shallowest leaf (window-1)
// Parent is column, so rotates to row for the split
expect(result).toHaveProperty("direction", "row");
const firstNode = (result as any).first;
expect(firstNode).toHaveProperty("direction", "column");
// window-1 should be replaced with [row: window-1 | window-5]
const window1Split = firstNode.first;
expect(window1Split).toHaveProperty("direction", "row");
expect(window1Split).toHaveProperty("first", "window-1");
expect(window1Split).toHaveProperty("second", "window-5");
});
});
describe("split percentage edge cases", () => {
it("should handle minimum split percentage", () => {
const config: LayoutConfig = {
insertionMode: "row",
splitPercentage: 10,
insertionPosition: "second",
};
const result = insertWindow("window-1", "window-2", config);
expect(result).toHaveProperty("splitPercentage", 10);
});
it("should handle maximum split percentage", () => {
const config: LayoutConfig = {
insertionMode: "row",
splitPercentage: 90,
insertionPosition: "second",
};
const result = insertWindow("window-1", "window-2", config);
expect(result).toHaveProperty("splitPercentage", 90);
});
});
});