Files
diegosouzapw--omniroute/tests/unit/ui/playground-compare-column.test.tsx
2026-07-13 13:39:12 +08:00

159 lines
4.7 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 } from "vitest";
import { vi } from "vitest";
vi.mock("remark-gfm", () => ({ default: () => {} }));
vi.mock("react-markdown", () => ({
default: ({ children }: { children: React.ReactNode }) => (
<div data-testid="markdown-content">{children}</div>
),
}));
const { default: CompareColumn } = await import(
"../../../src/app/(dashboard)/dashboard/playground/components/CompareColumn"
);
type ColumnStatus = "idle" | "streaming" | "done" | "error";
const BASE_METRICS = {
ttftMs: null,
totalMs: null,
tokensIn: 0,
tokensOut: 0,
tps: null,
costUsd: null,
};
function makeColumn(overrides: Partial<{
id: string;
model: string;
status: ColumnStatus;
metrics: typeof BASE_METRICS;
response: string;
errorMessage: string;
}> = {}) {
return {
id: "col-1",
model: "openai/gpt-4o",
status: "idle" as ColumnStatus,
metrics: { ...BASE_METRICS },
response: "",
...overrides,
};
}
const containers: Array<{ root: ReturnType<typeof createRoot>; el: HTMLDivElement }> = [];
function renderColumn(
column: ReturnType<typeof makeColumn>,
onCancel = vi.fn(),
onRemove = vi.fn(),
): HTMLDivElement {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
act(() => {
root.render(
<CompareColumn column={column} onCancel={onCancel} onRemove={onRemove} />,
);
});
containers.push({ root, el });
return el;
}
beforeEach(() => {
if (typeof Element.prototype.scrollIntoView === "undefined") {
Object.defineProperty(Element.prototype, "scrollIntoView", {
value: () => {},
writable: true,
configurable: true,
});
}
});
afterEach(() => {
for (const { root, el } of containers) {
act(() => root.unmount());
el.remove();
}
containers.length = 0;
});
describe("CompareColumn", () => {
it("renders model name in header", () => {
const el = renderColumn(makeColumn({ model: "anthropic/claude-3-opus" }));
expect(el.textContent).toContain("anthropic/claude-3-opus");
});
it("shows idle state with ready message", () => {
const el = renderColumn(makeColumn({ status: "idle" }));
expect(el.textContent).toContain("Ready to run");
});
it("shows streaming skeleton when status=streaming and response empty", () => {
const el = renderColumn(makeColumn({ status: "streaming", response: "" }));
expect(el.textContent).toContain("Waiting for response");
});
it("renders markdown response when streaming and response is non-empty", () => {
const el = renderColumn(
makeColumn({ status: "streaming", response: "Hello from model" }),
);
const markdown = el.querySelector("[data-testid='markdown-content']");
expect(markdown).not.toBeNull();
expect(markdown?.textContent).toContain("Hello from model");
});
it("shows error message when status=error", () => {
const el = renderColumn(
makeColumn({ status: "error", errorMessage: "Rate limit exceeded" }),
);
expect(el.textContent).toContain("Rate limit exceeded");
});
it("shows ProviderMetrics bar when streaming or done", () => {
const metrics = {
ttftMs: 234,
totalMs: 1200,
tokensIn: 142,
tokensOut: 38,
tps: 31.6,
costUsd: 0.002,
};
const el = renderColumn(makeColumn({ status: "done", metrics, response: "Done." }));
// Should show estimated
expect(el.textContent).toContain("estimated");
// Should show TTFT
expect(el.textContent).toContain("TTFT");
});
it("calls onCancel when cancel button clicked during streaming", () => {
const onCancel = vi.fn();
const el = renderColumn(makeColumn({ status: "streaming" }), onCancel);
const cancelBtn = el.querySelector("[aria-label='Cancel stream']") as HTMLButtonElement;
expect(cancelBtn).not.toBeNull();
act(() => cancelBtn.click());
expect(onCancel).toHaveBeenCalledWith("col-1");
});
it("calls onRemove when remove button clicked", () => {
const onRemove = vi.fn();
const el = renderColumn(makeColumn(), vi.fn(), onRemove);
const removeBtn = el.querySelector("[aria-label='Remove column for openai/gpt-4o']") as HTMLButtonElement;
expect(removeBtn).not.toBeNull();
act(() => removeBtn.click());
expect(onRemove).toHaveBeenCalledWith("col-1");
});
it("updates metrics displayed when metrics change (TTFT after first chunk)", () => {
const metricsWithTtft = { ...BASE_METRICS, ttftMs: 187 };
const el = renderColumn(
makeColumn({ status: "streaming", metrics: metricsWithTtft, response: "Hi" }),
);
expect(el.textContent).toContain("187ms");
});
});