231 lines
9.5 KiB
TypeScript
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
|
|
});
|
|
});
|
|
});
|