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 }); }); });