Files
nanobot/webui/src/tests/workbench-model.test.ts
T

221 lines
7.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
EMPTY_WORKBENCH_STATE,
MAX_WORKBENCH_PANES,
addWorkbenchPane,
attachWorkbenchPane,
createWorkbenchTab,
detachWorkbenchPane,
dissolveWorkbenchTab,
normalizeWorkbenchState,
orderWorkbenchTabs,
reconcileWorkbench,
renameWorkbenchTab,
setWorkbenchLayout,
setWorkbenchPaneLayoutOrder,
setWorkbenchSplitRatios,
workbenchTab,
workbenchTabForPane,
} from "@/components/workbench/workbench-model";
describe("workbench model", () => {
it("derives standalone panes without persisting virtual tabs", () => {
const match = workbenchTabForPane(EMPTY_WORKBENCH_STATE, "pane-a");
expect(match.tabKey).not.toBe("pane-a");
expect(match.tab).toEqual({
explicit: false,
title: null,
paneKeys: ["pane-a"],
layoutPaneKeys: ["pane-a"],
layout: "columns",
splitRatios: [],
});
expect(EMPTY_WORKBENCH_STATE.tabs).toEqual({});
});
it("persists only a visible singleton group", () => {
const state = createWorkbenchTab(EMPTY_WORKBENCH_STATE, "pane-a");
const match = workbenchTabForPane(state, "pane-a");
expect(workbenchTab(state, match.tabKey)).toMatchObject({
explicit: true,
paneKeys: ["pane-a"],
});
expect(detachWorkbenchPane(state, match.tabKey, "pane-a").tabs).toEqual({});
});
it("materializes a group when a pane is added", () => {
let state = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "pane-a", "pane-b");
const tabKey = workbenchTabForPane(state, "pane-a").tabKey;
state = setWorkbenchLayout(state, tabKey, "main-stack");
state = renameWorkbenchTab(state, tabKey, "Research");
expect(workbenchTab(state, tabKey)).toEqual({
explicit: false,
title: "Research",
paneKeys: ["pane-a", "pane-b"],
layoutPaneKeys: ["pane-a", "pane-b"],
layout: "main-stack",
splitRatios: [],
});
});
it("detaches a pane without persisting its standalone projection", () => {
let state = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "pane-a", "pane-b");
const tabKey = workbenchTabForPane(state, "pane-a").tabKey;
state = createWorkbenchTab(state, "pane-a");
state = addWorkbenchPane(state, "pane-a", "pane-c");
state = detachWorkbenchPane(state, tabKey, "pane-a");
expect(workbenchTab(state, tabKey)?.paneKeys).toEqual(["pane-b", "pane-c"]);
expect(workbenchTabForPane(state, "pane-a").tab.paneKeys).toEqual(["pane-a"]);
expect(Object.values(state.tabs).some((tab) => tab.paneKeys.includes("pane-a"))).toBe(false);
});
it("dissolves a group into derived standalone panes", () => {
const grouped = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "pane-a", "pane-b");
const tabKey = workbenchTabForPane(grouped, "pane-a").tabKey;
const state = dissolveWorkbenchTab(grouped, tabKey);
expect(state.tabs).toEqual({});
expect(workbenchTabForPane(state, "pane-a").tab.paneKeys).toEqual(["pane-a"]);
expect(workbenchTabForPane(state, "pane-b").tab.paneKeys).toEqual(["pane-b"]);
});
it("moves panes symmetrically and removes an implicit singleton source", () => {
let state = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "pane-a", "pane-b");
const sourceTabKey = workbenchTabForPane(state, "pane-a").tabKey;
state = createWorkbenchTab(state, "pane-c");
const targetTabKey = workbenchTabForPane(state, "pane-c").tabKey;
state = attachWorkbenchPane(state, targetTabKey, "pane-a");
expect(workbenchTab(state, sourceTabKey)).toBeNull();
expect(workbenchTab(state, targetTabKey)?.paneKeys).toEqual(["pane-c", "pane-a"]);
state = attachWorkbenchPane(state, targetTabKey, "pane-b");
expect(workbenchTab(state, targetTabKey)?.paneKeys).toEqual([
"pane-c",
"pane-a",
"pane-b",
]);
});
it("keeps membership independent from workspace pane order", () => {
let state = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "pane-a", "pane-b");
state = addWorkbenchPane(state, "pane-a", "pane-c");
const tabKey = workbenchTabForPane(state, "pane-a").tabKey;
const [ordered] = orderWorkbenchTabs(
state,
["pane-c", "pane-a", "pane-b"],
new Map(),
);
expect(workbenchTab(state, tabKey)?.paneKeys).toEqual(["pane-a", "pane-b", "pane-c"]);
expect(ordered.paneKeys).toEqual(["pane-c", "pane-a", "pane-b"]);
state = setWorkbenchPaneLayoutOrder(state, tabKey, ["pane-b", "pane-c", "pane-a"]);
expect(workbenchTab(state, tabKey)?.layoutPaneKeys).toEqual([
"pane-b",
"pane-c",
"pane-a",
]);
});
it("stores resize ratios and resets them when geometry changes", () => {
let state = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "pane-a", "pane-b");
const tabKey = workbenchTabForPane(state, "pane-a").tabKey;
state = setWorkbenchSplitRatios(state, tabKey, [0.35]);
expect(workbenchTab(state, tabKey)?.splitRatios).toEqual([0.35]);
state = setWorkbenchLayout(state, tabKey, "rows");
expect(workbenchTab(state, tabKey)?.splitRatios).toEqual([]);
});
it("keeps groups contiguous and ranks them by their latest pane", () => {
let state = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "pane-a", "pane-c");
state = addWorkbenchPane(state, "pane-b", "pane-d");
const alphaTabKey = workbenchTabForPane(state, "pane-a").tabKey;
const betaTabKey = workbenchTabForPane(state, "pane-b").tabKey;
const tabs = orderWorkbenchTabs(
state,
["pane-d", "pane-c", "pane-b", "pane-a", "pane-e"],
new Map([
["pane-a", "2026-08-01T10:00:00Z"],
["pane-b", "2026-08-03T10:00:00Z"],
["pane-c", "2026-08-05T10:00:00Z"],
["pane-d", "2026-08-04T10:00:00Z"],
["pane-e", "2026-08-02T10:00:00Z"],
]),
);
expect(tabs.map(({ tabKey, paneKeys }) => ({ tabKey, paneKeys }))).toEqual([
{ tabKey: alphaTabKey, paneKeys: ["pane-c", "pane-a"] },
{ tabKey: betaTabKey, paneKeys: ["pane-d", "pane-b"] },
{ tabKey: workbenchTabForPane(state, "pane-e").tabKey, paneKeys: ["pane-e"] },
]);
expect(Object.keys(state.tabs)).toHaveLength(2);
});
it("caps a group at four panes", () => {
let state = addWorkbenchPane(EMPTY_WORKBENCH_STATE, "pane-a", "pane-1");
for (let index = 2; index <= MAX_WORKBENCH_PANES; index += 1) {
state = addWorkbenchPane(state, "pane-a", `pane-${index}`);
}
const tabKey = workbenchTabForPane(state, "pane-a").tabKey;
expect(workbenchTab(state, tabKey)?.paneKeys).toEqual([
"pane-a",
"pane-1",
"pane-2",
"pane-3",
]);
});
it("repairs persisted groups without materializing missing sessions", () => {
const state = normalizeWorkbenchState({
version: 1,
tabs: {
alpha: {
title: "Alpha",
paneKeys: ["pane-a", "pane-b", "pane-b", 9],
layout: "unknown",
},
duplicate: {
paneKeys: ["pane-b", "deleted"],
layout: "grid",
},
invisible: {
paneKeys: ["pane-c"],
layout: "columns",
},
},
});
const reconciled = reconcileWorkbench(
state,
new Set(["pane-a", "pane-b", "pane-c"]),
);
expect(workbenchTab(reconciled, "alpha")).toEqual({
explicit: false,
title: "Alpha",
paneKeys: ["pane-a", "pane-b"],
layoutPaneKeys: ["pane-a", "pane-b"],
layout: "columns",
splitRatios: [],
});
expect(Object.keys(reconciled.tabs)).toEqual(["alpha"]);
expect(workbenchTabForPane(reconciled, "pane-c").tab.paneKeys).toEqual(["pane-c"]);
});
it("keeps persisted state sparse with thousands of standalone sessions", () => {
const sessionKeys = Array.from({ length: 2_000 }, (_, index) => `pane-${index}`);
const reconciled = reconcileWorkbench(EMPTY_WORKBENCH_STATE, new Set(sessionKeys));
const ordered = orderWorkbenchTabs(reconciled, sessionKeys, new Map());
expect(reconciled.tabs).toEqual({});
expect(ordered).toHaveLength(2_000);
});
});