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

586 lines
19 KiB
TypeScript

// @vitest-environment jsdom
/**
* Tests for CompressionPreviewAccordion (F7).
*
* Coverage targets:
* - smoke render (component mounts without errors)
* - lazy-render guard (D7): children only mount after accordion opens
* - mode select changes (off / lite / standard / aggressive / ultra)
* - Preview button dispatches POST /api/compression/preview with { messages, mode }
* - result grid (4 cards) renders on success
* - error path is sanitized (no stack-trace leak — "at /" not in DOM)
*/
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// ---------------------------------------------------------------------------
// Module mocks
// ---------------------------------------------------------------------------
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
// Stub shared components
vi.mock("@/shared/components", () => ({
Button: ({
children,
onClick,
disabled,
loading: _loading,
icon: _icon,
className: _className,
}: {
children: React.ReactNode;
onClick?: () => void;
disabled?: boolean;
loading?: boolean;
icon?: string;
className?: string;
}) => (
<button data-testid="button" onClick={onClick} disabled={disabled}>
{children}
</button>
),
Select: ({
value,
onChange,
options,
className: _className,
"aria-label": ariaLabel,
}: {
value: string;
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
options: ReadonlyArray<{ value: string; label: string }>;
className?: string;
"aria-label"?: string;
}) => (
<select data-testid="select" value={value} aria-label={ariaLabel} onChange={onChange}>
{options.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
),
}));
// Stub cn utility
vi.mock("@/shared/utils/cn", () => ({
cn: (...classes: (string | undefined | false)[]) => classes.filter(Boolean).join(" "),
}));
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
async function renderComponent(
props: {
forceOpen?: boolean;
inputContent?: string;
onOpenChange?: (open: boolean) => void;
} = {},
) {
const { default: CompressionPreviewAccordion } = await import(
"@/app/(dashboard)/dashboard/translator/components/advanced/CompressionPreviewAccordion"
);
const container = makeContainer();
const root = createRoot(container);
await act(async () => {
root.render(<CompressionPreviewAccordion {...props} />);
});
return { container, root };
}
/** Click the accordion toggle button to open/close it. */
async function clickToggle(container: HTMLElement) {
const btn = container.querySelector(
"button[aria-expanded]",
) as HTMLButtonElement | null;
expect(btn).toBeTruthy();
await act(async () => {
btn?.click();
});
}
// ---------------------------------------------------------------------------
// Setup / teardown
// ---------------------------------------------------------------------------
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
vi.clearAllMocks();
});
afterEach(() => {
while (cleanupCallbacks.length > 0) cleanupCallbacks.pop()?.();
document.body.innerHTML = "";
});
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe("CompressionPreviewAccordion — export", () => {
it("exports a default function component", async () => {
const mod = await import(
"@/app/(dashboard)/dashboard/translator/components/advanced/CompressionPreviewAccordion"
);
expect(typeof mod.default).toBe("function");
});
});
describe("CompressionPreviewAccordion — smoke render", () => {
it("renders without crashing (closed by default)", async () => {
const { container } = await renderComponent();
expect(container.querySelector("[data-testid='compression-accordion']")).toBeTruthy();
});
it("renders the toggle button with compress icon and i18n title", async () => {
const { container } = await renderComponent();
// compress icon in header
const icons = container.querySelectorAll(".material-symbols-outlined");
const iconTexts = Array.from(icons).map((el) => el.textContent?.trim());
expect(iconTexts).toContain("compress");
// title text (mock returns the key)
const text = container.textContent ?? "";
expect(text).toContain("advancedCompressionTitle");
});
it("renders the subtitle", async () => {
const { container } = await renderComponent();
const text = container.textContent ?? "";
expect(text).toContain("advancedCompressionSubtitle");
});
it("toggle button starts closed (aria-expanded=false)", async () => {
const { container } = await renderComponent();
const btn = container.querySelector("button[aria-expanded]");
expect(btn?.getAttribute("aria-expanded")).toBe("false");
});
it("toggle button starts open when forceOpen=true (aria-expanded=true)", async () => {
const { container } = await renderComponent({ forceOpen: true });
const btn = container.querySelector("button[aria-expanded]");
expect(btn?.getAttribute("aria-expanded")).toBe("true");
});
});
describe("CompressionPreviewAccordion — lazy-render guard (D7)", () => {
it("does NOT mount content when closed (forceOpen=false)", async () => {
const { container } = await renderComponent({ forceOpen: false });
// Content region should not exist
expect(container.querySelector("#compression-preview-content")).toBeNull();
// Mode select should not be in DOM
expect(container.querySelector("[data-testid='select']")).toBeNull();
});
it("mounts content after opening accordion", async () => {
const { container } = await renderComponent({ forceOpen: false });
// Initially closed
expect(container.querySelector("[data-testid='select']")).toBeNull();
// Open
await clickToggle(container);
// Content should now be mounted
expect(container.querySelector("#compression-preview-content")).toBeTruthy();
expect(container.querySelector("[data-testid='select']")).toBeTruthy();
});
it("mounts content immediately when forceOpen=true", async () => {
const { container } = await renderComponent({ forceOpen: true });
expect(container.querySelector("#compression-preview-content")).toBeTruthy();
expect(container.querySelector("[data-testid='select']")).toBeTruthy();
});
it("toggle opens accordion (aria-expanded flips to true)", async () => {
const { container } = await renderComponent();
const btn = container.querySelector("button[aria-expanded]");
expect(btn?.getAttribute("aria-expanded")).toBe("false");
await clickToggle(container);
expect(btn?.getAttribute("aria-expanded")).toBe("true");
});
it("toggle closes accordion again (aria-expanded flips back to false)", async () => {
const { container } = await renderComponent({ forceOpen: true });
const btn = container.querySelector("button[aria-expanded]");
expect(btn?.getAttribute("aria-expanded")).toBe("true");
await clickToggle(container);
expect(btn?.getAttribute("aria-expanded")).toBe("false");
});
it("calls onOpenChange with true when opening", async () => {
const onOpenChange = vi.fn();
const { container } = await renderComponent({ forceOpen: false, onOpenChange });
await clickToggle(container);
expect(onOpenChange).toHaveBeenCalledWith(true);
});
it("calls onOpenChange with false when closing", async () => {
const onOpenChange = vi.fn();
const { container } = await renderComponent({ forceOpen: true, onOpenChange });
await clickToggle(container);
expect(onOpenChange).toHaveBeenCalledWith(false);
});
});
describe("CompressionPreviewAccordion — empty state", () => {
it("shows empty-state hint when inputContent is empty string", async () => {
const { container } = await renderComponent({ forceOpen: true, inputContent: "" });
const text = container.textContent ?? "";
expect(text).toContain("compressionEmptyHint");
});
it("shows empty-state hint when inputContent is absent", async () => {
const { container } = await renderComponent({ forceOpen: true });
const text = container.textContent ?? "";
expect(text).toContain("compressionEmptyHint");
});
it("Preview button is disabled when inputContent is empty", async () => {
const { container } = await renderComponent({ forceOpen: true, inputContent: "" });
const btn = container.querySelector("[data-testid='button']") as HTMLButtonElement | null;
expect(btn?.disabled).toBe(true);
});
it("shows preview button enabled when inputContent is non-empty", async () => {
const { container } = await renderComponent({
forceOpen: true,
inputContent: JSON.stringify({ messages: [{ role: "user", content: "Hello" }] }),
});
const btn = container.querySelector("[data-testid='button']") as HTMLButtonElement | null;
expect(btn).toBeTruthy();
expect(btn?.disabled).toBe(false);
});
});
describe("CompressionPreviewAccordion — mode select", () => {
const MODES = ["off", "lite", "standard", "aggressive", "ultra"] as const;
it("renders all 5 mode options", async () => {
const { container } = await renderComponent({ forceOpen: true, inputContent: "hello" });
const select = container.querySelector("[data-testid='select']") as HTMLSelectElement | null;
expect(select).toBeTruthy();
const options = Array.from(select?.options ?? []).map((o) => o.value);
for (const mode of MODES) {
expect(options).toContain(mode);
}
});
it("default mode is 'standard'", async () => {
const { container } = await renderComponent({ forceOpen: true, inputContent: "hello" });
const select = container.querySelector("[data-testid='select']") as HTMLSelectElement | null;
expect(select?.value).toBe("standard");
});
for (const mode of MODES) {
it(`changing mode to '${mode}' updates select value`, async () => {
const { container } = await renderComponent({ forceOpen: true, inputContent: "hello" });
const select = container.querySelector("[data-testid='select']") as HTMLSelectElement | null;
expect(select).toBeTruthy();
await act(async () => {
// Set the value and fire change event
select!.value = mode;
select!.dispatchEvent(new Event("change", { bubbles: true }));
});
expect(select?.value).toBe(mode);
});
}
});
describe("CompressionPreviewAccordion — Preview fetch", () => {
it("calls POST /api/compression/preview with { messages, mode } on button click", async () => {
const mockResult = {
originalTokens: 100,
compressedTokens: 80,
tokensSaved: 20,
savingsPct: 20,
techniquesUsed: ["dedup", "trim"],
durationMs: 42,
};
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockResult,
});
vi.stubGlobal("fetch", fetchMock);
const inputContent = JSON.stringify({
messages: [{ role: "user", content: "Hello world" }],
});
const { container } = await renderComponent({ forceOpen: true, inputContent });
const btn = container.querySelector("[data-testid='button']") as HTMLButtonElement | null;
expect(btn).toBeTruthy();
await act(async () => {
btn?.click();
});
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledWith(
"/api/compression/preview",
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({ "Content-Type": "application/json" }),
}),
);
// Verify body has correct shape
const callArgs = fetchMock.mock.calls[0] as [string, RequestInit];
const body = JSON.parse(callArgs[1].body as string) as {
messages: Array<{ role: string; content: string }>;
mode: string;
};
expect(body).toMatchObject({
messages: [{ role: "user", content: "Hello world" }],
mode: "standard",
});
vi.unstubAllGlobals();
});
it("wraps plain-text inputContent as { role: 'user', content } when not valid JSON", async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
originalTokens: 10,
compressedTokens: 8,
tokensSaved: 2,
savingsPct: 20,
techniquesUsed: [],
durationMs: 5,
}),
});
vi.stubGlobal("fetch", fetchMock);
const { container } = await renderComponent({ forceOpen: true, inputContent: "plain text" });
const btn = container.querySelector("[data-testid='button']") as HTMLButtonElement | null;
await act(async () => {
btn?.click();
});
const callArgs = fetchMock.mock.calls[0] as [string, RequestInit];
const body = JSON.parse(callArgs[1].body as string) as {
messages: Array<{ role: string; content: string }>;
};
expect(body.messages).toEqual([{ role: "user", content: "plain text" }]);
vi.unstubAllGlobals();
});
it("sends selected mode in the request body", async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({
originalTokens: 50,
compressedTokens: 40,
tokensSaved: 10,
savingsPct: 20,
techniquesUsed: [],
durationMs: 20,
}),
});
vi.stubGlobal("fetch", fetchMock);
const { container } = await renderComponent({ forceOpen: true, inputContent: "some text" });
// Change mode to "aggressive"
const select = container.querySelector("[data-testid='select']") as HTMLSelectElement | null;
await act(async () => {
select!.value = "aggressive";
select!.dispatchEvent(new Event("change", { bubbles: true }));
});
// Click preview
const btn = container.querySelector("[data-testid='button']") as HTMLButtonElement | null;
await act(async () => {
btn?.click();
});
const callArgs = fetchMock.mock.calls[0] as [string, RequestInit];
const body = JSON.parse(callArgs[1].body as string) as { mode: string };
expect(body.mode).toBe("aggressive");
vi.unstubAllGlobals();
});
});
describe("CompressionPreviewAccordion — result grid (4 cards)", () => {
it("renders 4 metric cards after successful preview", async () => {
const mockResult = {
originalTokens: 200,
compressedTokens: 150,
tokensSaved: 50,
savingsPct: 25,
techniquesUsed: ["dedup"],
durationMs: 88,
};
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockResult,
});
vi.stubGlobal("fetch", fetchMock);
const { container } = await renderComponent({
forceOpen: true,
inputContent: "some input",
});
const btn = container.querySelector("[data-testid='button']") as HTMLButtonElement | null;
await act(async () => {
btn?.click();
});
const grid = container.querySelector("[data-testid='compression-result-grid']");
expect(grid).toBeTruthy();
const cards = grid?.querySelectorAll(".card");
expect(cards?.length).toBe(4);
const text = container.textContent ?? "";
expect(text).toContain("200"); // originalTokens
expect(text).toContain("150"); // compressedTokens
expect(text).toContain("50"); // tokensSaved
expect(text).toContain("88"); // durationMs
vi.unstubAllGlobals();
});
it("renders techniquesUsed list when non-empty", async () => {
const mockResult = {
originalTokens: 100,
compressedTokens: 90,
tokensSaved: 10,
savingsPct: 10,
techniquesUsed: ["dedup", "trim", "compact"],
durationMs: 33,
};
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => mockResult,
});
vi.stubGlobal("fetch", fetchMock);
const { container } = await renderComponent({
forceOpen: true,
inputContent: "some input",
});
await act(async () => {
(container.querySelector("[data-testid='button']") as HTMLButtonElement)?.click();
});
const text = container.textContent ?? "";
expect(text).toContain("dedup");
expect(text).toContain("trim");
expect(text).toContain("compact");
vi.unstubAllGlobals();
});
it("does NOT render result grid before a successful preview", async () => {
const { container } = await renderComponent({ forceOpen: true, inputContent: "hello" });
expect(container.querySelector("[data-testid='compression-result-grid']")).toBeNull();
});
});
describe("CompressionPreviewAccordion — error path (Hard Rule #12)", () => {
it("shows sanitized error message on fetch failure", async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: false,
json: async () => ({ error: "Internal Server Error" }),
});
vi.stubGlobal("fetch", fetchMock);
const { container } = await renderComponent({
forceOpen: true,
inputContent: "some input",
});
await act(async () => {
(container.querySelector("[data-testid='button']") as HTMLButtonElement)?.click();
});
const errorEl = container.querySelector("[role='alert']");
expect(errorEl).toBeTruthy();
expect(errorEl?.textContent).toContain("Internal Server Error");
vi.unstubAllGlobals();
});
it("error message does NOT contain stack-trace lines (at /path/...)", async () => {
const stackError = new Error(
"Something went wrong\n at /home/user/app/src/file.ts:42:13\n at Object.<anonymous> /home/user/app/tests/test.ts:10:5",
);
const fetchMock = vi.fn().mockRejectedValue(stackError);
vi.stubGlobal("fetch", fetchMock);
const { container } = await renderComponent({
forceOpen: true,
inputContent: "some input",
});
await act(async () => {
(container.querySelector("[data-testid='button']") as HTMLButtonElement)?.click();
});
const errorEl = container.querySelector("[role='alert']");
expect(errorEl).toBeTruthy();
const errorText = errorEl?.textContent ?? "";
// Must NOT contain "at /" (stack-trace pattern)
expect(errorText).not.toMatch(/\sat\s\//);
// Should still contain the core message
expect(errorText).toContain("Something went wrong");
vi.unstubAllGlobals();
});
it("shows 'Preview failed' when fetch returns non-ok without error field", async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: false,
json: async () => ({}), // no error field
});
vi.stubGlobal("fetch", fetchMock);
const { container } = await renderComponent({
forceOpen: true,
inputContent: "some input",
});
await act(async () => {
(container.querySelector("[data-testid='button']") as HTMLButtonElement)?.click();
});
const errorEl = container.querySelector("[role='alert']");
expect(errorEl).toBeTruthy();
expect(errorEl?.textContent).toContain("Preview failed");
vi.unstubAllGlobals();
});
});