Files
next-tjwater-frontend/src/features/workbench/workspace/workspace-model.test.ts
T

231 lines
9.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
MAX_ANALYSIS_WINDOWS,
applyWorkspaceLayout,
closeAnalysisWindow,
createInitialWorkspaceState,
maximizeWorkspaceWindow,
minimizeWorkspaceWindow,
openAnalysisDocument,
resizeWorkspace,
restoreWorkspaceWindow,
setWorkspacePrimaryWindow,
swapWorkspaceWindows,
updateWorkspaceWindowRect,
type AnalysisDocument,
type WorkspaceState
} from "./workspace-model";
const bounds = { width: 1180, height: 760 };
function documentAt(index: number): AnalysisDocument {
return {
id: `analysis-${index}`,
title: `分析 ${index}`,
subtitle: "测试",
generatedAt: "10:40:00",
blocks: []
};
}
function openDocuments(count: number) {
return Array.from({ length: count }, (_, index) => index + 1).reduce(
(current, index) => openAnalysisDocument(current, documentAt(index), bounds),
createInitialWorkspaceState(bounds)
);
}
function visibleIds(state: WorkspaceState) {
return [state.map, ...state.analyses]
.filter((window) => window.mode !== "minimized")
.map((window) => window.id);
}
describe("workspace window lifecycle", () => {
it("minimizes the map for the first analysis and restores it after the last window closes", () => {
const initial = createInitialWorkspaceState(bounds);
const opened = openAnalysisDocument(initial, documentAt(1), bounds);
expect(opened.map.mode).toBe("minimized");
expect(opened.analyses).toHaveLength(1);
const closed = closeAnalysisWindow(opened, "analysis-1");
expect(closed.analyses).toHaveLength(0);
expect(closed.map.mode).toBe("docked");
expect(closed.activeWindowId).toBe("workspace-map");
});
it("supports minimize, maximize and bounded restore", () => {
const opened = openAnalysisDocument(createInitialWorkspaceState(bounds), documentAt(1), bounds);
const minimized = minimizeWorkspaceWindow(opened, "analysis-1");
expect(minimized.analyses[0]?.mode).toBe("minimized");
const restored = restoreWorkspaceWindow(minimized, "analysis-1", bounds);
expect(restored.analyses[0]?.mode).toBe("floating");
const maximized = maximizeWorkspaceWindow(restored, "analysis-1");
expect(maximized.analyses[0]?.mode).toBe("maximized");
const resized = updateWorkspaceWindowRect(
maximized,
"analysis-1",
{ x: -100, y: -80, width: 4000, height: 3000 },
bounds
);
expect(resized.analyses[0]?.rect).toEqual({ x: 12, y: 12, width: 1156, height: 736 });
});
it("caps transient analysis windows without deleting an existing result", () => {
const state = openDocuments(MAX_ANALYSIS_WINDOWS);
const overflow = openAnalysisDocument(state, documentAt(99), bounds);
expect(overflow).toBe(state);
expect(overflow.analyses).toHaveLength(MAX_ANALYSIS_WINDOWS);
});
it("reclamps current and restore rectangles when the workspace shrinks", () => {
const opened = openAnalysisDocument(createInitialWorkspaceState(bounds), documentAt(1), bounds);
const moved = updateWorkspaceWindowRect(
opened,
"analysis-1",
{ x: 500, y: 260, width: 660, height: 480 },
bounds
);
const resized = resizeWorkspace(moved, { width: 800, height: 600 });
const analysis = resized.analyses[0];
expect(analysis?.rect).toEqual({ x: 128, y: 108, width: 660, height: 480 });
expect(analysis?.restoreRect).toEqual(analysis?.rect);
expect(resized.map.restoreRect.x + resized.map.restoreRect.width).toBeLessThanOrEqual(788);
expect(resized.map.restoreRect.y + resized.map.restoreRect.height).toBeLessThanOrEqual(588);
});
});
describe("workspace layout presets", () => {
it("places the active window first, keeps the map visible and minimizes primary-layout overflow", () => {
const arranged = applyWorkspaceLayout(openDocuments(5), "primary", bounds);
expect(arranged.layout.mode).toBe("primary");
expect(arranged.layout.orderedWindowIds[0]).toBe("analysis-5");
expect(visibleIds(arranged)).toEqual(["workspace-map", "analysis-4", "analysis-5"]);
expect(arranged.analyses.filter((window) => window.minimizedByLayout)).toHaveLength(3);
const primary = arranged.analyses.find((window) => window.id === "analysis-5");
expect(primary?.rect.width).toBeGreaterThan(arranged.map.rect.width);
expect(primary?.rect.height).toBe(bounds.height - 16);
});
it("restores every window into a bounded grid", () => {
const arranged = applyWorkspaceLayout(openDocuments(6), "grid", bounds);
const windows = [arranged.map, ...arranged.analyses];
expect(windows).toHaveLength(7);
expect(windows.every((window) => window.mode === "floating")).toBe(true);
for (const window of windows) {
expect(window.rect.x).toBeGreaterThanOrEqual(8);
expect(window.rect.y).toBeGreaterThanOrEqual(8);
expect(window.rect.x + window.rect.width).toBeLessThanOrEqual(bounds.width - 8);
expect(window.rect.y + window.rect.height).toBeLessThanOrEqual(bounds.height - 8);
}
});
it("uses two visible slots for split and restores all windows when changing presets", () => {
const primary = applyWorkspaceLayout(openDocuments(4), "primary", bounds);
const split = applyWorkspaceLayout(primary, "split", bounds);
expect(visibleIds(split)).toHaveLength(2);
expect(visibleIds(split)).toContain("workspace-map");
expect(split.analyses.filter((window) => window.minimizedByLayout)).toHaveLength(3);
const grid = applyWorkspaceLayout(split, "grid", bounds);
expect(visibleIds(grid)).toHaveLength(5);
});
it("auto-inserts a new Agent result into the current layout", () => {
const primary = applyWorkspaceLayout(openDocuments(2), "primary", bounds);
const opened = openAnalysisDocument(primary, documentAt(3), bounds);
expect(opened.layout.mode).toBe("primary");
expect(opened.layout.orderedWindowIds[0]).toBe("analysis-3");
expect(opened.activeWindowId).toBe("analysis-3");
expect(opened.map.mode).not.toBe("minimized");
});
it("reflows after manual minimize, taskbar restore and workspace resize", () => {
const arranged = applyWorkspaceLayout(openDocuments(4), "primary", bounds);
const minimized = minimizeWorkspaceWindow(arranged, "analysis-4", bounds);
const replacement = minimized.analyses.find((window) => window.id === "analysis-3");
expect(minimized.analyses.find((window) => window.id === "analysis-4")?.minimizedByLayout).toBe(false);
expect(replacement?.mode).toBe("floating");
const restored = restoreWorkspaceWindow(minimized, "analysis-4", bounds);
expect(restored.layout.orderedWindowIds[0]).toBe("analysis-4");
expect(restored.analyses.find((window) => window.id === "analysis-4")?.mode).toBe("floating");
const resized = resizeWorkspace(restored, { width: 900, height: 620 });
expect(resized.analyses.find((window) => window.id === "analysis-4")?.rect.height).toBe(604);
});
it("supports cascade layout and an explicit primary-window command", () => {
const cascade = applyWorkspaceLayout(openDocuments(3), "cascade", bounds);
expect(visibleIds(cascade)).toHaveLength(4);
expect(cascade.analyses[0]?.rect.x).toBeGreaterThan(cascade.analyses[1]?.rect.x ?? 0);
const primary = applyWorkspaceLayout(cascade, "primary", bounds);
const promoted = setWorkspacePrimaryWindow(primary, "analysis-1", bounds);
expect(promoted.layout.orderedWindowIds[0]).toBe("analysis-1");
expect(promoted.analyses.find((window) => window.id === "analysis-1")?.rect.width).toBeGreaterThan(promoted.map.rect.width);
});
});
describe("workspace window exchange", () => {
it("swaps layout slots and preserves the order after resize", () => {
const arranged = applyWorkspaceLayout(openDocuments(2), "primary", bounds);
const mapRect = arranged.map.rect;
const primaryRect = arranged.analyses.find((window) => window.id === "analysis-2")?.rect;
const swapped = swapWorkspaceWindows(arranged, "workspace-map", "analysis-2", bounds);
expect(swapped.map.rect).toEqual(primaryRect);
expect(swapped.analyses.find((window) => window.id === "analysis-2")?.rect).toEqual(mapRect);
const resized = resizeWorkspace(swapped, { width: 1000, height: 700 });
expect(resized.layout.orderedWindowIds.indexOf("workspace-map")).toBeLessThan(
resized.layout.orderedWindowIds.indexOf("analysis-2")
);
expect(resized.map.rect.width).toBeGreaterThan(resized.analyses.find((window) => window.id === "analysis-2")!.rect.width);
});
it("exchanges full window roles and rectangles in free layout", () => {
const initial = createInitialWorkspaceState(bounds);
const mapRect = initial.map.rect;
const opened = openAnalysisDocument(initial, documentAt(1), bounds);
const analysisRect = opened.analyses[0]!.rect;
const restoredMap = { ...opened, map: { ...opened.map, mode: "docked" as const } };
const swapped = swapWorkspaceWindows(restoredMap, "analysis-1", "workspace-map", bounds);
expect(swapped.analyses[0]?.mode).toBe("docked");
expect(swapped.analyses[0]?.rect).toEqual(mapRect);
expect(swapped.map.mode).toBe("floating");
expect(swapped.map.rect).toEqual(analysisRect);
});
it("exits automatic layout when a window is placed in blank workspace", () => {
const arranged = applyWorkspaceLayout(openDocuments(2), "grid", bounds);
const moved = updateWorkspaceWindowRect(
arranged,
"analysis-2",
{ x: 90, y: 70, width: 420, height: 360 },
bounds
);
expect(moved.layout.mode).toBe("free");
expect(moved.analyses.find((window) => window.id === "analysis-2")?.rect).toEqual({
x: 90,
y: 70,
width: 420,
height: 360
});
});
});