// @vitest-environment happy-dom import { beforeAll, describe, expect, it } from "vitest"; import { renderSidebar } from "./catalog.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")); }); const ID = "11111111-2222-3333-4444-555555555555"; function session(overrides: Partial = {}): Session { return { id: ID, cwd: "/home/naps/tea/maestro", command: "claude", env: {}, metadata: {}, status: "Running", created_at: "2026-08-24T10:00:00Z", ...overrides, }; } /** A store whose updates actually reach subscribers, unlike the one-shot * harness in catalog.test.ts. */ function liveStore(initial: Session[]) { const state = { sessions: new Map(initial.map((s) => [s.id, s])) }; const listeners = new Set(); const store = { snapshot: () => state, subscribe: (fn: CatalogListener) => { listeners.add(fn); fn(state); return () => listeners.delete(fn); }, } as unknown as CatalogStore; const push = (sessions: Session[]) => { state.sessions = new Map(sessions.map((s) => [s.id, s])); for (const fn of listeners) fn(state); }; return { store, push }; } const rowOf = (el: HTMLElement) => el.querySelector(".tree-leaf")!; describe("sidebar refresh keeps rows in place (docs/SIDEBAR.md)", () => { it("patches an activity change into the standing row instead of rebuilding it", () => { const { store, push } = liveStore([session({ activity: "Idle" })]); const { el } = renderSidebar(store, {}); const before = rowOf(el); push([session({ activity: "AwaitingInput" })]); // Same element: a rebuild under the pointer would drop `:hover` and restart // the row's transitions, which is the flicker this avoids. expect(rowOf(el)).toBe(before); // …and the card still tells the truth about the session. expect(before.querySelector(".activity-rail")?.getAttribute("data-activity")).toBe( "awaiting-input", ); expect(before.querySelector(".card-line2")?.getAttribute("data-activity")).toBe( "awaiting-input", ); expect(before.getAttribute("aria-label")).toContain("Awaiting input"); }); it("rebuilds when the row set changes", () => { const { store, push } = liveStore([session()]); const { el } = renderSidebar(store, {}); const before = rowOf(el); push([session(), session({ id: "99999999-2222-3333-4444-555555555555", command: "bash" })]); expect(el.querySelectorAll(".tree-leaf")).toHaveLength(2); expect(rowOf(el)).not.toBe(before); }); it("rebuilds when a session dies", () => { const { store, push } = liveStore([session()]); const { el } = renderSidebar(store, {}); const before = rowOf(el); push([session({ status: "Dead" })]); expect(rowOf(el)).not.toBe(before); expect(rowOf(el).dataset.status).toBe("dead"); }); it("rebuilds when a session is renamed", () => { const { store, push } = liveStore([session()]); const { el } = renderSidebar(store, {}); const before = rowOf(el); push([session({ metadata: { name: "harmony" } })]); expect(rowOf(el)).not.toBe(before); expect(rowOf(el).querySelector(".leaf-name")?.textContent).toBe("harmony"); }); });