| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- /**
- * @vitest-environment happy-dom
- */
- import { renderToStaticMarkup } from "react-dom/server";
- import { describe, expect, it, vi } from "vitest";
- import type {
- ModelBreakdownItem,
- ModelBreakdownLabels,
- } from "@/components/analytics/model-breakdown-column";
- import {
- ModelBreakdownColumn,
- ModelBreakdownRow,
- } from "@/components/analytics/model-breakdown-column";
- // -- mocks --
- vi.mock("next-intl", () => ({
- useTranslations: () => (key: string) => `t:${key}`,
- }));
- vi.mock("@/components/ui/dialog", () => ({
- Dialog: ({ children }: { children: React.ReactNode }) => (
- <div data-testid="dialog">{children}</div>
- ),
- DialogContent: ({ children }: { children: React.ReactNode }) => (
- <div data-testid="dialog-content">{children}</div>
- ),
- DialogHeader: ({ children }: { children: React.ReactNode }) => (
- <div data-testid="dialog-header">{children}</div>
- ),
- DialogTitle: ({ children }: { children: React.ReactNode }) => (
- <h2 data-testid="dialog-title">{children}</h2>
- ),
- }));
- vi.mock("@/components/ui/separator", () => ({
- Separator: () => <hr />,
- }));
- vi.mock("@/lib/utils/currency", async () => {
- const actual =
- await vi.importActual<typeof import("@/lib/utils/currency")>("@/lib/utils/currency");
- return {
- ...actual,
- formatCurrency: (value: number) => `$${value.toFixed(2)}`,
- };
- });
- // -- helpers --
- function makeItem(overrides: Partial<ModelBreakdownItem> = {}): ModelBreakdownItem {
- return {
- model: "claude-opus-4",
- requests: 150,
- cost: 3.5,
- inputTokens: 10000,
- outputTokens: 5000,
- cacheCreationTokens: 2000,
- cacheReadTokens: 8000,
- ...overrides,
- };
- }
- const customLabels: ModelBreakdownLabels = {
- unknownModel: "Custom Unknown",
- modal: {
- requests: "Custom Requests",
- cost: "Custom Cost",
- inputTokens: "Custom Input",
- outputTokens: "Custom Output",
- cacheCreationTokens: "Custom Cache Write",
- cacheReadTokens: "Custom Cache Read",
- totalTokens: "Custom Total Tokens",
- costPercentage: "Custom Cost %",
- cacheHitRate: "Custom Cache Hit",
- cacheTokens: "Custom Cache Tokens",
- performanceHigh: "Custom High",
- performanceMedium: "Custom Medium",
- performanceLow: "Custom Low",
- },
- };
- function renderText(element: React.ReactElement): string {
- const markup = renderToStaticMarkup(element);
- const container = document.createElement("div");
- // Safe: content comes from our own renderToStaticMarkup, not user input
- container.textContent = "";
- const template = document.createElement("template");
- template.innerHTML = markup;
- container.appendChild(template.content.cloneNode(true));
- return container.textContent ?? "";
- }
- // -- tests --
- describe("ModelBreakdownColumn", () => {
- it("renders model name for each page item", () => {
- const items = [makeItem({ model: "gpt-4.1" }), makeItem({ model: "claude-sonnet-4" })];
- const text = renderText(
- <ModelBreakdownColumn
- pageItems={items}
- currencyCode="USD"
- totalCost={10}
- keyPrefix="key"
- pageOffset={0}
- />
- );
- expect(text).toContain("gpt-4.1");
- expect(text).toContain("claude-sonnet-4");
- });
- it("renders unknownModel label for null model", () => {
- const items = [makeItem({ model: null })];
- const text = renderText(
- <ModelBreakdownColumn
- pageItems={items}
- currencyCode="USD"
- totalCost={10}
- keyPrefix="key"
- pageOffset={0}
- />
- );
- // Falls back to useTranslations which returns "t:unknownModel"
- expect(text).toContain("t:unknownModel");
- });
- it("renders request count and token amounts", () => {
- const items = [
- makeItem({
- requests: 42,
- inputTokens: 1500,
- outputTokens: 500,
- cacheCreationTokens: 200,
- cacheReadTokens: 300,
- }),
- ];
- const text = renderText(
- <ModelBreakdownColumn
- pageItems={items}
- currencyCode="USD"
- totalCost={10}
- keyPrefix="key"
- pageOffset={0}
- />
- );
- // Request count
- expect(text).toContain("42");
- // Total tokens = 1500 + 500 + 200 + 300 = 2500 -> "2.5K"
- expect(text).toContain("2.5K");
- });
- it("passes correct props to ModelBreakdownRow", () => {
- const item = makeItem({ model: "test-model", cost: 5.0, requests: 99 });
- const text = renderText(
- <ModelBreakdownColumn
- pageItems={[item]}
- currencyCode="USD"
- totalCost={10}
- keyPrefix="test"
- pageOffset={0}
- />
- );
- // Model name
- expect(text).toContain("test-model");
- // Request count
- expect(text).toContain("99");
- // Cost formatted
- expect(text).toContain("$5.00");
- // Cost percentage = (5/10)*100 = 50.0
- expect(text).toContain("50.0%");
- });
- it("uses custom labels when provided", () => {
- const items = [makeItem({ model: null })];
- const text = renderText(
- <ModelBreakdownColumn
- pageItems={items}
- currencyCode="USD"
- totalCost={10}
- keyPrefix="key"
- pageOffset={0}
- labels={customLabels}
- />
- );
- // Custom unknown model label instead of "t:unknownModel"
- expect(text).toContain("Custom Unknown");
- expect(text).not.toContain("t:unknownModel");
- // Custom modal labels appear in the dialog content
- expect(text).toContain("Custom Requests");
- expect(text).toContain("Custom Cost");
- expect(text).toContain("Custom Total Tokens");
- expect(text).toContain("Custom Cache Tokens");
- expect(text).toContain("Custom Cache Hit");
- });
- });
- describe("ModelBreakdownRow", () => {
- it("renders model name and metrics in the row", () => {
- const text = renderText(
- <ModelBreakdownRow
- model="claude-opus-4"
- requests={150}
- cost={3.5}
- inputTokens={10000}
- outputTokens={5000}
- cacheCreationTokens={2000}
- cacheReadTokens={8000}
- currencyCode="USD"
- totalCost={10}
- />
- );
- expect(text).toContain("claude-opus-4");
- expect(text).toContain("150");
- expect(text).toContain("$3.50");
- });
- it("computes cache hit rate correctly", () => {
- // totalInputTokens = 10000 + 2000 + 8000 = 20000
- // cacheHitRate = (8000 / 20000) * 100 = 40.0
- const text = renderText(
- <ModelBreakdownRow
- model="test"
- requests={1}
- cost={1}
- inputTokens={10000}
- outputTokens={5000}
- cacheCreationTokens={2000}
- cacheReadTokens={8000}
- currencyCode="USD"
- totalCost={10}
- />
- );
- expect(text).toContain("40.0%");
- });
- it("shows zero cache hit rate when no input tokens", () => {
- const text = renderText(
- <ModelBreakdownRow
- model="test"
- requests={1}
- cost={1}
- inputTokens={0}
- outputTokens={100}
- cacheCreationTokens={0}
- cacheReadTokens={0}
- currencyCode="USD"
- totalCost={10}
- />
- );
- expect(text).toContain("0.0%");
- });
- it("uses translation fallback when no labels provided", () => {
- const text = renderText(
- <ModelBreakdownRow
- model={null}
- requests={1}
- cost={1}
- inputTokens={100}
- outputTokens={50}
- cacheCreationTokens={0}
- cacheReadTokens={0}
- currencyCode="USD"
- totalCost={10}
- />
- );
- // unknownModel via translation mock
- expect(text).toContain("t:unknownModel");
- // modal labels via translation mock
- expect(text).toContain("t:modal.requests");
- expect(text).toContain("t:modal.cacheWrite");
- expect(text).toContain("t:modal.cacheRead");
- });
- it("uses custom labels when provided", () => {
- const text = renderText(
- <ModelBreakdownRow
- model={null}
- requests={1}
- cost={1}
- inputTokens={100}
- outputTokens={50}
- cacheCreationTokens={0}
- cacheReadTokens={0}
- currencyCode="USD"
- totalCost={10}
- labels={customLabels}
- />
- );
- expect(text).toContain("Custom Unknown");
- expect(text).toContain("Custom Requests");
- expect(text).toContain("Custom Cache Write");
- expect(text).toContain("Custom Cache Read");
- expect(text).toContain("Custom Cache Tokens");
- expect(text).not.toContain("t:unknownModel");
- });
- });
|