import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { act, cleanup, render, waitFor } from "@testing-library/react"; const api = vi.hoisted(() => ({ getUserInfo: vi.fn(async () => ({ is_admin: true })), getUserPreferences: vi.fn(async () => ({ reopenTabsOnLogin: false })), saveUserPreferences: vi.fn(async () => {}), getSSHHosts: vi.fn(async () => [] as unknown[]), getSSHFolders: vi.fn(async () => []), getOpenTabs: vi.fn(async () => []), getActiveSessions: vi.fn(async () => []), addOpenTab: vi.fn(async () => {}), deleteOpenTab: vi.fn(async () => {}), patchOpenTab: vi.fn(async () => {}), createSSHHost: vi.fn(async () => ({})), })); vi.mock("@/main-axios", () => api); vi.mock("@/hooks/use-sync-status", () => ({ useSyncStatus: () => undefined, useSyncAttentionCount: () => 0, })); import { ShellControllerProvider, useShellController, } from "@/shell/controller/ShellControllerProvider"; import { getShellController } from "@/shell/controller/shell-store"; import type { ShellController } from "@/shell/controller/types"; import { registerTabType, resetTabTypes } from "@/shell/tab-registry"; import { resetShellBridge, shell as bridgeShell, } from "@/plugin-host/shell-bridge"; let shell: ShellController | null = null; function Capture() { shell = useShellController(); return null; } const logout = vi.fn(); function mount() { return render( , ); } beforeEach(() => { localStorage.clear(); shell = null; logout.mockClear(); registerTabType({ id: "notes", component: () => null, hostless: true, multiInstance: true, }); }); afterEach(() => { cleanup(); resetTabTypes(); resetShellBridge(); }); describe("ShellControllerProvider", () => { it("starts on the dashboard and knows the user", async () => { mount(); await waitFor(() => expect(shell?.isAdmin).toBe(true)); expect(shell!.username).toBe("ada"); expect(shell!.tabs.list.map((tab) => tab.id)).toEqual(["dashboard"]); expect(shell!.tabs.activeId).toBe("dashboard"); }); it("opens, renames and closes tabs, back to the dashboard", async () => { mount(); await waitFor(() => expect(shell).not.toBeNull()); let id = ""; act(() => { id = shell!.tabs.openMultiInstance("notes"); }); expect(shell!.tabs.activeId).toBe(id); act(() => shell!.tabs.rename(id, "Mine")); expect(shell!.tabs.list.find((tab) => tab.id === id)?.label).toBe("Mine"); await act(async () => { await shell!.tabs.close(id); }); expect(shell!.tabs.list.map((tab) => tab.id)).toEqual(["dashboard"]); expect(shell!.tabs.activeId).toBe("dashboard"); }); it("opens the settings singleton once and passes the section on", async () => { mount(); await waitFor(() => expect(shell).not.toBeNull()); act(() => { shell!.tabs.openSingleton("settings"); }); act(() => { shell!.tabs.openSingleton("admin-settings"); }); const settings = shell!.tabs.list.filter((tab) => tab.id === "settings"); expect(settings).toHaveLength(1); expect(settings[0].data).toMatchObject({ section: "admin-general" }); }); it("toggles the palette from the keyboard and the controller", async () => { mount(); await waitFor(() => expect(shell).not.toBeNull()); act(() => { window.dispatchEvent( new KeyboardEvent("keydown", { key: "k", code: "KeyK", ctrlKey: true }), ); }); expect(shell!.palette.isOpen).toBe(true); act(() => shell!.palette.close()); expect(shell!.palette.isOpen).toBe(false); }); it("publishes itself for plugins and the old bridge", async () => { mount(); await waitFor(() => expect(getShellController()).not.toBeNull()); act(() => bridgeShell.openSingletonTab("plugins")); await waitFor(() => expect(shell!.tabs.list.some((tab) => tab.id === "plugins")).toBe(true), ); act(() => bridgeShell.openRailView("credentials")); expect(shell!.regions.left).toMatchObject({ view: "credentials", open: true, }); }); it("logs out on the logout event", async () => { mount(); await waitFor(() => expect(shell).not.toBeNull()); act(() => { window.dispatchEvent( new CustomEvent("termix:logout", { detail: { manual: true } }), ); }); expect(logout).toHaveBeenCalledWith({ manual: true }); }); it("splits the active tab and closes the split", async () => { mount(); await waitFor(() => expect(shell).not.toBeNull()); let id = ""; act(() => { id = shell!.tabs.openMultiInstance("notes"); }); act(() => shell!.split.handleAction({ kind: "splitActive", edge: "right" }), ); expect(shell!.split.active).not.toBeNull(); expect(shell!.split.paneIdByTabId.has(id)).toBe(true); const splitId = shell!.split.active!.id; act(() => shell!.split.handleAction({ kind: "unsplit", splitTabId: splitId }), ); expect(shell!.split.active).toBeNull(); expect(shell!.tabs.activeId).toBe(id); }); });