项目文件夹

文件
2026-07-13 13:39:12 +08:00

848 行
31 KiB
TypeScript

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
// @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");
});
});