import { scopeThreadRef } from "@t3tools/client-runtime"; import { type EnvironmentId, ThreadId } from "@t3tools/contracts"; import { beforeEach, describe, expect, it } from "vite-plus/test"; import { migratePersistedRightPanelState, selectActiveRightPanel, selectActiveRightPanelSurface, selectActiveRightPanelKindWithUrl, selectThreadRightPanelState, useRightPanelStore, } from "./rightPanelStore"; const refA = scopeThreadRef("env-1" as EnvironmentId, ThreadId.make("thread-A")); const refB = scopeThreadRef("env-1" as EnvironmentId, ThreadId.make("thread-B")); beforeEach(() => { useRightPanelStore.setState({ byThreadKey: {} }); }); describe("rightPanelStore", () => { it("drops the legacy singleton terminal surface during migration", () => { expect( migratePersistedRightPanelState({ byThreadKey: { "env-1:thread-A": { activeSurfaceId: "terminal", surfaces: [ { id: "browser:tab-a", kind: "preview", resourceId: "tab-a" }, { id: "terminal", kind: "terminal" }, ], }, }, }), ).toEqual({ byThreadKey: { "env-1:thread-A": { isOpen: false, activeSurfaceId: null, surfaces: [{ id: "browser:tab-a", kind: "preview", resourceId: "tab-a" }], }, }, }); }); it("upgrades saved single-session terminal surfaces to split-capable surfaces", () => { expect( migratePersistedRightPanelState({ byThreadKey: { "env-1:thread-A": { isOpen: true, activeSurfaceId: "terminal:term-1", surfaces: [{ id: "terminal:term-1", kind: "terminal", resourceId: "term-1" }], }, }, }), ).toEqual({ byThreadKey: { "env-1:thread-A": { isOpen: true, activeSurfaceId: "terminal:term-1", surfaces: [ { id: "terminal:term-1", kind: "terminal", resourceId: "term-1", terminalIds: ["term-1"], activeTerminalId: "term-1", }, ], }, }, }); }); it("open sets the active panel for a thread", () => { useRightPanelStore.getState().open(refA, "preview"); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("preview"); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refB)).toBeNull(); }); it("opening a different kind keeps both surfaces and activates the new one", () => { useRightPanelStore.getState().open(refA, "plan"); useRightPanelStore.getState().open(refA, "preview"); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("preview"); expect( selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces, ).toHaveLength(2); }); it("close hides the panel without clearing its selected surface", () => { useRightPanelStore.getState().open(refA, "plan"); useRightPanelStore.getState().close(refA); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBeNull(); expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ isOpen: false, activeSurfaceId: "plan", surfaces: [{ id: "plan", kind: "plan" }], }); }); it("toggles empty panel visibility without creating a surface", () => { useRightPanelStore.getState().toggleVisibility(refA); expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ isOpen: true, activeSurfaceId: null, surfaces: [], }); useRightPanelStore.getState().toggleVisibility(refA); expect(useRightPanelStore.getState().byThreadKey).toEqual({}); }); it("toggle hides the panel without discarding the active surface", () => { useRightPanelStore.getState().toggle(refA, "diff"); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("diff"); useRightPanelStore.getState().toggle(refA, "diff"); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBeNull(); expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ isOpen: false, activeSurfaceId: "diff", surfaces: [{ id: "diff", kind: "diff" }], }); }); it("toggle to a different kind switches active", () => { useRightPanelStore.getState().toggle(refA, "preview"); useRightPanelStore.getState().toggle(refA, "plan"); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("plan"); }); it("?diff=1 always wins over persisted state", () => { useRightPanelStore.getState().open(refA, "preview"); expect( selectActiveRightPanelKindWithUrl(useRightPanelStore.getState().byThreadKey, refA, true), ).toBe("diff"); expect( selectActiveRightPanelKindWithUrl(useRightPanelStore.getState().byThreadKey, refA, false), ).toBe("preview"); }); it("removeThread clears persisted state", () => { useRightPanelStore.getState().open(refA, "plan"); useRightPanelStore.getState().removeThread(refA); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBeNull(); }); it("close on never-opened thread is a no-op", () => { useRightPanelStore.getState().close(refA); expect(useRightPanelStore.getState().byThreadKey).toEqual({}); }); it("tracks one surface per browser session", () => { useRightPanelStore.getState().openBrowser(refA, "tab-a"); useRightPanelStore.getState().openBrowser(refA, "tab-b"); const state = selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA); expect(state.surfaces.map((surface) => surface.id)).toEqual(["browser:tab-a", "browser:tab-b"]); expect(selectActiveRightPanelSurface(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ id: "browser:tab-b", kind: "preview", resourceId: "tab-b", }); }); it("tracks one surface per terminal session", () => { useRightPanelStore.getState().openTerminal(refA, "term-1"); useRightPanelStore.getState().openTerminal(refA, "term-2"); const state = selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA); expect(state.surfaces).toEqual([ { id: "terminal:term-1", kind: "terminal", resourceId: "term-1", terminalIds: ["term-1"], activeTerminalId: "term-1", }, { id: "terminal:term-2", kind: "terminal", resourceId: "term-2", terminalIds: ["term-2"], activeTerminalId: "term-2", }, ]); expect(state.activeSurfaceId).toBe("terminal:term-2"); }); it("tracks split panes and the active pane within a terminal surface", () => { useRightPanelStore.getState().openTerminal(refA, "term-1"); useRightPanelStore.getState().splitTerminal(refA, "terminal:term-1", "term-2"); expect(selectActiveRightPanelSurface(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ id: "terminal:term-1", kind: "terminal", resourceId: "term-1", terminalIds: ["term-1", "term-2"], activeTerminalId: "term-2", }); useRightPanelStore.getState().activateTerminal(refA, "terminal:term-1", "term-1"); useRightPanelStore.getState().closeTerminal(refA, "terminal:term-1", "term-1"); expect(selectActiveRightPanelSurface(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ id: "terminal:term-1", kind: "terminal", resourceId: "term-1", terminalIds: ["term-2"], activeTerminalId: "term-2", }); }); it("tracks vertical layout for a terminal surface", () => { useRightPanelStore.getState().openTerminal(refA, "term-1"); useRightPanelStore.getState().splitTerminal(refA, "terminal:term-1", "term-2", "vertical"); expect(selectActiveRightPanelSurface(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ id: "terminal:term-1", kind: "terminal", resourceId: "term-1", terminalIds: ["term-1", "term-2"], activeTerminalId: "term-2", splitDirection: "vertical", }); }); it("closing the final terminal pane removes its surface but keeps the panel open", () => { useRightPanelStore.getState().openTerminal(refA, "term-1"); useRightPanelStore.getState().closeTerminal(refA, "terminal:term-1", "term-1"); expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ isOpen: true, activeSurfaceId: null, surfaces: [], }); }); it("closing the active surface activates a neighboring surface", () => { useRightPanelStore.getState().openBrowser(refA, "tab-a"); useRightPanelStore.getState().openTerminal(refA, "term-1"); useRightPanelStore.getState().closeSurface(refA, "terminal:term-1"); expect(selectActiveRightPanelSurface(useRightPanelStore.getState().byThreadKey, refA)?.id).toBe( "browser:tab-a", ); }); it("closing the final surface leaves the panel open and empty", () => { useRightPanelStore.getState().openTerminal(refA, "term-1"); useRightPanelStore.getState().closeSurface(refA, "terminal:term-1"); expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ isOpen: true, activeSurfaceId: null, surfaces: [], }); }); it("reconciles browser surfaces without deleting other surface kinds", () => { useRightPanelStore.getState().openTerminal(refA, "term-1"); useRightPanelStore.getState().openBrowser(refA, "tab-a"); useRightPanelStore.getState().openBrowser(refA, "tab-b"); useRightPanelStore.getState().reconcileBrowserSurfaces(refA, ["tab-b", "tab-c"]); expect( selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces.map( (surface) => surface.id, ), ).toEqual(["terminal:term-1", "browser:tab-b", "browser:tab-c"]); }); });