| 1 | import assert from "node:assert/strict"; |
| 2 | import React, { act } from "react"; |
| 3 | import { createRoot } from "react-dom/client"; |
| 4 | import { JSDOM } from "jsdom"; |
| 5 | import { useWorkspacePanelCommands } from "../app-runtime/useWorkspacePanelCommands"; |
| 6 | import { useSessionNavigationCommands, type SessionNavigationCommandsInput } from "../app-runtime/useSessionNavigationCommands"; |
| 7 | import { loadWorkspacePanelOpen, saveWorkspacePanelOpen, useLayoutStore } from "../store/layout"; |
| 8 | import { useActivityBarStore } from "../store/activityBar"; |
| 9 | import { useRemoteStore } from "../store/remote"; |
| 10 | import type { RemoteHostView } from "../lib/types"; |
| 11 | |
| 12 | const dom = new JSDOM("<div id='root'></div>", { url: "http://localhost" }); |
| 13 | Object.assign(globalThis, { window: dom.window, document: dom.window.document, localStorage: dom.window.localStorage, |
| 14 | IS_REACT_ACT_ENVIRONMENT: true }); |
| 15 | const root = createRoot(document.getElementById("root")!); |
| 16 | let commands!: ReturnType<typeof useWorkspacePanelCommands>; |
| 17 | let closes = 0; let widthClears = 0; |
| 18 | const closeOverlays = () => { closes++; }; |
| 19 | const clearLiveWidth = () => { widthClears++; }; |
| 20 | let restoredWidth = 0; |
| 21 | const globalRoot = "/fixture/global-workspace"; |
| 22 | let navigation!: ReturnType<typeof useSessionNavigationCommands>; |
| 23 | let navigationRequest: unknown; |
| 24 | let draftRequest: unknown; |
| 25 | const setTreeWidth = (width: number) => { restoredWidth = width; }; |
| 26 | function Probe({ workspace, visible, sessionId }: { workspace: string; visible: boolean; sessionId: string }) { |
| 27 | commands = useWorkspacePanelCommands({ sessionId, workspaceRoot: workspace, visible, closeOverlays, clearLiveWidth, |
| 28 | availableWidth: 800, clampTreeWidth: (width) => width, setTreeWidth, gridOpen: visible, t: (key: string) => key } as never); |
| 29 | navigation = useSessionNavigationCommands({ |
| 30 | activeTab: { id: "fixture", scope: workspace === globalRoot ? "global" : "project", workspaceRoot: workspace }, |
| 31 | closeTransientOverlays: closeOverlays, clearImDetail: () => {}, prepareBlankWorkspace: commands.prepareBlankWorkspace, |
| 32 | enterConversation: () => {}, |
| 33 | draft: { open: async (scope, workspaceRoot) => { draftRequest = { scope, workspaceRoot }; }, dismiss: () => {} }, |
| 34 | navigation: { enqueueNavigation: async request => { navigationRequest = request; } }, |
| 35 | } as SessionNavigationCommandsInput); |
| 36 | return null; |
| 37 | } |
| 38 | const paint = (workspace: string, visible = false, sessionId = `session:${workspace}`) => |
| 39 | act(async () => root.render(<Probe workspace={workspace} visible={visible} sessionId={sessionId} />)); |
| 40 | try { |
| 41 | saveWorkspacePanelOpen(false, "A"); saveWorkspacePanelOpen(true, "B"); |
| 42 | await paint("A"); |
| 43 | const first = commands; |
| 44 | assert.equal(useLayoutStore.getState().workspacePanelOpen, false); |
| 45 | await act(async () => commands.openRightDockMode("changed")); |
| 46 | assert.equal(loadWorkspacePanelOpen("A"), true); |
| 47 | await paint("A", true); |
| 48 | await act(async () => { commands.toggleWorkspaceMaximized(); commands.handleWorkspacePreviewModeChange(true); }); |
| 49 | assert.equal(useLayoutStore.getState().workspacePanelMaximized, true); |
| 50 | await act(async () => commands.openRightDockMode("context")); |
| 51 | assert.equal(useLayoutStore.getState().workspacePanelMaximized, false); |
| 52 | assert.equal(useLayoutStore.getState().workspacePreviewActive, false); |
| 53 | await act(async () => commands.toggleWorkspacePanel()); |
| 54 | assert.equal(loadWorkspacePanelOpen("A"), false); |
| 55 | assert.equal(widthClears, 1); |
| 56 | await paint("B"); |
| 57 | assert.equal(useLayoutStore.getState().workspacePanelOpen, true, "different project restores its own preference"); |
| 58 | await paint("B", true); |
| 59 | assert.deepEqual(useActivityBarStore.getState().tabs.map(tab => tab.type), ["context"], |
| 60 | "an expanded empty dock opens Overview when a session becomes active"); |
| 61 | const overviewTabId = useActivityBarStore.getState().activeTabId!; |
| 62 | await act(async () => useActivityBarStore.getState().closeTab(overviewTabId)); |
| 63 | await paint("B", true); |
| 64 | assert.deepEqual(useActivityBarStore.getState().tabs, [], |
| 65 | "closing the default tab does not reopen it during the same session"); |
| 66 | assert.equal(useLayoutStore.getState().workspacePanelOpen, false, |
| 67 | "closing the final dock tab collapses the entire workspace panel"); |
| 68 | assert.equal(loadWorkspacePanelOpen("B"), false, "automatic collapse persists for the current project"); |
| 69 | await paint("B", true, "session:B:next"); |
| 70 | assert.deepEqual(useActivityBarStore.getState().tabs, [], "a new session preserves the collapsed dock"); |
| 71 | await act(async () => commands.toggleWorkspacePanel()); |
| 72 | await paint("B", true, "session:B:next"); |
| 73 | assert.deepEqual(useActivityBarStore.getState().tabs.map(tab => tab.type), ["context"], |
| 74 | "explicitly reopening the dock in a new session seeds Overview"); |
| 75 | await act(async () => commands.openRightDockMode("files")); |
| 76 | await act(async () => useActivityBarStore.getState().closeTab(useActivityBarStore.getState().activeTabId!)); |
| 77 | assert.equal(useLayoutStore.getState().workspacePanelOpen, true, "closing one of multiple tabs keeps the dock open"); |
| 78 | await act(async () => { |
| 79 | commands.openRightDockMode("changed"); |
| 80 | commands.toggleWorkspaceMaximized(); |
| 81 | }); |
| 82 | const beforeCloseAll = widthClears; |
| 83 | await act(async () => { |
| 84 | useActivityBarStore.getState().tabs.forEach(tab => useActivityBarStore.getState().closeTab(tab.id)); |
| 85 | assert.equal(useLayoutStore.getState().workspacePanelOpen, false, "close-all collapses synchronously"); |
| 86 | }); |
| 87 | assert.equal(useLayoutStore.getState().workspacePanelMaximized, false, "closing all tabs resets maximized mode"); |
| 88 | assert.equal(widthClears, beforeCloseAll + 1, "automatic collapse clears the live resize width once"); |
| 89 | await act(async () => commands.openDockEntry("files")); |
| 90 | assert.equal(useLayoutStore.getState().workspacePanelOpen, true, "opening an entry expands the dock again"); |
| 91 | assert.deepEqual(useActivityBarStore.getState().tabs.map(tab => tab.type), ["file"]); |
| 92 | await act(async () => { |
| 93 | useActivityBarStore.getState().closeTab(useActivityBarStore.getState().activeTabId!); |
| 94 | commands.openRightDockMode("changed"); |
| 95 | }); |
| 96 | assert.equal(useLayoutStore.getState().workspacePanelOpen, true, "a later open in the same batch wins over the close"); |
| 97 | assert.equal(loadWorkspacePanelOpen("B"), true); |
| 98 | saveWorkspacePanelOpen(true, "empty-project"); |
| 99 | await paint("empty-project"); |
| 100 | assert.equal(useLayoutStore.getState().workspacePanelOpen, true, |
| 101 | "restoring an empty project is not mistaken for closing the final tab"); |
| 102 | assert.equal(loadWorkspacePanelOpen("B"), true, "project restoration does not overwrite the previous preference"); |
| 103 | await paint("A"); |
| 104 | assert.equal(useLayoutStore.getState().workspacePanelOpen, false); |
| 105 | assert.equal(commands.closeWorkspacePanel, first.closeWorkspacePanel); |
| 106 | assert.equal(commands.openRightDockMode, first.openRightDockMode); |
| 107 | await act(async () => commands.openRightDockMode("files")); |
| 108 | const hosts = [{ id: "offline" }, { id: "online" }] as RemoteHostView[]; |
| 109 | await act(async () => { |
| 110 | useRemoteStore.getState().setHosts(hosts); |
| 111 | useRemoteStore.getState().applyStatus({ hostId: "online", state: "connected" }); |
| 112 | commands.openRemoteDock(); |
| 113 | }); |
| 114 | assert.equal(useRemoteStore.getState().explorerHostId, "online"); |
| 115 | assert.equal(useRemoteStore.getState().explorerOpen, false, "request is consumed by the same dock owner"); |
| 116 | assert.equal(useLayoutStore.getState().rightDockMode, "remote"); |
| 117 | await act(async () => { commands.restoreWorkspaceDockWidths(640, 0); }); |
| 118 | assert.equal(restoredWidth, 640, "dock width restore clamps through the owner and writes the layout store port"); |
| 119 | await act(async () => useRemoteStore.getState().setHosts([])); |
| 120 | assert.equal(useLayoutStore.getState().rightDockMode, "files"); |
| 121 | await paint("A"); |
| 122 | await act(async () => commands.openRightDockMode("changed")); |
| 123 | saveWorkspacePanelOpen(true, "B"); |
| 124 | await act(async () => { |
| 125 | commands.toggleWorkspaceMaximized(); |
| 126 | commands.prepareBlankWorkspace("B"); |
| 127 | }); |
| 128 | assert.equal(useLayoutStore.getState().workspacePanelOpen, false, "new-session intent collapses the dock immediately"); |
| 129 | assert.equal(useLayoutStore.getState().workspacePanelMaximized, false); |
| 130 | assert.equal(loadWorkspacePanelOpen("A"), true, "another project's preference is untouched"); |
| 131 | await paint("B"); |
| 132 | assert.equal(useLayoutStore.getState().workspacePanelOpen, false, "destination restoration cannot reopen the blank-session dock"); |
| 133 | await act(async () => commands.openRightDockMode("files")); |
| 134 | await paint("B"); |
| 135 | assert.equal(useLayoutStore.getState().workspacePanelOpen, true, "manual open stays open on subsequent renders"); |
| 136 | await paint("A"); |
| 137 | assert.equal(useLayoutStore.getState().workspacePanelOpen, true, "ordinary project navigation retains restoration behavior"); |
| 138 | saveWorkspacePanelOpen(true, ""); |
| 139 | saveWorkspacePanelOpen(true, globalRoot); |
| 140 | await act(async () => navigation.openBlankSession("global", globalRoot)); |
| 141 | assert.deepEqual(draftRequest, { scope: "global", workspaceRoot: "" }, "global draft requests retain the empty root contract"); |
| 142 | assert.equal(navigationRequest, undefined, "local new-session navigation does not create a formal blank session"); |
| 143 | assert.equal(loadWorkspacePanelOpen(""), true, "global creation does not overwrite the legacy fallback for other projects"); |
| 144 | await paint(globalRoot); |
| 145 | assert.equal(useLayoutStore.getState().workspacePanelOpen, false, "global destination restoration cannot reopen the new-session dock"); |
| 146 | await act(async () => commands.openRightDockMode("files")); |
| 147 | await paint("A"); |
| 148 | await paint(globalRoot); |
| 149 | assert.equal(useLayoutStore.getState().workspacePanelOpen, true, "manual global preference still restores on ordinary navigation"); |
| 150 | await act(async () => navigation.handleNewTab()); |
| 151 | assert.equal(loadWorkspacePanelOpen(globalRoot), false, "new-session toolbar uses the active global directory"); |
| 152 | assert.deepEqual(draftRequest, { scope: "global", workspaceRoot: "" }); |
| 153 | await paint("A"); |
| 154 | assert.equal(useLayoutStore.getState().workspacePanelOpen, true, "global creation preserves the source project's preference"); |
| 155 | await act(async () => root.unmount()); |
| 156 | const before = { closes, widthClears, layout: useLayoutStore.getState() }; |
| 157 | first.openRightDockMode("changed"); first.toggleWorkspaceMaximized(); first.closeWorkspacePanel(); |
| 158 | assert.deepEqual({ closes, widthClears, layout: useLayoutStore.getState() }, before, "disposed entries cannot change layout or project preferences"); |
| 159 | console.log("workspace commands: scoped restoration, preview/maximize, remote requests and synchronous disposal passed"); |
| 160 | } finally { dom.window.close(); } |
| 161 |