// @vitest-environment jsdom import React from "react"; import { act } from "react"; import { createRoot } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; // ── Mocks ───────────────────────────────────────────────────────────────────── vi.mock("next/link", () => ({ default: ({ href, children, ...props }: React.AnchorHTMLAttributes & { href: string }) => ( {children} ), })); vi.mock("next-intl", () => ({ useTranslations: () => (key: string) => key, useLocale: () => "en", })); // Stub fetch globally const mockFetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ connections: [], keys: [], data: [], cloudEnabled: false }), }); vi.stubGlobal("fetch", mockFetch); // Stub next/navigation vi.mock("next/navigation", () => ({ notFound: () => { throw new Error("NOT_FOUND"); }, })); // Stub CLI_TOOLS catalog vi.mock("@/shared/constants/cliTools", () => ({ CLI_TOOLS: { claude: { id: "claude", name: "Claude Code", icon: "terminal", color: "#D97757", category: "code", configType: "env", vendor: "Anthropic", baseUrlSupport: "full", defaultModels: [], }, codex: { id: "codex", name: "Codex", icon: "terminal", color: "#000", category: "code", configType: "custom", vendor: "OpenAI", baseUrlSupport: "full", defaultModels: [], }, custom: { id: "custom", name: "Custom CLI", icon: "terminal", color: "#888", category: "code", configType: "custom-builder", vendor: undefined, baseUrlSupport: "full", defaultModels: [], }, "hermes-agent": { id: "hermes-agent", name: "Hermes Agent", icon: "terminal", color: "#5865f2", category: "agent", configType: "custom", vendor: "HermesAI", baseUrlSupport: "full", defaultModels: [], }, forge: { id: "forge", name: "Forge", icon: "terminal", color: "#888", category: "code", configType: "custom", vendor: undefined, baseUrlSupport: "partial", defaultModels: [], }, }, })); // Stub model constants vi.mock("@/shared/constants/models", () => ({ PROVIDER_ID_TO_ALIAS: {}, getModelsByProviderId: () => [], })); // Stub specialized cards — render a testid so we can identify which was rendered vi.mock("../../../src/app/(dashboard)/dashboard/cli-code/components/index", () => ({ ClaudeToolCard: () =>
, CodexToolCard: () =>
, DroidToolCard: () =>
, OpenClawToolCard: () =>
, ClineToolCard: () =>
, KiloToolCard: () =>
, DefaultToolCard: ({ toolId }: { toolId: string }) => (
), AntigravityToolCard: () =>
, CopilotToolCard: () =>
, CustomCliCard: () =>
, HermesAgentToolCard: () =>
, })); vi.mock("../../../src/app/(dashboard)/dashboard/cli-code/components/CliproxyapiToolCard", () => ({ default: () =>
, })); // ── Import after mocks ──────────────────────────────────────────────────────── const { default: ToolDetailClient } = await import( "@/app/(dashboard)/dashboard/cli-code/components/ToolDetailClient" ); // ── Helpers ─────────────────────────────────────────────────────────────────── const containers: HTMLElement[] = []; function renderDetail(toolId: string, category: "code" | "agent"): HTMLElement { const container = document.createElement("div"); document.body.appendChild(container); containers.push(container); const root = createRoot(container); act(() => { root.render(); }); return container; } // ── Lifecycle ───────────────────────────────────────────────────────────────── beforeEach(() => { ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } ).IS_REACT_ACT_ENVIRONMENT = true; mockFetch.mockClear(); }); afterEach(() => { while (containers.length > 0) { containers.pop()?.remove(); } document.body.innerHTML = ""; }); // ── Tests ───────────────────────────────────────────────────────────────────── describe("ToolDetailClient", () => { it("renders ClaudeToolCard for toolId=claude", async () => { const container = renderDetail("claude", "code"); // Wait for async state resolution await act(async () => {}); expect(container.querySelector("[data-testid='ClaudeToolCard']")).not.toBeNull(); }); it("renders CodexToolCard for toolId=codex", async () => { const container = renderDetail("codex", "code"); await act(async () => {}); expect(container.querySelector("[data-testid='CodexToolCard']")).not.toBeNull(); }); it("renders CustomCliCard for toolId=custom", async () => { const container = renderDetail("custom", "code"); await act(async () => {}); expect(container.querySelector("[data-testid='CustomCliCard']")).not.toBeNull(); }); it("renders DefaultToolCard for unknown tool (forge, configType:custom)", async () => { const container = renderDetail("forge", "code"); await act(async () => {}); const card = container.querySelector("[data-testid='DefaultToolCard']"); expect(card).not.toBeNull(); expect(card!.getAttribute("data-toolid")).toBe("forge"); }); it("renders nothing (null) for completely unknown toolId", async () => { const container = renderDetail("totally-unknown-xyz", "code"); await act(async () => {}); // CLI_TOOLS["totally-unknown-xyz"] is undefined → returns null → empty container expect(container.textContent).toBe(""); }); });