Files
diegosouzapw--omniroute/tests/unit/translator-friendly-test-bench.test.tsx
2026-07-13 13:39:12 +08:00

848 lines
31 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @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<string, unknown>) => {
// 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;
}) => (
<div
data-testid="collapsible"
data-title={typeof title === "string" ? title : undefined}
data-subtitle={typeof subtitle === "string" ? subtitle : undefined}
data-icon={icon}
data-default-open={defaultOpen ? "true" : "false"}
>
{defaultOpen !== false && children}
</div>
),
}));
// ── Shared components stubs ─────────────────────────────────────────────────
vi.mock("@/shared/components", () => ({
Card: ({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) => <div data-testid="card" className={className}>{children}</div>,
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;
}) => (
<button
data-testid="button"
data-icon={icon}
disabled={disabled || loading}
onClick={onClick}
aria-label={ariaLabel}
className={className}
>
{children}
</button>
),
Select: ({
value,
onChange,
options,
}: {
value: string;
onChange: (e: { target: { value: string } }) => void;
options: Array<{ value: string; label: string }>;
}) => (
<select
data-testid="select"
value={value}
onChange={(e) => onChange({ target: { value: e.target.value } })}
>
{options.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
),
Badge: ({
children,
variant,
size,
}: {
children: React.ReactNode;
variant?: string;
size?: string;
}) => (
<span data-testid="badge" data-variant={variant} data-size={size}>
{children}
</span>
),
}));
// ── 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(<TestBenchAccordion />);
});
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(<TestBenchAccordion />);
});
// 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(<TestBenchAccordion forceOpen={true} />);
});
// 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(<TestBenchAccordion forceOpen={true} />);
});
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(<TestBenchAccordion forceOpen={true} />);
});
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(<TestBenchAccordion forceOpen={true} />);
});
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(<TestBenchAccordion forceOpen={true} />);
});
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(<TestBenchAccordion forceOpen={true} />);
});
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(<TestBenchAccordion forceOpen={true} />);
});
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(<TestBenchAccordion forceOpen={true} />);
});
// 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(<TestBenchAccordion forceOpen={true} />);
});
// 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(<TestBenchAccordion forceOpen={true} />);
});
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(<TestBenchAccordion forceOpen={true} />);
});
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(<TestBenchAccordion forceOpen={true} />);
});
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(<TestBenchAccordion forceOpen={true} onOpenChange={onOpenChange} />);
});
// 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(<TestBenchAccordion onOpenChange={onOpenChange} />);
});
// 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(<TestBenchAccordion forceOpen={true} />);
});
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(<TestBenchAccordion forceOpen={true} />);
});
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");
});
});