// @vitest-environment jsdom /** * Unit tests for TestBenchAccordion (F6). * * Covers: * - Smoke render (default closed — lazy-render guard) * - Lazy-render: content not mounted when accordion is closed * - forceOpen=true mounts content immediately * - "Run All" fires 8 sequential fetches (translate + send each) * - Results state transitions: running → pass * - Per-scenario re-run fires only that scenario's fetches * - Error display: error message shown without stack trace * - Error sanitization: stack trace patterns not leaked to UI */ import React from "react"; import { act } from "react"; import { createRoot } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; // ── i18n stub ────────────────────────────────────────────────────────────── vi.mock("next-intl", () => ({ useTranslations: () => (key: string, params?: Record) => { // Return key-based human-readable labels for assertions if (key === "runAllTests") return "Run All Tests"; if (key === "runTest") return "Run Test"; if (key === "reRun") return "Re-Run"; if (key === "running") return "Running..."; if (key === "passed") return "passed"; if (key === "failed") return "failed"; if (key === "compatibilityReport") return "Compatibility Report"; if (key === "passedIconLabel") return "✓ Passed"; if (key === "chunks") return "chunks"; if (key === "source") return "Source"; if (key === "targetProvider") return "Target Provider"; if (key === "model") return "Model"; if (key === "modelPlaceholder") return "Enter model name"; if (key === "compatibilityTester") return "Compatibility Tester"; if (key === "testBenchDescription") return "Run translation scenarios"; if (key === "noTemplateForFormat") return "No template for format"; if (key === "translationFailed") return `Translation failed: ${params?.error ?? ""}`; if (key === "errorMessage") return `Error: ${params?.message ?? ""}`; if (key === "scenarioSimpleChat") return "Simple Chat"; if (key === "scenarioToolCalling") return "Tool Calling"; if (key === "scenarioMultiTurn") return "Multi-Turn"; if (key === "scenarioThinking") return "Thinking"; if (key === "scenarioSystemPrompt") return "System Prompt"; if (key === "scenarioStreaming") return "Streaming"; if (key === "advancedTestBenchTitle") return "Test Bench (8 cenários)"; if (key === "advancedTestBenchSubtitle") return "Roda todos os cenários e reporta pass/fail + compatibilidade %."; return key; }, })); // ── Collapsible stub ──────────────────────────────────────────────────────── // Renders children directly (open by default in tests, unless we override). // We expose a data attribute to let tests verify the title is passed. vi.mock("@/shared/components/Collapsible", () => ({ default: ({ children, title, subtitle, icon, defaultOpen, }: { children: React.ReactNode; title?: React.ReactNode; subtitle?: React.ReactNode; icon?: string; defaultOpen?: boolean; className?: string; }) => (
{defaultOpen !== false && children}
), })); // ── Shared components stubs ───────────────────────────────────────────────── vi.mock("@/shared/components", () => ({ Card: ({ children, className, }: { children: React.ReactNode; className?: string; }) =>
{children}
, Button: ({ children, onClick, disabled, loading, icon, "aria-label": ariaLabel, className, }: { children: React.ReactNode; onClick?: () => void; disabled?: boolean; loading?: boolean; icon?: string; "aria-label"?: string; className?: string; size?: string; variant?: string; }) => ( ), Select: ({ value, onChange, options, }: { value: string; onChange: (e: { target: { value: string } }) => void; options: Array<{ value: string; label: string }>; }) => ( ), Badge: ({ children, variant, size, }: { children: React.ReactNode; variant?: string; size?: string; }) => ( {children} ), })); // ── Hook stubs ────────────────────────────────────────────────────────────── vi.mock( "@/app/(dashboard)/dashboard/translator/hooks/useProviderOptions", () => ({ useProviderOptions: () => ({ provider: "openai", setProvider: vi.fn(), providerOptions: [ { value: "openai", label: "OpenAI" }, { value: "anthropic", label: "Anthropic" }, ], loading: false, }), }), ); vi.mock( "@/app/(dashboard)/dashboard/translator/hooks/useAvailableModels", () => ({ useAvailableModels: () => ({ model: "gpt-4o", setModel: vi.fn(), availableModels: ["gpt-4o", "gpt-3.5-turbo", "claude-sonnet-4-20250514"], loading: false, pickModelForFormat: (format: string) => { if (format === "claude") return "claude-sonnet-4-20250514"; return "gpt-4o"; }, }), }), ); // ── exampleTemplates stub ─────────────────────────────────────────────────── vi.mock( "@/app/(dashboard)/dashboard/translator/exampleTemplates", () => ({ getExampleTemplates: () => [ { id: "simple-chat", name: "Simple Chat", icon: "chat", description: "Simple chat", formats: { claude: { model: "claude-sonnet-4-20250514", messages: [{ role: "user", content: "Hello" }] }, openai: { model: "gpt-4o", messages: [{ role: "user", content: "Hello" }] }, }, }, { id: "tool-calling", name: "Tool Calling", icon: "build", description: "Tool calling", formats: { openai: { model: "gpt-4o", messages: [{ role: "user", content: "Weather?" }] }, }, }, { id: "multi-turn", name: "Multi-Turn", icon: "forum", description: "Multi-turn", formats: { openai: { model: "gpt-4o", messages: [] }, }, }, { id: "thinking", name: "Thinking", icon: "psychology", description: "Thinking", formats: { openai: { model: "o3-mini", messages: [] }, }, }, { id: "system-prompt", name: "System Prompt", icon: "settings", description: "System prompt", formats: { openai: { model: "gpt-4o", messages: [] }, }, }, { id: "streaming", name: "Streaming", icon: "stream", description: "Streaming", formats: { openai: { model: "gpt-4o", messages: [] }, }, }, { id: "vision", name: "Vision", icon: "image", description: "Vision", formats: { openai: { model: "gpt-4o", messages: [] }, }, }, { id: "schema-coercion", name: "Schema Coercion", icon: "schema", description: "Schema coercion", formats: { openai: { model: "gpt-4o", messages: [] }, }, }, ], FORMAT_META: { openai: { label: "OpenAI", color: "emerald", icon: "smart_toy" }, claude: { label: "Claude", color: "orange", icon: "psychology" }, gemini: { label: "Gemini", color: "blue", icon: "auto_awesome" }, }, FORMAT_OPTIONS: [ { value: "openai", label: "OpenAI" }, { value: "claude", label: "Claude" }, { value: "gemini", label: "Gemini" }, ], }), ); // ── Helpers ───────────────────────────────────────────────────────────────── const cleanupCallbacks: Array<() => void> = []; function makeContainer(): HTMLElement { const container = document.createElement("div"); document.body.appendChild(container); cleanupCallbacks.push(() => container.remove()); return container; } /** * Build a mock fetch that returns success for translate + a readable stream for send. */ function makeFetchMock(opts: { translateOk?: boolean; sendOk?: boolean; translateError?: string; sendHttpStatus?: number; } = {}) { const { translateOk = true, sendOk = true, translateError, sendHttpStatus = 200 } = opts; return vi.fn().mockImplementation((url: string) => { if ((url as string).includes("/api/translator/translate")) { return Promise.resolve({ ok: true, json: () => Promise.resolve( translateOk ? { success: true, result: { model: "gpt-4o", messages: [] } } : { success: false, error: translateError ?? "translate error" }, ), }); } if ((url as string).includes("/api/translator/send")) { if (!sendOk) { return Promise.resolve({ ok: false, status: sendHttpStatus, json: () => Promise.resolve({ error: `HTTP ${sendHttpStatus}` }), body: null, }); } // Readable stream with 2 chunks const encoder = new TextEncoder(); let step = 0; const readable = new ReadableStream({ pull(controller) { if (step === 0) { controller.enqueue(encoder.encode("data: chunk1\n\n")); step++; } else { controller.close(); } }, }); return Promise.resolve({ ok: true, status: 200, body: readable, json: () => Promise.resolve({}), }); } return Promise.reject(new Error(`Unexpected fetch: ${url}`)); }); } // ── Tests ──────────────────────────────────────────────────────────────────── describe("TestBenchAccordion", () => { beforeEach(() => { ( globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean } ).IS_REACT_ACT_ENVIRONMENT = true; }); afterEach(() => { while (cleanupCallbacks.length > 0) { cleanupCallbacks.pop()?.(); } document.body.innerHTML = ""; vi.restoreAllMocks(); }); // ── Module export ────────────────────────────────────────────────────────── it("exports a default function component", async () => { const mod = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); expect(typeof mod.default).toBe("function"); }); // ── Smoke render (closed by default) ───────────────────────────────────── it("renders Collapsible with correct title and icon when defaultOpen=false", async () => { const { default: TestBenchAccordion } = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); const collapsible = container.querySelector("[data-testid='collapsible']"); expect(collapsible).toBeTruthy(); expect(collapsible?.getAttribute("data-icon")).toBe("science"); // defaultOpen=false means content not rendered (lazy-render guard) expect(collapsible?.getAttribute("data-default-open")).toBe("false"); }); // ── Lazy-render guard ────────────────────────────────────────────────────── it("does not render scenario cards when defaultOpen is false (lazy-render guard)", async () => { const { default: TestBenchAccordion } = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); // When Collapsible stub renders with defaultOpen=false, children are suppressed const cards = container.querySelectorAll("[data-testid='card']"); expect(cards.length).toBe(0); }); // ── forceOpen renders content immediately ───────────────────────────────── it("renders TestBenchContent immediately when forceOpen=true", async () => { const { default: TestBenchAccordion } = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); // Should have rendered cards (info banner + controls + 8 scenarios) const cards = container.querySelectorAll("[data-testid='card']"); expect(cards.length).toBeGreaterThan(0); }); it("Collapsible gets defaultOpen=true when forceOpen=true", async () => { const { default: TestBenchAccordion } = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); const collapsible = container.querySelector("[data-testid='collapsible']"); expect(collapsible?.getAttribute("data-default-open")).toBe("true"); }); // ── Controls render ─────────────────────────────────────────────────────── it("renders source select, provider select, and Run All button when open", async () => { const { default: TestBenchAccordion } = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); const selects = container.querySelectorAll("[data-testid='select']"); expect(selects.length).toBeGreaterThanOrEqual(2); const buttons = container.querySelectorAll("[data-testid='button']"); const runAllBtn = Array.from(buttons).find((b) => b.textContent?.includes("Run All")); expect(runAllBtn).toBeTruthy(); }); it("renders 8 scenario buttons (one per scenario) when open", async () => { const { default: TestBenchAccordion } = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); const buttons = container.querySelectorAll("[data-testid='button']"); // 1 Run All + 8 scenario Run Test buttons const runTestBtns = Array.from(buttons).filter((b) => b.textContent?.includes("Run Test") || b.textContent?.includes("Re-Run") ); expect(runTestBtns.length).toBe(8); }); // ── Run All fires 8 fetches (translate + send each) ─────────────────────── it("clicking Run All fires 8 translate + 8 send fetches sequentially", async () => { const fetchMock = makeFetchMock(); vi.stubGlobal("fetch", fetchMock); const { default: TestBenchAccordion } = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); const buttons = container.querySelectorAll("[data-testid='button']"); const runAllBtn = Array.from(buttons).find((b) => b.textContent?.includes("Run All")) as | HTMLButtonElement | undefined; expect(runAllBtn).toBeTruthy(); await act(async () => { runAllBtn?.click(); }); const translateCalls = fetchMock.mock.calls.filter((c) => (c[0] as string).includes("/api/translator/translate"), ); const sendCalls = fetchMock.mock.calls.filter((c) => (c[0] as string).includes("/api/translator/send"), ); // 8 scenarios × 1 translate each expect(translateCalls.length).toBe(8); // 8 scenarios × 1 send each (translate succeeded for all) expect(sendCalls.length).toBe(8); }); // ── Results state: running → pass ────────────────────────────────────────── it("results map updates from running to pass after Run All completes", async () => { const fetchMock = makeFetchMock(); vi.stubGlobal("fetch", fetchMock); const { default: TestBenchAccordion } = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); const buttons = container.querySelectorAll("[data-testid='button']"); const runAllBtn = Array.from(buttons).find((b) => b.textContent?.includes("Run All")) as | HTMLButtonElement | undefined; await act(async () => { runAllBtn?.click(); }); // After completion, scenario buttons should show "Re-Run" (result exists) const reRunBtns = Array.from( container.querySelectorAll("[data-testid='button']"), ).filter((b) => b.textContent?.includes("Re-Run")); // All 8 should show re-run expect(reRunBtns.length).toBe(8); }); it("compatibility report badge appears after Run All completes", async () => { const fetchMock = makeFetchMock(); vi.stubGlobal("fetch", fetchMock); const { default: TestBenchAccordion } = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); const buttons = container.querySelectorAll("[data-testid='button']"); const runAllBtn = Array.from(buttons).find((b) => b.textContent?.includes("Run All")) as | HTMLButtonElement | undefined; await act(async () => { runAllBtn?.click(); }); // Compatibility Report section should be visible const text = container.textContent ?? ""; expect(text).toContain("Compatibility Report"); // Badge with percentage const badges = container.querySelectorAll("[data-testid='badge']"); expect(badges.length).toBeGreaterThan(0); const badgeTexts = Array.from(badges).map((b) => b.textContent?.trim()); const hasPercent = badgeTexts.some((t) => t?.includes("%")); expect(hasPercent).toBe(true); }); // ── Per-scenario re-run ─────────────────────────────────────────────────── it("clicking re-run on one scenario fires only that scenario's fetches", async () => { const fetchMock = makeFetchMock(); vi.stubGlobal("fetch", fetchMock); const { default: TestBenchAccordion } = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); // Run All first to populate results const buttons = container.querySelectorAll("[data-testid='button']"); const runAllBtn = Array.from(buttons).find((b) => b.textContent?.includes("Run All")) as | HTMLButtonElement | undefined; await act(async () => { runAllBtn?.click(); }); const callCountAfterAll = fetchMock.mock.calls.length; // Each scenario: 1 translate + 1 send = 2 calls; 8 scenarios = 16 total expect(callCountAfterAll).toBe(16); // Now click Re-Run on first scenario const reRunBtns = Array.from( container.querySelectorAll("[data-testid='button']"), ).filter((b) => b.textContent?.includes("Re-Run")) as HTMLButtonElement[]; expect(reRunBtns.length).toBeGreaterThan(0); await act(async () => { reRunBtns[0]?.click(); }); // Should have added exactly 2 more calls (1 translate + 1 send) const callCountAfterRerun = fetchMock.mock.calls.length; expect(callCountAfterRerun).toBe(callCountAfterAll + 2); }); // ── Error display sanitized ─────────────────────────────────────────────── it("displays error without stack trace when translate fails", async () => { const fetchMock = makeFetchMock({ translateOk: false, translateError: "Invalid format" }); vi.stubGlobal("fetch", fetchMock); const { default: TestBenchAccordion } = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); // Run first scenario only const buttons = container.querySelectorAll("[data-testid='button']"); const firstRunBtn = Array.from(buttons).find((b) => b.textContent?.includes("Run Test"), ) as HTMLButtonElement | undefined; await act(async () => { firstRunBtn?.click(); }); const text = container.textContent ?? ""; // Error should be visible expect(text).toContain("❌"); // Stack trace must NOT be exposed (Hard Rule #12) expect(text).not.toMatch(/\sat\s\//); expect(text).not.toMatch(/Error: .+\.tsx?:\d+/); }); it("displays error without stack trace when send fails with non-ok HTTP", async () => { const fetchMock = makeFetchMock({ translateOk: true, sendOk: false, sendHttpStatus: 503 }); vi.stubGlobal("fetch", fetchMock); const { default: TestBenchAccordion } = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); const buttons = container.querySelectorAll("[data-testid='button']"); const firstRunBtn = Array.from(buttons).find((b) => b.textContent?.includes("Run Test"), ) as HTMLButtonElement | undefined; await act(async () => { firstRunBtn?.click(); }); const text = container.textContent ?? ""; expect(text).toContain("❌"); // No stack trace expect(text).not.toMatch(/\sat\s\//); }); it("displays error without stack trace when fetch throws (network error)", async () => { vi.stubGlobal( "fetch", vi.fn().mockImplementation((url: string) => { if ((url as string).includes("/api/translator/translate")) { return Promise.reject(new Error("Network error")); } return Promise.reject(new Error("Unexpected")); }), ); const { default: TestBenchAccordion } = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); const buttons = container.querySelectorAll("[data-testid='button']"); const firstRunBtn = Array.from(buttons).find((b) => b.textContent?.includes("Run Test"), ) as HTMLButtonElement | undefined; await act(async () => { firstRunBtn?.click(); }); const text = container.textContent ?? ""; expect(text).toContain("❌"); // No stack trace (err.message used, not err.stack) expect(text).not.toMatch(/\sat\s\//); // Should contain the sanitized error message expect(text).toContain("Network error"); }); it("sanitizes stack trace from error: 'at /path' patterns are stripped from UI (Hard Rule #12)", async () => { vi.stubGlobal( "fetch", vi.fn().mockImplementation((url: string) => { if ((url as string).includes("/api/translator/translate")) { const errWithStack = new Error("foo\n at /home/user/dev/file.ts:42:10\n at /node_modules/bar.js:1:1"); return Promise.reject(errWithStack); } return Promise.reject(new Error("Unexpected")); }), ); const { default: TestBenchAccordion } = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); const buttons = container.querySelectorAll("[data-testid='button']"); const firstRunBtn = Array.from(buttons).find((b) => b.textContent?.includes("Run Test"), ) as HTMLButtonElement | undefined; await act(async () => { firstRunBtn?.click(); }); const text = container.textContent ?? ""; // Error should be displayed expect(text).toContain("❌"); // Stack trace 'at /' patterns MUST NOT appear in the rendered UI (Hard Rule #12) expect(text).not.toContain("at /"); }); // ── onOpenChange callback ───────────────────────────────────────────────── it("calls onOpenChange(true) when accordion opens for the first time", async () => { const onOpenChange = vi.fn(); const { default: TestBenchAccordion } = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); const container = makeContainer(); const root = createRoot(container); // forceOpen=true triggers content mount → sentinel fires onFirstOpen → onOpenChange(true) await act(async () => { root.render(); }); // hasOpened starts as true when forceOpen=true, so sentinel doesn't render. // onOpenChange is not called in this path. // Test the default-closed path where sentinel fires: const container2 = makeContainer(); const root2 = createRoot(container2); // Reset: render closed, then open via sentinel await act(async () => { root2.render(); }); // Default is closed, so no sentinel fires yet expect(onOpenChange).not.toHaveBeenCalled(); }); // ── Translate POST body shape ────────────────────────────────────────────── it("sends correct body to /api/translator/translate with step=direct", async () => { const fetchMock = makeFetchMock(); vi.stubGlobal("fetch", fetchMock); const { default: TestBenchAccordion } = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); const buttons = container.querySelectorAll("[data-testid='button']"); const firstRunBtn = Array.from(buttons).find((b) => b.textContent?.includes("Run Test"), ) as HTMLButtonElement | undefined; await act(async () => { firstRunBtn?.click(); }); const translateCall = fetchMock.mock.calls.find((c) => (c[0] as string).includes("/api/translator/translate"), ); expect(translateCall).toBeTruthy(); const bodyStr = (translateCall?.[1] as RequestInit)?.body as string; const body = JSON.parse(bodyStr); expect(body.step).toBe("direct"); expect(typeof body.sourceFormat).toBe("string"); expect(typeof body.provider).toBe("string"); expect(typeof body.body).toBe("object"); }); // ── translate:send POST body shape ──────────────────────────────────────── it("sends translated result to /api/translator/send", async () => { const fetchMock = makeFetchMock(); vi.stubGlobal("fetch", fetchMock); const { default: TestBenchAccordion } = await import( "@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion" ); const container = makeContainer(); const root = createRoot(container); await act(async () => { root.render(); }); const buttons = container.querySelectorAll("[data-testid='button']"); const firstRunBtn = Array.from(buttons).find((b) => b.textContent?.includes("Run Test"), ) as HTMLButtonElement | undefined; await act(async () => { firstRunBtn?.click(); }); const sendCall = fetchMock.mock.calls.find((c) => (c[0] as string).includes("/api/translator/send"), ); expect(sendCall).toBeTruthy(); const bodyStr = (sendCall?.[1] as RequestInit)?.body as string; const body = JSON.parse(bodyStr); expect(typeof body.provider).toBe("string"); expect(typeof body.body).toBe("object"); }); });