Files
grimoire/src/lib/layout-presets.test.ts
2025-12-18 13:47:40 +01:00

341 lines
9.8 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
collectWindowIds,
applyPresetToLayout,
balanceLayout,
BUILT_IN_PRESETS,
} from "./layout-presets";
import type { MosaicNode } from "react-mosaic-component";
describe("layout-presets", () => {
describe("collectWindowIds", () => {
it("collects IDs from single window", () => {
expect(collectWindowIds("w1")).toEqual(["w1"]);
});
it("collects IDs from binary tree", () => {
const layout: MosaicNode<string> = {
direction: "row",
first: "w1",
second: "w2",
splitPercentage: 50,
};
expect(collectWindowIds(layout)).toEqual(["w1", "w2"]);
});
it("collects IDs in depth-first order", () => {
const layout: MosaicNode<string> = {
direction: "row",
first: {
direction: "column",
first: "w1",
second: "w2",
splitPercentage: 50,
},
second: {
direction: "column",
first: "w3",
second: "w4",
splitPercentage: 50,
},
splitPercentage: 50,
};
expect(collectWindowIds(layout)).toEqual(["w1", "w2", "w3", "w4"]);
});
});
describe("grid preset", () => {
const gridPreset = BUILT_IN_PRESETS.grid;
it("handles 2 windows (1x2 grid)", () => {
const layout = gridPreset.generate(["w1", "w2"]);
const windowIds = collectWindowIds(layout);
expect(windowIds).toEqual(["w1", "w2"]);
expect(windowIds.length).toBe(2);
});
it("handles 3 windows (1x3 single row)", () => {
const layout = gridPreset.generate(["w1", "w2", "w3"]);
const windowIds = collectWindowIds(layout);
expect(windowIds).toEqual(["w1", "w2", "w3"]);
expect(windowIds.length).toBe(3);
});
it("handles 4 windows (2x2 perfect grid)", () => {
const layout = gridPreset.generate(["w1", "w2", "w3", "w4"]);
const windowIds = collectWindowIds(layout);
expect(windowIds).toEqual(["w1", "w2", "w3", "w4"]);
expect(windowIds.length).toBe(4);
});
it("handles 5 windows (2x3 with expanded last row)", () => {
const layout = gridPreset.generate(["w1", "w2", "w3", "w4", "w5"]);
const windowIds = collectWindowIds(layout);
expect(windowIds).toEqual(["w1", "w2", "w3", "w4", "w5"]);
expect(windowIds.length).toBe(5);
// Should create 2 rows: [w1,w2,w3] and [w4,w5]
// Last row windows expand to fill space (no empty slots)
});
it("handles 7 windows (2x4 with expanded last row)", () => {
const layout = gridPreset.generate([
"w1",
"w2",
"w3",
"w4",
"w5",
"w6",
"w7",
]);
const windowIds = collectWindowIds(layout);
expect(windowIds).toEqual(["w1", "w2", "w3", "w4", "w5", "w6", "w7"]);
expect(windowIds.length).toBe(7);
// Should create 2 rows: [w1,w2,w3,w4] and [w5,w6,w7]
});
it("handles 9 windows (3x3 perfect grid)", () => {
const layout = gridPreset.generate([
"w1",
"w2",
"w3",
"w4",
"w5",
"w6",
"w7",
"w8",
"w9",
]);
const windowIds = collectWindowIds(layout);
expect(windowIds.length).toBe(9);
});
it("handles 11 windows (3x4 with expanded last row)", () => {
const layout = gridPreset.generate([
"w1",
"w2",
"w3",
"w4",
"w5",
"w6",
"w7",
"w8",
"w9",
"w10",
"w11",
]);
const windowIds = collectWindowIds(layout);
expect(windowIds.length).toBe(11);
// Should create 3 rows: [w1-4], [w5-8], [w9-11]
});
it("preserves window order in depth-first traversal", () => {
const original = ["a", "b", "c", "d", "e"];
const layout = gridPreset.generate(original);
const collected = collectWindowIds(layout);
expect(collected).toEqual(original);
});
});
describe("side-by-side preset", () => {
const sideBySidePreset = BUILT_IN_PRESETS["side-by-side"];
it("handles 2 windows (50/50)", () => {
const layout = sideBySidePreset.generate(["w1", "w2"]);
const windowIds = collectWindowIds(layout);
expect(windowIds).toEqual(["w1", "w2"]);
});
it("handles 3 windows (equal splits)", () => {
const layout = sideBySidePreset.generate(["w1", "w2", "w3"]);
const windowIds = collectWindowIds(layout);
expect(windowIds).toEqual(["w1", "w2", "w3"]);
});
it("handles 4 windows (max allowed)", () => {
const layout = sideBySidePreset.generate(["w1", "w2", "w3", "w4"]);
const windowIds = collectWindowIds(layout);
expect(windowIds).toEqual(["w1", "w2", "w3", "w4"]);
});
it("throws error for 5+ windows", () => {
expect(() =>
sideBySidePreset.generate(["w1", "w2", "w3", "w4", "w5"])
).toThrow("maximum 4 windows");
});
});
describe("main-sidebar preset", () => {
const mainSidebarPreset = BUILT_IN_PRESETS["main-sidebar"];
it("handles 2 windows (main + 1 sidebar)", () => {
const layout = mainSidebarPreset.generate(["w1", "w2"]);
const windowIds = collectWindowIds(layout);
expect(windowIds).toEqual(["w1", "w2"]);
});
it("handles 5 windows (main + 4 sidebars)", () => {
const layout = mainSidebarPreset.generate([
"w1",
"w2",
"w3",
"w4",
"w5",
]);
const windowIds = collectWindowIds(layout);
expect(windowIds).toEqual(["w1", "w2", "w3", "w4", "w5"]);
// First window is main, rest are stacked vertically
});
it("handles 10 windows (main + 9 sidebars)", () => {
const windows = Array.from({ length: 10 }, (_, i) => `w${i + 1}`);
const layout = mainSidebarPreset.generate(windows);
const windowIds = collectWindowIds(layout);
expect(windowIds).toEqual(windows);
});
});
describe("balanceLayout", () => {
it("returns null for null layout", () => {
expect(balanceLayout(null)).toBeNull();
});
it("returns single window unchanged", () => {
expect(balanceLayout("w1")).toBe("w1");
});
it("balances a simple binary split", () => {
const unbalanced: MosaicNode<string> = {
direction: "row",
first: "w1",
second: "w2",
splitPercentage: 70,
};
const balanced = balanceLayout(unbalanced);
expect(balanced).toEqual({
direction: "row",
first: "w1",
second: "w2",
splitPercentage: 50,
});
});
it("balances nested splits recursively", () => {
const unbalanced: MosaicNode<string> = {
direction: "row",
first: {
direction: "column",
first: "w1",
second: "w2",
splitPercentage: 30,
},
second: {
direction: "column",
first: "w3",
second: "w4",
splitPercentage: 80,
},
splitPercentage: 60,
};
const balanced = balanceLayout(unbalanced);
// All splits should be 50%
expect(balanced).toMatchObject({
splitPercentage: 50,
first: { splitPercentage: 50 },
second: { splitPercentage: 50 },
});
});
it("preserves window IDs and directions", () => {
const original: MosaicNode<string> = {
direction: "column",
first: "w1",
second: {
direction: "row",
first: "w2",
second: "w3",
splitPercentage: 75,
},
splitPercentage: 25,
};
const balanced = balanceLayout(original);
const windowIds = collectWindowIds(balanced);
expect(windowIds).toEqual(["w1", "w2", "w3"]);
// Check directions preserved
if (balanced && typeof balanced !== "string") {
expect(balanced.direction).toBe("column");
if (typeof balanced.second !== "string") {
expect(balanced.second.direction).toBe("row");
}
}
});
});
describe("applyPresetToLayout", () => {
it("throws error if too few windows", () => {
const layout: MosaicNode<string> = "w1";
expect(() =>
applyPresetToLayout(layout, BUILT_IN_PRESETS.grid)
).toThrow("at least 2 windows");
});
it("throws error if too many windows for side-by-side", () => {
const layout: MosaicNode<string> = {
direction: "row",
first: {
direction: "row",
first: "w1",
second: "w2",
splitPercentage: 50,
},
second: {
direction: "row",
first: {
direction: "row",
first: "w3",
second: "w4",
splitPercentage: 50,
},
second: "w5",
splitPercentage: 50,
},
splitPercentage: 50,
};
expect(() =>
applyPresetToLayout(layout, BUILT_IN_PRESETS["side-by-side"])
).toThrow("maximum 4 windows");
});
it("applies grid preset to existing layout", () => {
const existingLayout: MosaicNode<string> = {
direction: "row",
first: "w1",
second: "w2",
splitPercentage: 50,
};
const result = applyPresetToLayout(existingLayout, BUILT_IN_PRESETS.grid);
const windowIds = collectWindowIds(result);
expect(windowIds).toEqual(["w1", "w2"]);
});
it("preserves all windows when applying preset", () => {
const originalWindows = ["w1", "w2", "w3", "w4", "w5", "w6", "w7"];
// Create a simple layout
let layout: MosaicNode<string> = originalWindows[0];
for (let i = 1; i < originalWindows.length; i++) {
layout = {
direction: "row",
first: layout,
second: originalWindows[i],
splitPercentage: 50,
};
}
const result = applyPresetToLayout(layout, BUILT_IN_PRESETS.grid);
const windowIds = collectWindowIds(result);
expect(windowIds).toEqual(originalWindows);
});
});
});