// @vitest-environment happy-dom // // The sidebar's search field is the touch entry point to the command bar // (#157, docs/COMMAND-BAR.md "Touch entry"): tapping or focusing it asks the // shell to open the bar with an empty query, instead of filtering the tree. import { beforeAll, describe, expect, it } from "vitest"; import { OPEN_COMMAND_BAR_EVENT, renderSidebar } from "./catalog.js"; import { sessionBarCandidates } from "./commandBar.js"; import { matchAndRank } from "./matcher.js"; import type { CatalogListener, CatalogStore } from "./events.js"; import type { Session } from "./types.js"; beforeAll(() => { Object.defineProperty(window, "localStorage", { value: { getItem: () => null, setItem: () => {} }, }); window.fetch = () => Promise.reject(new Error("offline")); }); function session(id: string, cwd: string): Session { return { id, name: id, cwd, command: "claude", env: {}, metadata: {}, status: "Running", created_at: "2026-08-24T10:00:00Z", } as Session; } function storeWith(sessions: Session[]): CatalogStore { const state = { sessions: new Map(sessions.map((s) => [s.id, s])) }; return { snapshot: () => state, subscribe: (fn: CatalogListener) => { fn(state); return () => {}; }, } as unknown as CatalogStore; } function mount(sessions: Session[] = [session("alpha", "/home/n/alpha")]) { const handle = renderSidebar(storeWith(sessions)); document.body.appendChild(handle.el); const search = handle.el.querySelector(".search")!; const opens: number[] = []; const listener = () => opens.push(1); document.addEventListener(OPEN_COMMAND_BAR_EVENT, listener); return { handle, search, opens, dispose: () => { document.removeEventListener(OPEN_COMMAND_BAR_EVENT, listener); handle.dispose(); handle.el.remove(); }, }; } describe("sidebar search field opens the command bar (#157)", () => { it("asks the shell to open the bar when the field is focused", () => { const t = mount(); t.search.dispatchEvent(new FocusEvent("focus")); expect(t.opens).toHaveLength(1); t.dispose(); }); it("opens on a tap, where a focus event may never arrive", () => { const t = mount(); t.search.dispatchEvent(new Event("pointerdown", { bubbles: true })); expect(t.opens).toHaveLength(1); t.dispose(); }); it("does not trap the keyboard in the field: it gives focus up on open", () => { const t = mount(); t.search.focus(); expect(document.activeElement).not.toBe(t.search); t.dispose(); }); it("opens the bar with an empty query — the field never holds one", () => { const t = mount(); t.search.focus(); expect(t.search.value).toBe(""); t.dispose(); }); it("is readonly: no on-screen keyboard over a field nothing is typed into", () => { const t = mount(); expect(t.search.readOnly).toBe(true); t.dispose(); }); it("no longer filters the tree in place", () => { const t = mount([session("alpha", "/home/n/alpha"), session("beta", "/home/n/beta")]); t.search.value = "alpha"; t.search.dispatchEvent(new Event("input")); expect(t.handle.el.querySelectorAll(".tree-leaf")).toHaveLength(2); t.dispose(); }); it("has no 'No matches' state left: an empty catalog is empty, not filtered", () => { const t = mount([]); expect(t.handle.el.querySelector(".empty-title")?.textContent).toBe("No sessions yet"); t.dispose(); }); }); // The inline filter matched a session on its name and its directory, case- and // whitespace-insensitively. That behaviour did not go away with the field — it // moved into the bar, which ranks the same two fields (docs/COMMAND-BAR.md). describe("what the retired inline filter did, the bar now does", () => { it("matches a session on both its name and its directory", () => { const sessions = [session("alpha", "/home/n/alpha"), session("beta", "/home/n/beta")]; const candidates = sessionBarCandidates(sessions, "/home/n"); const byName = matchAndRank("alpha", candidates).map((r) => r.candidate.session.id); expect(byName).toEqual(["alpha"]); const byDir = matchAndRank("beta", candidates).map((r) => r.candidate.session.id); expect(byDir).toEqual(["beta"]); }); it("ignores case and surrounding whitespace, and shows nothing on no match", () => { const candidates = sessionBarCandidates([session("alpha", "/home/n/Alpha")], "/home/n"); expect(matchAndRank(" ALPHA ", candidates)).toHaveLength(1); expect(matchAndRank("zzz", candidates)).toHaveLength(0); }); });