import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react"; import type { ReactNode } from "react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { makeLocalStorageMock } from "@/hooks/local-storage-mock.test-helpers"; import type { Agent, AgentDiscovery } from "@/lib/types/http"; import { InstalledAgentCard } from "./installed-agent-card"; const STORAGE_KEY = "kandev:agents:collapsedBlocks:v1"; const localStorageMock = makeLocalStorageMock(); vi.stubGlobal("localStorage", localStorageMock); Object.defineProperty(window, "localStorage", { value: localStorageMock, configurable: true }); // Chrome around the collapse behavior: dialogs, runtime-update control, and // identity chrome are all irrelevant to the body visibility + header count. vi.mock("@/components/agent-logo", () => ({ AgentLogo: () => , })); vi.mock("@/components/settings/record-badges", () => ({ NotInstalledBadge: () => null, })); vi.mock("@/components/settings/agent-login-dialog", () => ({ AgentLoginDialog: () => null, })); vi.mock("@/components/settings/agent-runtime-update-control", () => ({ AgentRuntimeUpdateControl: () => null, })); vi.mock("@/components/settings/host-shell-dialog", () => ({ HostShellDialog: () => null, })); vi.mock("@/components/routing/app-link", () => ({ default: ({ children, ...props }: { children?: ReactNode }) => {children}, })); vi.mock("@kandev/ui/tooltip", () => ({ Tooltip: ({ children }: { children?: ReactNode }) => <>{children}, TooltipTrigger: ({ children }: { children?: ReactNode }) => <>{children}, TooltipContent: () => null, })); const DISPLAY_NAME = "Claude Code"; function makeDiscovery(name: string): AgentDiscovery { return { name, supports_mcp: false, mcp_config_path: null, installation_paths: [], available: true, matched_path: null, }; } function makeSavedAgent(name: string, profileCount: number): Agent { return { id: `agent-${name}`, name, supports_mcp: false, mcp_config_path: null, profiles: Array.from({ length: profileCount }, (_, i) => ({ id: `p-${name}-${i}`, name: `Profile ${i + 1}`, })), created_at: "2026-01-01T00:00:00Z", updated_at: "2026-01-01T00:00:00Z", } as Agent; } function renderCard(profileCount: number) { const agent = makeDiscovery("claude"); return render(
profiles
, ); } function renderSetupCard(savedAgent?: Agent) { return render(
profiles
, ); } const toggle = () => screen.getByTestId("collapse-agent-claude"); const toggleAriaLabel = () => toggle().getAttribute("aria-label"); const toggleAriaExpanded = () => toggle().getAttribute("aria-expanded"); const body = () => screen.queryByTestId("profiles-body"); const headerCount = () => screen.queryByTestId("collapsed-count-claude"); /** Radix Presence unmounts closed content after the exit animation; in a DOM * test environment either the element is detached or it carries `hidden`. * Both mean the body is not visible to the user. */ function expectBodyNotVisible() { const el = body(); expect(el === null || el.hidden).toBe(true); } describe("InstalledAgentCard collapse", () => { beforeEach(() => { localStorageMock.clear(); }); afterEach(() => { cleanup(); }); it("renders the profiles body expanded by default and stores nothing", () => { renderCard(3); expect(body()).not.toBeNull(); expect(body()!.hidden).toBe(false); expect(window.localStorage.getItem(STORAGE_KEY)).toBeNull(); expect(toggleAriaExpanded()).toBe("true"); expect(toggleAriaLabel()).toBe("Collapse Claude Code profiles"); }); it("collapses the body, shows the count in the header, and persists", async () => { renderCard(3); fireEvent.click(toggle()); await waitFor(() => expectBodyNotVisible()); expect(headerCount()?.textContent).toContain("3 profiles"); expect(window.localStorage.getItem(STORAGE_KEY)).toBe(JSON.stringify({ claude: true })); expect(toggleAriaExpanded()).toBe("false"); expect(toggleAriaLabel()).toBe("Expand Claude Code profiles"); }); it("mounts collapsed when a collapsed entry is stored and shows the count", async () => { window.localStorage.setItem(STORAGE_KEY, JSON.stringify({ claude: true })); renderCard(3); await waitFor(() => expectBodyNotVisible()); expect(headerCount()?.textContent).toContain("3 profiles"); expect(toggleAriaExpanded()).toBe("false"); }); it("expanding restores the body and removes the count from the header", async () => { window.localStorage.setItem(STORAGE_KEY, JSON.stringify({ claude: true })); renderCard(3); await waitFor(() => expectBodyNotVisible()); fireEvent.click(toggle()); await waitFor(() => expect(body()?.hidden).toBe(false)); expect(headerCount()).toBeNull(); expect(toggleAriaExpanded()).toBe("true"); }); it("keeps other agents' collapse entries from affecting this card", () => { window.localStorage.setItem(STORAGE_KEY, JSON.stringify({ codex: true })); renderCard(3); expect(body()).not.toBeNull(); expect(body()!.hidden).toBe(false); }); it("shows No profiles yet in the header for a collapsed zero-profile agent", async () => { renderCard(0); fireEvent.click(toggle()); await waitFor(() => expectBodyNotVisible()); expect(headerCount()?.textContent).toContain("No profiles yet"); }); it("places the count left of the collapse button while collapsed", async () => { renderCard(3); fireEvent.click(toggle()); await waitFor(() => expectBodyNotVisible()); const count = headerCount(); expect(count).not.toBeNull(); expect( count!.compareDocumentPosition(toggle()) & Node.DOCUMENT_POSITION_FOLLOWING, ).toBeTruthy(); }); it("applies the toggle in memory and never throws when persisting the collapse fails", () => { renderCard(3); const original = localStorageMock.setItem; localStorageMock.setItem = () => { throw new Error("quota exceeded"); }; try { expect(() => fireEvent.click(toggle())).not.toThrow(); // The write failed, but the collapse still applies for the session. expectBodyNotVisible(); expect(toggleAriaExpanded()).toBe("false"); expect(window.localStorage.getItem(STORAGE_KEY)).toBeNull(); } finally { localStorageMock.setItem = original; } }); it("lets a collapsed card expand when persisting the change fails", async () => { window.localStorage.setItem(STORAGE_KEY, JSON.stringify({ claude: true })); renderCard(3); await waitFor(() => expectBodyNotVisible()); const original = localStorageMock.setItem; localStorageMock.setItem = () => { throw new Error("quota exceeded"); }; try { expect(() => fireEvent.click(toggle())).not.toThrow(); await waitFor(() => expect(body()?.hidden).toBe(false)); expect(headerCount()).toBeNull(); expect(toggleAriaExpanded()).toBe("true"); } finally { localStorageMock.setItem = original; } }); }); describe("InstalledAgentCard setup links", () => { afterEach(() => { cleanup(); }); it("uses the base setup route when no agent record exists", () => { renderSetupCard(); expect(screen.getByTestId("setup-profile-mock-agent").getAttribute("href")).toBe( "/settings/agents/mock-agent", ); }); it("uses create mode when a saved agent has no profiles", () => { renderSetupCard(makeSavedAgent("mock-agent", 0)); expect(screen.getByTestId("setup-profile-mock-agent").getAttribute("href")).toBe( "/settings/agents/mock-agent?mode=create", ); }); });