// @vitest-environment happy-dom import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { KILL_SESSION_EVENT, RENAME_SESSION_EVENT, SESSION_SELECT_EVENT, SPAWN_SIMILAR_EVENT, } from "../../uiEvents.js"; import { COMMAND_SPECS, type Command } from "../../commands.js"; import type { Session } from "../../types.js"; import { CommandBar, SPAWN_DIRECTORY_EVENT, type CommandBarProps } from "./CommandBar.js"; vi.mock("../../api.js", async (importOriginal) => ({ ...(await importOriginal>()), listDir: vi.fn(async () => ({ path: "", entries: [] })), projects: vi.fn(async () => []), recentDirs: vi.fn(async () => ({ dirs: [] })), })); function session(id: string, over: Partial = {}): Session { return { id, cwd: "/home/me/dev/api", command: "bash", env: {}, metadata: { name: id }, status: "Running", created_at: "2026-01-01T00:00:00Z", ...over, }; } function command(over: Partial = {}): Command { return { id: "cmd", label: "A command", group: "Test", keys: ["a"], scope: "global", run: () => {}, ...over, }; } const sessions = [ session("api", { current_cwd: "/home/me/dev/api", last_focused_at: "2026-01-01T11:00:00Z" }), session("worker", { current_cwd: "/home/me/dev/worker", last_focused_at: "2026-01-01T10:00:00Z", }), session("notes", { current_cwd: "/home/me/notes" }), ]; function open(over: Partial = {}) { return render( {}} onClose={() => {}} {...over} />, ); } function input(): HTMLInputElement { return screen.getByRole("combobox") as HTMLInputElement; } function type(value: string): void { fireEvent.change(input(), { target: { value } }); } function press(key: string, init: Record = {}): void { fireEvent.keyDown(input(), { key, ...init }); } function rowNames(): string[] { return [...document.querySelectorAll(".cmdbar-row-name")].map((r) => r.textContent ?? ""); } function activeName(): string | undefined { return document.querySelector(".cmdbar-row.is-active .cmdbar-row-name")?.textContent ?? undefined; } afterEach(() => { cleanup(); vi.clearAllMocks(); }); describe("the overlay", () => { it("is a registry command, so the help overlay lists it", () => { const spec = COMMAND_SPECS.find((s) => s.id === "bar.open"); expect(spec?.keys).toEqual(["ctrl-k"]); expect(spec?.scope).toBe("global"); }); it("lists sessions by recency under a kind header on an empty query", () => { open(); expect([...document.querySelectorAll(".cmdbar-group-title")].map((h) => h.textContent)).toEqual([ "Sessions", ]); expect(rowNames()).toEqual(["api", "worker", "notes"]); expect(activeName()).toBe("api"); }); it("ranks a typed query through the matcher and highlights the matched range", () => { open(); type("wor"); expect(rowNames()).toEqual(["worker"]); expect([...document.querySelectorAll(".cmdbar-row-name mark")].map((m) => m.textContent)).toEqual( ["wor"], ); }); it("highlights inside the directory when that is the field that matched", () => { open(); type("dev/wor"); expect(rowNames()).toEqual(["worker"]); expect( [...document.querySelectorAll(".cmdbar-row-meta mark")].map((m) => m.textContent).join(""), ).toBe("dev/wor"); }); it("renders a no-results state rather than an empty list", () => { open(); type("zzz"); expect(rowNames()).toEqual([]); expect(screen.getByText("No matches")).toBeDefined(); }); it("moves the highlight with the arrows and ctrl-p/ctrl-n, without wrapping", () => { open(); press("ArrowUp"); expect(activeName()).toBe("api"); press("ArrowDown"); expect(activeName()).toBe("worker"); press("n", { ctrlKey: true }); expect(activeName()).toBe("notes"); press("ArrowDown"); expect(activeName()).toBe("notes"); press("p", { ctrlKey: true }); expect(activeName()).toBe("worker"); }); it("puts the highlight back on the best row whenever the query changes", () => { open(); press("ArrowDown"); expect(activeName()).toBe("worker"); type("o"); expect(activeName()).toBe(rowNames()[0]); }); it("opens the highlighted session through the caller's open path on Enter", () => { const onOpenSession = vi.fn(); const onClose = vi.fn(); open({ onOpenSession, onClose }); press("ArrowDown"); press("Enter"); expect(onOpenSession).toHaveBeenCalledWith("worker"); expect(onClose).toHaveBeenCalled(); }); it("raises the shared select event when the caller supplies no handler", () => { const listener = vi.fn(); document.addEventListener(SESSION_SELECT_EVENT, listener); open({ onOpenSession: undefined }); press("Enter"); document.removeEventListener(SESSION_SELECT_EVENT, listener); expect((listener.mock.calls[0][0] as CustomEvent).detail).toBe("api"); }); it("Enter on an empty result list does nothing", () => { const onOpenSession = vi.fn(); const onClose = vi.fn(); open({ onOpenSession, onClose }); type("zzz"); press("Enter"); expect(onOpenSession).not.toHaveBeenCalled(); expect(onClose).not.toHaveBeenCalled(); }); it("closes on Escape and on a press outside the panel", () => { const onClose = vi.fn(); open({ onClose }); press("Escape"); expect(onClose).toHaveBeenCalledTimes(1); fireEvent.mouseDown(document.querySelector(".cmdbar-overlay") as HTMLElement); expect(onClose).toHaveBeenCalledTimes(2); }); it("stays open for a press inside the panel", () => { const onClose = vi.fn(); open({ onClose }); fireEvent.mouseDown(document.querySelector(".cmdbar-panel") as HTMLElement); expect(onClose).not.toHaveBeenCalled(); }); it("returns the keyboard to whatever held it when it opened", () => { const before = document.createElement("input"); document.body.appendChild(before); before.focus(); const view = open(); expect(document.activeElement).toBe(input()); view.unmount(); expect(document.activeElement).toBe(before); before.remove(); }); it("clicking a row opens that session", () => { const onOpenSession = vi.fn(); open({ onOpenSession }); fireEvent.click(document.querySelectorAll(".cmdbar-row")[1]); expect(onOpenSession).toHaveBeenCalledWith("worker"); }); }); describe("the sigils", () => { const commands = [ command({ id: "help.toggle", label: "Show help", keys: ["?"] }), command({ id: "spawn.open", label: "Open the spawn form", keys: ["n"], run: vi.fn() }), ]; it("lists every command in registry order under `>`, with its chord and no header", () => { open({ commands }); type(">"); expect(rowNames()).toEqual(["Show help", "Open the spawn form"]); expect(document.querySelectorAll(".cmdbar-group-title")).toHaveLength(0); expect([...document.querySelectorAll(".cmdbar-row-keys kbd")].map((k) => k.textContent)).toEqual( ["?", "n"], ); }); it("matches a command on its label and never on its chord", () => { open({ commands }); type(">help"); expect(rowNames()).toEqual(["Show help"]); type(">?"); expect(rowNames()).toEqual([]); }); it("runs the highlighted command on Enter", () => { const run = vi.fn(); open({ commands: [command({ label: "Run me", run })] }); type(">run"); press("Enter"); expect(run).toHaveBeenCalled(); }); it("keeps the list mixed with no sigil, and returns to it when the sigil goes", () => { open({ commands }); type(">show"); expect(rowNames()).toEqual(["Show help"]); type(""); expect(rowNames()).toEqual(["api", "worker", "notes"]); }); it("lists recent dirs then projects under `~`", async () => { open({ loadDirectories: async () => ["/home/me/dev/api", "/srv/x"] }); type("~"); await waitFor(() => expect(rowNames()).toEqual(["~/dev/api", "/srv/x"])); expect(document.querySelectorAll(".cmdbar-group-title")).toHaveLength(0); }); it("fills the list in from /api/dirs as a path is typed", async () => { const listDirEntries = vi.fn(async () => ["maestro", "api"]); open({ loadDirectories: async () => ["/home/me/notes"], listDirEntries }); // The second `~` is a literal query character and reaches the matcher. type("~~/dev/mae"); await waitFor(() => expect(rowNames()).toEqual(["~/dev/maestro"])); expect(listDirEntries).toHaveBeenCalledWith("~/dev/"); }); it("spawns in the highlighted directory on Enter", async () => { const onSpawnDirectory = vi.fn(); open({ loadDirectories: async () => ["/srv/x"], onSpawnDirectory }); type("~"); await waitFor(() => expect(rowNames()).toEqual(["/srv/x"])); press("Enter"); expect(onSpawnDirectory).toHaveBeenCalledWith("/srv/x"); }); it("raises the spawn event when the caller supplies no handler", async () => { const listener = vi.fn(); document.addEventListener(SPAWN_DIRECTORY_EVENT, listener); open({ loadDirectories: async () => ["/srv/x"] }); type("~"); await waitFor(() => expect(rowNames()).toEqual(["/srv/x"])); press("Enter"); document.removeEventListener(SPAWN_DIRECTORY_EVENT, listener); expect((listener.mock.calls[0][0] as CustomEvent).detail).toBe("/srv/x"); }); }); describe("the session action page", () => { function drill(over: Partial = {}) { const view = open(over); press("Tab"); return view; } it("lists kill, rename and spawn similar for the highlighted session", () => { drill(); expect(rowNames()).toEqual(["Kill session", "Rename session", "Spawn similar"]); }); it("names the session being acted on", () => { drill(); expect(screen.getByText('Actions for "api"')).toBeDefined(); }); it("drops the kind headers, since the page is one kind", () => { drill(); expect(document.querySelectorAll(".cmdbar-group-title")).toHaveLength(0); }); it("stays fuzzy-searchable over the three actions", () => { drill(); type("ren"); expect(rowNames()).toEqual(["Rename session"]); }); it("does nothing on a command row", () => { open({ commands: [command({ label: "Show help" })] }); type(">show"); press("Tab"); expect(rowNames()).toEqual(["Show help"]); }); it("does nothing on a directory row", async () => { open({ loadDirectories: async () => ["/srv/x"] }); type("~"); await waitFor(() => expect(rowNames()).toEqual(["/srv/x"])); press("Tab"); expect(rowNames()).toEqual(["/srv/x"]); }); it("returns to the result list on Escape, with the previous query intact", () => { const onClose = vi.fn(); open({ onClose }); type("wor"); press("Tab"); expect(rowNames()).toEqual(["Kill session", "Rename session", "Spawn similar"]); press("Escape"); expect(onClose).not.toHaveBeenCalled(); expect(input().value).toBe("wor"); expect(rowNames()).toEqual(["worker"]); }); it("returns to the result list on backspace at an empty query", () => { drill(); fireEvent.keyDown(input(), { key: "Backspace" }); expect(rowNames()).toEqual(["api", "worker", "notes"]); }); it("keeps backspace ordinary while the page's query has text", () => { drill(); type("ren"); fireEvent.keyDown(input(), { key: "Backspace" }); expect(rowNames()).toEqual(["Rename session"]); }); it("closes the bar when the page's Escape has already been used", () => { const onClose = vi.fn(); drill({ onClose }); press("Escape"); press("Escape"); expect(onClose).toHaveBeenCalledTimes(1); }); it("raises kill, rename and spawn similar with the session, closing the bar", () => { for (const [query, key] of [ ["kill", "onKillSession"], ["rename", "onRenameSession"], ["spawn", "onSpawnSimilar"], ] as const) { const handler = vi.fn(); const onClose = vi.fn(); drill({ [key]: handler, onClose }); type(query); press("Enter"); expect(handler).toHaveBeenCalledWith(expect.objectContaining({ id: "api" })); expect(onClose).toHaveBeenCalled(); cleanup(); } }); it("hands each action to the flow it already has, by default", () => { for (const [query, event] of [ ["kill", KILL_SESSION_EVENT], ["rename", RENAME_SESSION_EVENT], ["spawn", SPAWN_SIMILAR_EVENT], ] as const) { const listener = vi.fn(); document.addEventListener(event, listener); drill(); type(query); press("Enter"); document.removeEventListener(event, listener); expect((listener.mock.calls[0][0] as CustomEvent).detail.id).toBe("api"); cleanup(); } }); });