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

397 lines
13 KiB
TypeScript

// @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";
import type { FormatId, TranslateMode } from "@/app/(dashboard)/dashboard/translator/types";
// --- Mock next-intl ---
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
// --- Mock shared components ---
vi.mock("@/shared/components", () => ({
Button: ({
children,
onClick,
disabled,
loading,
"aria-label": ariaLabel,
}: {
children?: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
loading?: boolean;
"aria-label"?: string;
}) => (
<button
data-testid="button"
onClick={onClick}
disabled={disabled || loading}
aria-label={ariaLabel}
>
{children}
</button>
),
Select: ({
options = [],
value,
onChange,
placeholder,
"aria-label": ariaLabel,
}: {
options?: Array<{ value: string; label: string }>;
value?: string;
onChange?: (e: React.ChangeEvent<HTMLSelectElement>) => void;
placeholder?: string;
"aria-label"?: string;
}) => (
<select data-testid="select" value={value} onChange={onChange} aria-label={ariaLabel}>
{placeholder && <option value="">{placeholder}</option>}
{options.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
),
SegmentedControl: ({
options = [],
value,
onChange,
"aria-label": ariaLabel,
}: {
options?: Array<{ value: string; label: string }>;
value?: string;
onChange?: (v: string) => void;
"aria-label"?: string;
}) => (
<div data-testid="segmented-control" role="tablist" aria-label={ariaLabel}>
{options.map((o) => (
<button
key={o.value}
role="tab"
aria-selected={value === o.value}
onClick={() => onChange?.(o.value)}
data-value={o.value}
>
{o.label}
</button>
))}
</div>
),
InfoTooltip: ({ text }: { text: string }) => (
<span data-testid="info-tooltip" aria-label={text}>
info
</span>
),
}));
// --- Mock useAvailableModels ---
vi.mock(
"@/app/(dashboard)/dashboard/translator/hooks/useAvailableModels",
() => ({
useAvailableModels: () => ({
model: "gpt-4o",
setModel: vi.fn(),
availableModels: ["gpt-4o", "claude-sonnet-4-20250514"],
loading: false,
pickModelForFormat: () => "gpt-4o",
}),
})
);
// --- Mock exampleTemplates ---
vi.mock(
"@/app/(dashboard)/dashboard/translator/exampleTemplates",
() => ({
FORMAT_OPTIONS: [
{ value: "openai", label: "OpenAI" },
{ value: "claude", label: "Claude" },
{ value: "gemini", label: "Gemini" },
],
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" },
},
getExampleTemplates: () => [
{
id: "simple-chat",
name: "Simple Chat",
icon: "chat",
description: "A simple chat example",
formats: {
openai: { model: "gpt-4o", messages: [{ role: "user", content: "Hello" }] },
claude: { model: "claude-sonnet-4-20250514", messages: [{ role: "user", content: "Hello" }] },
},
},
{
id: "tool-calling",
name: "Tool Calling",
icon: "build",
description: "Tool calling example",
formats: {
openai: { model: "gpt-4o", tools: [] },
},
},
],
})
);
// --- Setup ---
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
function makeProps(overrides: Partial<{
source: FormatId;
target: FormatId;
provider: string;
inputText: string;
mode: TranslateMode;
onSourceChange: (s: FormatId) => void;
onTargetChange: (t: FormatId) => void;
onProviderChange: (p: string) => void;
onInputChange: (text: string) => void;
onModeChange: (m: TranslateMode) => void;
onSubmit: () => void;
onOpenAdvanced: () => void;
isLoading: boolean;
providerOptions: Array<{ value: string; label: string }>;
loading: boolean;
}> = {}) {
return {
source: "claude" as FormatId,
target: "openai" as FormatId,
provider: "openai",
inputText: "",
mode: "send" as TranslateMode,
onSourceChange: vi.fn(),
onTargetChange: vi.fn(),
onProviderChange: vi.fn(),
onInputChange: vi.fn(),
onModeChange: vi.fn(),
onSubmit: vi.fn(),
onOpenAdvanced: vi.fn(),
isLoading: false,
providerOptions: [{ value: "openai", label: "OpenAI" }, { value: "anthropic", label: "Anthropic" }],
loading: false,
...overrides,
};
}
describe("SimpleControls", () => {
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.clearAllMocks();
});
it("exports a default function component", async () => {
const mod = await import(
"@/app/(dashboard)/dashboard/translator/components/SimpleControls"
);
expect(typeof mod.default).toBe("function");
});
it("renders smoke: mounts without throwing", async () => {
const { default: SimpleControls } = await import(
"@/app/(dashboard)/dashboard/translator/components/SimpleControls"
);
const container = makeContainer();
const root = createRoot(container);
const props = makeProps();
await act(async () => {
root.render(<SimpleControls {...props} />);
});
expect(container.innerHTML).not.toBe("");
});
it("renders 3 Select elements (source, provider, example) + 1 SegmentedControl", async () => {
const { default: SimpleControls } = await import(
"@/app/(dashboard)/dashboard/translator/components/SimpleControls"
);
const container = makeContainer();
const root = createRoot(container);
const props = makeProps();
await act(async () => {
root.render(<SimpleControls {...props} />);
});
const selects = container.querySelectorAll("[data-testid='select']");
expect(selects.length).toBeGreaterThanOrEqual(3);
const segmented = container.querySelectorAll("[data-testid='segmented-control']");
expect(segmented.length).toBe(1);
});
it("renders the submit button", async () => {
const { default: SimpleControls } = await import(
"@/app/(dashboard)/dashboard/translator/components/SimpleControls"
);
const container = makeContainer();
const root = createRoot(container);
const props = makeProps({ inputText: "Hello" });
await act(async () => {
root.render(<SimpleControls {...props} />);
});
const buttons = container.querySelectorAll("[data-testid='button']");
expect(buttons.length).toBeGreaterThanOrEqual(1);
});
it("calls onSourceChange when source select changes", async () => {
const { default: SimpleControls } = await import(
"@/app/(dashboard)/dashboard/translator/components/SimpleControls"
);
const container = makeContainer();
const root = createRoot(container);
const onSourceChange = vi.fn();
const props = makeProps({ onSourceChange });
await act(async () => {
root.render(<SimpleControls {...props} />);
});
// The first select is the source select (aria-label uses fallback "My app uses")
const sourceSelect = container.querySelector("select[aria-label='My app uses']") as HTMLSelectElement | null;
expect(sourceSelect).toBeTruthy();
await act(async () => {
if (sourceSelect) {
Object.defineProperty(sourceSelect, "value", { writable: true, value: "openai" });
sourceSelect.dispatchEvent(new Event("change", { bubbles: true }));
}
});
expect(onSourceChange).toHaveBeenCalled();
});
it("calls onModeChange when segmented control tab is clicked", async () => {
const { default: SimpleControls } = await import(
"@/app/(dashboard)/dashboard/translator/components/SimpleControls"
);
const container = makeContainer();
const root = createRoot(container);
const onModeChange = vi.fn();
const props = makeProps({ onModeChange, mode: "send" });
await act(async () => {
root.render(<SimpleControls {...props} />);
});
// Find the "preview" tab button in the segmented control
const previewTab = container.querySelector(
"[data-testid='segmented-control'] button[data-value='preview']"
) as HTMLButtonElement | null;
expect(previewTab).toBeTruthy();
await act(async () => {
previewTab?.click();
});
expect(onModeChange).toHaveBeenCalledWith("preview");
});
it("calls onInputChange when textarea content changes", async () => {
const { default: SimpleControls } = await import(
"@/app/(dashboard)/dashboard/translator/components/SimpleControls"
);
const container = makeContainer();
const root = createRoot(container);
const onInputChange = vi.fn();
const props = makeProps({ onInputChange });
await act(async () => {
root.render(<SimpleControls {...props} />);
});
const textarea = container.querySelector("textarea") as HTMLTextAreaElement | null;
expect(textarea).toBeTruthy();
await act(async () => {
if (textarea) {
Object.defineProperty(textarea, "value", { writable: true, value: "Hello world" });
textarea.dispatchEvent(new Event("change", { bubbles: true }));
}
});
expect(onInputChange).toHaveBeenCalled();
});
it("calls onOpenAdvanced when Advanced button is clicked", async () => {
const { default: SimpleControls } = await import(
"@/app/(dashboard)/dashboard/translator/components/SimpleControls"
);
const container = makeContainer();
const root = createRoot(container);
const onOpenAdvanced = vi.fn();
const props = makeProps({ onOpenAdvanced });
await act(async () => {
root.render(<SimpleControls {...props} />);
});
// Find the Advanced button (has aria-label fallback "Advanced")
const advancedBtn = container.querySelector(
"button[aria-label='Advanced']"
) as HTMLButtonElement | null;
expect(advancedBtn).toBeTruthy();
await act(async () => {
advancedBtn?.click();
});
expect(onOpenAdvanced).toHaveBeenCalled();
});
it("submit button is disabled when inputText is empty", async () => {
const { default: SimpleControls } = await import(
"@/app/(dashboard)/dashboard/translator/components/SimpleControls"
);
const container = makeContainer();
const root = createRoot(container);
const props = makeProps({ inputText: "" });
await act(async () => {
root.render(<SimpleControls {...props} />);
});
const submitBtn = container.querySelector(
"[data-testid='button']"
) as HTMLButtonElement | null;
expect(submitBtn?.disabled).toBe(true);
});
it("submit button is enabled when inputText has content", async () => {
const { default: SimpleControls } = await import(
"@/app/(dashboard)/dashboard/translator/components/SimpleControls"
);
const container = makeContainer();
const root = createRoot(container);
const props = makeProps({ inputText: "Hello" });
await act(async () => {
root.render(<SimpleControls {...props} />);
});
const submitBtn = container.querySelector(
"[data-testid='button']"
) as HTMLButtonElement | null;
expect(submitBtn?.disabled).toBe(false);
});
it("calls onOpenAdvanced when __custom__ example option is selected", async () => {
const { default: SimpleControls } = await import(
"@/app/(dashboard)/dashboard/translator/components/SimpleControls"
);
const container = makeContainer();
const root = createRoot(container);
const onOpenAdvanced = vi.fn();
const props = makeProps({ onOpenAdvanced });
await act(async () => {
root.render(<SimpleControls {...props} />);
});
// The example select has a __custom__ option (aria-label uses fallback "Start with")
const exampleSelect = container.querySelector(
"select[aria-label='Start with']"
) as HTMLSelectElement | null;
expect(exampleSelect).toBeTruthy();
await act(async () => {
if (exampleSelect) {
Object.defineProperty(exampleSelect, "value", { writable: true, value: "__custom__" });
exampleSelect.dispatchEvent(new Event("change", { bubbles: true }));
}
});
expect(onOpenAdvanced).toHaveBeenCalled();
});
});