import { afterAll, beforeAll, describe, expect, test } from "bun:test"; import { GlobalRegistrator } from "@happy-dom/global-registrator"; import { act, type ReactNode } from "react"; import { createRoot as createReactRoot } from "react-dom/client"; import { AppContext, type AppContextValue } from "../../app-context"; import type { Session } from "../../types"; import { SessionHeader } from "./session-header"; // The header only renders inside the session shell, which always has the app // context; its chat actions (copy link, pin) run through it. None of these // tests run one, so any read of the context fails loudly. const appContext = new Proxy({} as AppContextValue, { get(_target, key) { throw new Error(`The SessionHeader test's app context has no ${String(key)}`); }, }); function createRoot(container: Element) { const root = createReactRoot(container); return { render: (node: ReactNode) => root.render({node}), unmount: () => root.unmount(), }; } beforeAll(() => { GlobalRegistrator.register(); ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean } ).IS_REACT_ACT_ENVIRONMENT = true; }); afterAll(() => { GlobalRegistrator.unregister(); }); const session = { id: "session-1", workspaceId: "workspace-1", initialMessage: "Run this exact bash script once in the workspace root.", title: null, titleSource: null, parentSessionId: null, model: "codex/gpt-5.6-sol", reasoningEffort: "high", latencyMode: "standard", metadata: {}, status: "idle", pinned: false, effectiveControl: { state: "active", directState: "active", primaryBlocker: null, additionalBlockerCount: 0, }, } as Session; describe("SessionHeader mobile touch targets", () => { test("keeps the editable session title at least 44px high on coarse pointers", async () => { const container = document.createElement("div"); document.body.append(container); const root = createRoot(container); try { await act(async () => { root.render( undefined} inspectorOpen={false} onToggleInspector={() => undefined} onRename={async () => null} onPin={async () => null} />, ); }); const title = container.querySelector( 'button[title$="· click to rename"]', ); expect(title).not.toBeNull(); expect(title!.className).toContain("pointer-coarse:min-h-11"); } finally { await act(async () => root.unmount()); container.remove(); } }); test("renders access beside the title actions and names workspace open/hide state", async () => { const container = document.createElement("div"); document.body.append(container); const root = createRoot(container); const render = (inspectorOpen: boolean) => ( undefined} inspectorOpen={inspectorOpen} onToggleInspector={() => undefined} onRename={async () => null} onPin={async () => null} accessSlot={Private} /> ); try { await act(async () => root.render(render(false))); const access = container.querySelector('[data-testid="session-access-slot"]'); const title = container.querySelector('button[title$="· click to rename"]'); expect(access).not.toBeNull(); expect(title).not.toBeNull(); expect(access?.parentElement?.contains(title ?? null)).toBe(true); expect(container.querySelector('[aria-label="Open workspace"]')).not.toBeNull(); await act(async () => root.render(render(true))); expect(container.querySelector('[aria-label="Hide workspace"]')).not.toBeNull(); } finally { await act(async () => root.unmount()); container.remove(); } }); }); test("reuses the compact Schedule button without requiring creation metadata", async () => { const container = document.createElement("div"); document.body.append(container); const root = createRoot(container); let opened = 0; const render = (linked: boolean) => ( undefined} inspectorOpen={false} onToggleInspector={() => undefined} onRename={async () => null} onPin={async () => null} onOpenSchedule={ linked ? () => { opened++; } : null } /> ); try { await act(async () => root.render(render(true))); const button = container.querySelector( 'button[title="Open schedules for this session"]', ); expect(button?.textContent?.trim()).toBe("Schedule"); await act(async () => button?.click()); expect(opened).toBe(1); await act(async () => root.render(render(false))); expect(container.querySelector('button[title="Open schedules for this session"]')).toBeNull(); // A paused or finished schedule still links to its settings, marked inactive. await act(async () => root.render( undefined} inspectorOpen={false} onToggleInspector={() => undefined} onRename={async () => null} onPin={async () => null} onOpenSchedule={() => { opened++; }} />, ), ); const inactive = container.querySelector( 'button[title="No upcoming runs. Open schedules for this session"]', ); expect(inactive?.textContent?.trim()).toBe("Schedule"); expect(inactive?.querySelector("svg")?.getAttribute("class")).toContain("lucide-calendar-off"); await act(async () => inactive?.click()); expect(opened).toBe(2); } finally { await act(async () => root.unmount()); container.remove(); } }); test("narrow headers keep a compact lifecycle dot instead of hiding status", async () => { const container = document.createElement("div"); document.body.append(container); const root = createRoot(container); const render = (value: Session, status: Session["status"]) => ( undefined} inspectorOpen={false} onToggleInspector={() => undefined} onRename={async () => null} onPin={async () => null} /> ); const compact = () => container.querySelector("[data-compact-session-status]"); try { for (const [status, label] of [ ["running", "Running"], ["failed", "Failed"], ["requires_action", "Waiting on you"], ["waiting_capacity", "Limit reached"], ] as const) { await act(async () => root.render(render(session, status))); expect(compact()?.dataset.compactSessionStatus).toBe(status); expect(compact()?.textContent).toBe(label); expect(compact()?.className).toContain("lg:hidden"); } // A session the runtime could not start is stuck, not waiting on you. await act(async () => root.render( render( { ...session, admissionBlock: { reason: "database_claim_rejected", sqlState: "42501", retryPolicy: "explicit_recheck", blockedAt: "2026-09-25T12:00:00.000Z", }, } as Session, "requires_action", ), ), ); expect(compact()?.dataset.compactSessionStatus).toBe("blocked"); expect(compact()?.textContent).toBe("Stuck"); expect(container.textContent).not.toContain("Waiting on you"); await act(async () => root.render( render( { ...session, inputWait: { deadlineAt: "2026-09-25T12:00:00.000Z", reason: "Waiting for CI" }, } as Session, "idle", ), ), ); expect(compact()?.textContent).toBe("Waiting"); // Only the desktop badge carries the wait marker, so selectors stay unique. expect(container.querySelectorAll("[data-session-wait-badge]")).toHaveLength(1); await act(async () => root.render( render( { ...session, effectiveControl: { ...session.effectiveControl, state: "paused", directState: "paused", }, } as Session, "idle", ), ), ); expect(compact()?.textContent).toBe("Paused"); expect(container.querySelector(".sr-only.lg\\:hidden")?.textContent).toBe("Connection live."); } finally { await act(async () => root.unmount()); container.remove(); } }); test("names the model cleanly for org- and workspace-connected copies alike", async () => { const container = document.createElement("div"); document.body.append(container); const root = createRoot(container); const render = (model: string) => ( undefined} inspectorOpen={false} onToggleInspector={() => undefined} onRename={async () => null} onPin={async () => null} billingClass="claude_subscription" /> ); try { const seen: string[] = []; for (const model of [ "organization-claude-subscription/claude-opus-5-5", "workspace-claude-subscription/claude-opus-5-5", ]) { await act(async () => root.render(render(model))); const text = container.querySelector("header")?.textContent ?? ""; expect(text).toContain("Opus 5.5"); expect(text).not.toContain("claude-subscription"); expect(container.querySelector('[data-model-vendor="anthropic"]')).not.toBeNull(); seen.push(container.querySelector("[data-model-vendor]")?.parentElement?.textContent ?? ""); } expect(seen[0]).toBe(seen[1]); } finally { await act(async () => root.unmount()); container.remove(); } });