import { afterEach, describe, expect, test } from "bun:test"; import type { ReactNode } from "react"; import { act, useReducer, useState } from "react"; import type { PluginRegistry } from "../../../plugins/registry"; import { TestDialogProvider, emitKeypress as emitTuiKeypress, testRender, type TestKeyEvent } from "../../../renderers/opentui/test-utils"; import { openTuiUiHost } from "../../../renderers/opentui/ui-host"; import { AppContext, appReducer, createInitialState, resolveTickerForPane, usePaneTicker, } from "../../../state/app/context"; import { TICKER_RESEARCH_PANE_ID, cloneLayout, createDefaultConfig, type LayoutConfig } from "../../../types/config"; import type { PaneProps } from "../../../types/plugin"; import { Text, Textarea } from "../../../ui"; import { TransientLayoutProvider, useTransientLayout, type TransientLayoutState } from "../transient-layout"; import { resolvePaneFocusSourceLayout } from "./fullscreen"; import { Shell, buildNativeWindowState, resolveAppHeaderHeightCells, resolvePaneManagementShortcut, } from "./index"; let testSetup: Awaited> | undefined; afterEach(() => { if (testSetup) { testSetup.renderer.destroy(); testSetup = undefined; } }); function createShellPluginRegistry(options?: { portfolioListComponent?: (props: PaneProps) => ReactNode; tickerDetailComponent?: (props: PaneProps) => ReactNode; }): PluginRegistry { return { panes: new Map([ ["portfolio-list", { id: "portfolio-list", name: "Portfolio List", component: options?.portfolioListComponent ?? (() => Portfolio Body), defaultPosition: "left", }], [TICKER_RESEARCH_PANE_ID, { id: TICKER_RESEARCH_PANE_ID, name: "Ticker Research", component: options?.tickerDetailComponent ?? (() => Ticker Research Body), defaultPosition: "right", defaultMode: "floating", }], ]), paneTemplates: new Map(), commands: new Map(), getEnabledTickerActions() { return [...this.tickerActions.values()]; }, tickerActions: new Map(), brokers: new Map(), allPlugins: new Map(), getPluginPaneIds: () => [], getPluginPaneTemplateIds: () => [], hasPaneSettings: (paneId: string) => paneId === "portfolio-list:main", notify: () => {}, openPaneSettingsFn: () => {}, openCommandBar: () => {}, showPane: () => {}, openWindowMode: () => {}, openWindowModeFn: () => {}, updateLayoutFn: () => {}, hidePane: () => {}, } as unknown as PluginRegistry; } const emitKeypress = (event: TestKeyEvent) => emitTuiKeypress(testSetup!, event, { trackPropagation: true, frames: 2 }); type ShellTestAction = { type: string; [key: string]: any }; function requireLayoutInstance(config: ReturnType, instanceId: string) { const instance = config.layout.instances.find((entry) => entry.instanceId === instanceId); if (!instance) throw new Error(`missing default pane ${instanceId}`); return instance; } function createShellStateWithLayout( config: ReturnType, layout: LayoutConfig, focusedPaneId: string | null, ) { return { ...createInitialState({ ...config, layout, layouts: [{ name: "Default", layout: cloneLayout(layout) }], }), focusedPaneId, }; } async function renderShellForWindowModeTest( state: ReturnType, options: { registry?: PluginRegistry; width?: number; height?: number; dispatch?: (action: ShellTestAction) => void; } = {}, ) { const actions: ShellTestAction[] = []; const registry = options.registry ?? createShellPluginRegistry(); testSetup = await testRender( actions.push(action)) }}> , { width: options.width ?? 80, height: options.height ?? 24 }, ); await testSetup.renderOnce(); return { actions, registry }; } function CaptureTransientLayout({ controls, }: { controls: { transientLayout: TransientLayoutState | null }; }) { const { transientLayout } = useTransientLayout(); controls.transientLayout = transientLayout; return null; } function ShellTransientHarness({ initialState, registry, controls, }: { initialState: ReturnType; registry: PluginRegistry; controls: { dispatch?: (action: ShellTestAction) => void; state?: ReturnType; transientLayout: TransientLayoutState | null; }; }) { const [state, dispatch] = useReducer(appReducer, initialState); controls.dispatch = dispatch; controls.state = state; return ( ); } function findUpdateLayout(actions: ShellTestAction[]) { return actions.find((action) => action.type === "UPDATE_LAYOUT"); } describe("Shell", () => { test.each([false, true])("keeps both numeric edges visible under terminal focus borders (floating=%s)", async (floating) => { const config = createDefaultConfig("/tmp/gloomberb-pane-border-test"); const main = requireLayoutInstance(config, "portfolio-list:main"); const detail = requireLayoutInstance(config, "ticker-detail:main"); const layout: LayoutConfig = { dockRoot: { kind: "pane", instanceId: main.instanceId }, instances: floating ? [main, detail] : [main], floating: floating ? [{ instanceId: detail.instanceId, x: 4, y: 2, width: 50, height: 12 }] : [], detached: [], }; let contentWidth = 0; const EdgeValues = ({ width }: PaneProps) => { contentWidth = width; return {`49.6%${".".repeat(Math.max(0, width - 10))}-8.5%`}; }; const registry = createShellPluginRegistry(floating ? { tickerDetailComponent: EdgeValues } : { portfolioListComponent: EdgeValues }); await renderShellForWindowModeTest(createShellStateWithLayout(config, layout, floating ? detail.instanceId : main.instanceId), { registry }); await testSetup!.renderOnce(); expect(contentWidth).toBe((floating ? 50 : 80) - 2); expect(testSetup!.captureCharFrame()).toContain(`49.6%${".".repeat(contentWidth - 10)}-8.5%`); }); test("uses the desktop titlebar overlay height for shell chrome math", () => { expect(resolveAppHeaderHeightCells({ titleBarOverlay: true, cellHeightPx: 18 })).toBe(28 / 18); expect(resolveAppHeaderHeightCells({ titleBarOverlay: false, cellHeightPx: 18 })).toBe(1); }); test("keeps command bar native occlusion scoped to the panel", () => { const state = buildNativeWindowState( ["portfolio-list:main"], [], null, { open: false, width: 120, contentHeight: 40 }, [ { id: "command-bar:panel", rect: { x: 24, y: 8, width: 72, height: 14 }, zIndex: Number.MAX_SAFE_INTEGER, }, ], ); expect(state.occluders).toEqual([ { id: "command-bar:panel", paneId: null, rect: { x: 24, y: 9, width: 72, height: 14 }, zIndex: Number.MAX_SAFE_INTEGER, }, ]); expect(state.occluders.some((occluder) => occluder.id === "overlay:global")).toBe(false); }); test("opens the pane menu when clicking the docked header action area", async () => { const config = createDefaultConfig("/tmp/gloomberb-shell-test"); const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main"); if (!mainPane) throw new Error("missing default portfolio pane"); const singlePaneLayout = { dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" }, instances: [{ ...mainPane }], floating: [], }; const nextConfig = { ...config, layout: cloneLayout(singlePaneLayout), layouts: [{ name: "Default", layout: cloneLayout(singlePaneLayout) }], }; const state = createInitialState(nextConfig); const pluginRegistry = createShellPluginRegistry(); testSetup = await testRender( {} }}> , { width: 40, height: 10 }, ); await testSetup.renderOnce(); const actionCol = testSetup.captureCharFrame().split("\n")[0]?.indexOf("..."); expect(actionCol).toBeGreaterThanOrEqual(0); await act(async () => { await testSetup!.mockMouse.click(actionCol! + 1, 1); }); await testSetup.renderOnce(); const frame = testSetup.captureCharFrame(); expect(frame).toContain("Settings"); expect(frame).toContain("Ctrl+,"); expect(frame).not.toContain("Layout Actions"); expect(frame).not.toContain("CmdOrCtrl"); }); test("shows a Pop Out action in the pane menu when a desktop bridge is available", async () => { const config = createDefaultConfig("/tmp/gloomberb-shell-test"); const mainPane = config.layout.instances.find((instance) => instance.instanceId === "portfolio-list:main"); if (!mainPane) throw new Error("missing default portfolio pane"); const singlePaneLayout = { dockRoot: { kind: "pane" as const, instanceId: "portfolio-list:main" }, instances: [{ ...mainPane }], floating: [], detached: [], }; const nextConfig = { ...config, layout: cloneLayout(singlePaneLayout), layouts: [{ name: "Default", layout: cloneLayout(singlePaneLayout) }], }; const state = createInitialState(nextConfig); const pluginRegistry = createShellPluginRegistry(); testSetup = await testRender( {} }}> {}, subscribeState: () => () => {}, subscribeDockPreview: () => () => {}, }} /> , { width: 40, height: 10 }, ); await testSetup.renderOnce(); const floatingActionCol = testSetup.captureCharFrame().split("\n")[0]?.indexOf("..."); expect(floatingActionCol).toBeGreaterThanOrEqual(0); await act(async () => { await testSetup!.mockMouse.click(floatingActionCol! + 1, 1); }); await testSetup.renderOnce(); expect(testSetup.captureCharFrame()).toContain("Pop Out"); }); test("shows the pane menu above a high z-index floating pane", async () => { const config = createDefaultConfig("/tmp/gloomberb-shell-test"); const detailPane = config.layout.instances.find((instance) => instance.instanceId === "ticker-detail:main"); if (!detailPane) throw new Error("missing default Ticker Research pane"); const floatingOnlyLayout = { dockRoot: null, instances: [{ ...detailPane }], floating: [{ instanceId: "ticker-detail:main", x: 0, y: 0, width: 40, height: 8, zIndex: 195 }], }; const nextConfig = { ...config, layout: cloneLayout(floatingOnlyLayout), layouts: [{ name: "Default", layout: cloneLayout(floatingOnlyLayout) }], }; const state = { ...createInitialState(nextConfig), focusedPaneId: "ticker-detail:main", }; const pluginRegistry = createShellPluginRegistry(); testSetup = await testRender( {} }}> , { width: 40, height: 10 }, ); await testSetup.renderOnce(); const highZActionCol = testSetup.captureCharFrame().split("\n")[0]?.indexOf("..."); expect(highZActionCol).toBeGreaterThanOrEqual(0); await act(async () => { await testSetup!.mockMouse.click(highZActionCol! + 1, 1); }); await testSetup.renderOnce(); const frame = testSetup.captureCharFrame(); expect(frame).toContain("Dock Pane"); expect(frame).not.toContain("Layout Actions"); }); test("resolves pane management shortcuts", () => { const base = { ctrl: false, meta: true, super: true, shift: true, alt: false }; expect(resolvePaneManagementShortcut({ ...base, name: ",", key: ",", shift: false })).toBe("settings"); expect(resolvePaneManagementShortcut({ ...base, name: "w", key: "w", ctrl: true, meta: false, super: false, shift: false })).toBe("close"); expect(resolvePaneManagementShortcut({ ...base, name: "w", key: "w", shift: false, alt: true })).toBe("close-all-floating"); expect(resolvePaneManagementShortcut({ ...base, name: "W", key: "W", shift: false })).toBeNull(); expect(resolvePaneManagementShortcut({ ...base, name: "D", key: "D" })).toBe("toggle-floating"); expect(resolvePaneManagementShortcut({ ...base, name: "o", key: "o" })).toBe("pop-out"); expect(resolvePaneManagementShortcut({ ...base, name: "c", key: "c" })).toBe("copy-screenshot"); expect(resolvePaneManagementShortcut({ ...base, name: "s", key: "s" })).toBe("share"); expect(resolvePaneManagementShortcut({ ...base, name: "l", key: "l" })).toBe("layout-gallery"); expect(resolvePaneManagementShortcut({ ...base, name: "f", key: "f" })).toBe("toggle-fullscreen"); expect(resolvePaneManagementShortcut({ ...base, name: "g", key: "g" })).toBe("gridlock-all"); expect(resolvePaneManagementShortcut({ ...base, name: "m", key: "m" })).toBe("window-mode"); expect(resolvePaneManagementShortcut({ ...base, name: "r", key: "r" })).toBe("window-resize-mode"); expect(resolvePaneManagementShortcut({ ...base, name: "n", key: "n" })).toBeNull(); expect(resolvePaneManagementShortcut({ ...base, name: "d", key: "d", alt: true })).toBeNull(); expect(resolvePaneManagementShortcut({ ...base, name: "d", key: "d", meta: false, super: false })).toBeNull(); }); test("opens the layout browser from the primary Shift-L shortcut", async () => { const config = createDefaultConfig("/tmp/gloomberb-shell-layout-browser-shortcut-test"); const opened: string[] = []; const registry = createShellPluginRegistry(); registry.showPane = (paneId) => opened.push(paneId); await renderShellForWindowModeTest(createInitialState(config), { registry }); await emitKeypress({ name: "l", ctrl: true, shift: true }); expect(opened).toEqual(["layout-marketplace"]); }); test("toggles the focused pane fullscreen without persisting layout", async () => { const config = createDefaultConfig("/tmp/gloomberb-shell-fullscreen-shortcut-test"); const mainPane = requireLayoutInstance(config, "portfolio-list:main"); const detailPane = requireLayoutInstance(config, "ticker-detail:main"); const dockedLayout = { dockRoot: { kind: "split" as const, axis: "horizontal" as const, ratio: 0.5, first: { kind: "pane" as const, instanceId: "portfolio-list:main" }, second: { kind: "pane" as const, instanceId: "ticker-detail:main" }, }, instances: [{ ...mainPane }, { ...detailPane }], floating: [], detached: [], }; const { actions } = await renderShellForWindowModeTest( createShellStateWithLayout(config, dockedLayout, "portfolio-list:main"), { width: 80, height: 18 }, ); expect(testSetup.captureCharFrame()).toContain("Main Portfolio"); expect(testSetup.captureCharFrame()).toContain("Ticker Research Body"); await emitKeypress({ name: "f", ctrl: true, shift: true }); let frame = testSetup.captureCharFrame(); expect(frame).toContain("Main Portfolio"); expect(frame).not.toContain("Ticker Research Body"); expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false); await emitKeypress({ name: "f", ctrl: true, shift: true }); await act(async () => { await testSetup!.renderOnce(); }); frame = testSetup.captureCharFrame(); expect(frame).toContain("Main Portfolio"); expect(frame).toContain("Ticker Research Body"); expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false); }); test("captures the source layout for transient pane focus", () => { const config = createDefaultConfig("/tmp/gloomberb-shell-fullscreen-layout-test"); const layout = cloneLayout(config.layout); layout.dockRoot = { kind: "pane", instanceId: "portfolio-list:main" }; layout.floating = [{ instanceId: "ticker-detail:main", x: 8, y: 2, width: 32, height: 10, zIndex: 75 }]; const focusedLayout = resolvePaneFocusSourceLayout(layout, "ticker-detail:main"); expect(focusedLayout).toMatchObject({ dockRoot: { kind: "pane", instanceId: "portfolio-list:main" }, floating: [{ instanceId: "ticker-detail:main", x: 8, y: 2, width: 32, height: 10, zIndex: 75 }], }); expect(focusedLayout).not.toBe(layout); expect(focusedLayout?.instances.map((instance) => instance.instanceId)).toEqual( layout.instances.map((instance) => instance.instanceId), ); }); test("locks layout-changing mouse drags while fullscreen is active", async () => { const config = createDefaultConfig("/tmp/gloomberb-shell-fullscreen-drag-test"); const mainPane = requireLayoutInstance(config, "portfolio-list:main"); const detailPane = requireLayoutInstance(config, "ticker-detail:main"); const dockedLayout = { dockRoot: { kind: "split" as const, axis: "horizontal" as const, ratio: 0.5, first: { kind: "pane" as const, instanceId: "portfolio-list:main" }, second: { kind: "pane" as const, instanceId: "ticker-detail:main" }, }, instances: [{ ...mainPane }, { ...detailPane }], floating: [], detached: [], }; const { actions } = await renderShellForWindowModeTest( createShellStateWithLayout(config, dockedLayout, "portfolio-list:main"), { width: 80, height: 18 }, ); await emitKeypress({ name: "f", ctrl: true, shift: true }); await act(async () => { await testSetup!.mockMouse.drag(4, 0, 32, 4); await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); const frame = testSetup.captureCharFrame(); expect(frame).toContain("Main Portfolio"); expect(frame).not.toContain("Ticker Research Body"); expect(actions.some((action) => action.type === "UPDATE_LAYOUT")).toBe(false); }); test("updates the floating pane preview before mouse release", async () => { const config = createDefaultConfig("/tmp/gloomberb-shell-live-floating-drag-test"); const floatingLayout = cloneLayout(config.layout); floatingLayout.dockRoot = { kind: "pane", instanceId: "portfolio-list:main" }; floatingLayout.floating = [{ instanceId: "ticker-detail:main", x: 8, y: 2, width: 32, height: 10, zIndex: 75 }]; await renderShellForWindowModeTest( createShellStateWithLayout(config, floatingLayout, "ticker-detail:main"), { width: 80, height: 18 }, ); await act(async () => { await testSetup!.mockMouse.pressDown(10, 3); await testSetup!.renderOnce(); await testSetup!.mockMouse.moveTo(16, 6); await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); const frame = testSetup!.captureCharFrame(); const rows = frame.split("\n"); expect(rows[2]?.indexOf(":: Main Portfolio") ?? -1).toBeLessThan(0); expect(rows[5]?.indexOf(":: Main Portfolio")).toBeGreaterThanOrEqual(14); await act(async () => { await testSetup!.mockMouse.release(16, 6); await testSetup!.renderOnce(); }); }); test("keeps the focused textarea cursor visible when it is not covered", async () => { const config = createDefaultConfig("/tmp/gloomberb-shell-cursor-visible-test"); const mainPane = requireLayoutInstance(config, "portfolio-list:main"); const detailPane = requireLayoutInstance(config, "ticker-detail:main"); const registry = createShellPluginRegistry({ portfolioListComponent: ({ focused, width, height }) => (