import { describe, it, expect, vi, beforeEach } from "vitest"; import { render, screen, waitFor, fireEvent } from "@testing-library/react"; import type { ReactNode } from "react"; import UsagePage from "./page"; // ─── Component Prop Types ──────────────────────────────────────────── interface ChildrenProps { children: ReactNode; } interface StyledChildrenProps extends ChildrenProps { className?: string; } // ─── Mocks ─────────────────────────────────────────────────────────── // Mock @/components/ui/card — @mosaic/ui can't be resolved in vitest vi.mock("@/components/ui/card", () => ({ Card: ({ children, className }: StyledChildrenProps): React.JSX.Element => (
{children}
), CardHeader: ({ children }: ChildrenProps): React.JSX.Element =>
{children}
, CardContent: ({ children, className }: StyledChildrenProps): React.JSX.Element => (
{children}
), CardFooter: ({ children }: ChildrenProps): React.JSX.Element =>
{children}
, CardTitle: ({ children, className }: StyledChildrenProps): React.JSX.Element => (

{children}

), CardDescription: ({ children, className }: StyledChildrenProps): React.JSX.Element => (

{children}

), })); // Mock recharts — jsdom has no SVG layout engine, so we render stubs vi.mock("recharts", () => ({ LineChart: ({ children }: ChildrenProps): React.JSX.Element => (
{children}
), Line: (): React.JSX.Element =>
, BarChart: ({ children }: ChildrenProps): React.JSX.Element => (
{children}
), Bar: (): React.JSX.Element =>
, PieChart: ({ children }: ChildrenProps): React.JSX.Element => (
{children}
), Pie: (): React.JSX.Element =>
, Cell: (): React.JSX.Element =>
, XAxis: (): React.JSX.Element =>
, YAxis: (): React.JSX.Element =>
, CartesianGrid: (): React.JSX.Element =>
, Tooltip: (): React.JSX.Element =>
, ResponsiveContainer: ({ children }: ChildrenProps): React.JSX.Element =>
{children}
, Legend: (): React.JSX.Element =>
, })); // Mock the telemetry API module vi.mock("@/lib/api/telemetry", () => ({ fetchUsageSummary: vi.fn(), fetchTokenUsage: vi.fn(), fetchCostBreakdown: vi.fn(), fetchTaskOutcomes: vi.fn(), })); // Import mocked modules after vi.mock import { fetchUsageSummary, fetchTokenUsage, fetchCostBreakdown, fetchTaskOutcomes, } from "@/lib/api/telemetry"; // ─── Test Data ─────────────────────────────────────────────────────── const mockSummary = { totalTokens: 245800, totalCost: 3.42, taskCount: 47, avgQualityGatePassRate: 0.87, }; const mockTokenUsage = [ { date: "2026-02-08", inputTokens: 10000, outputTokens: 5000, totalTokens: 15000 }, { date: "2026-02-09", inputTokens: 12000, outputTokens: 6000, totalTokens: 18000 }, ]; const mockCostBreakdown = [ { model: "claude-sonnet-4-5", provider: "anthropic", cost: 18.5, taskCount: 124 }, { model: "gpt-4o", provider: "openai", cost: 12.3, taskCount: 89 }, ]; const mockTaskOutcomes = [ { outcome: "Success", count: 312, color: "#6EBF8B" }, { outcome: "Partial", count: 48, color: "#F5C862" }, ]; function setupMocks(overrides?: { empty?: boolean; error?: boolean }): void { if (overrides?.error) { vi.mocked(fetchUsageSummary).mockRejectedValue(new Error("Network error")); vi.mocked(fetchTokenUsage).mockRejectedValue(new Error("Network error")); vi.mocked(fetchCostBreakdown).mockRejectedValue(new Error("Network error")); vi.mocked(fetchTaskOutcomes).mockRejectedValue(new Error("Network error")); return; } const summary = overrides?.empty ? { ...mockSummary, taskCount: 0 } : mockSummary; vi.mocked(fetchUsageSummary).mockResolvedValue(summary); vi.mocked(fetchTokenUsage).mockResolvedValue(mockTokenUsage); vi.mocked(fetchCostBreakdown).mockResolvedValue(mockCostBreakdown); vi.mocked(fetchTaskOutcomes).mockResolvedValue(mockTaskOutcomes); } // ─── Tests ─────────────────────────────────────────────────────────── describe("UsagePage", (): void => { beforeEach((): void => { vi.clearAllMocks(); }); it("should render the page title and subtitle", (): void => { setupMocks(); render(); expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("Usage"); expect(screen.getByText("Token usage and cost overview")).toBeInTheDocument(); }); it("should have proper layout structure", (): void => { setupMocks(); const { container } = render(); const main = container.querySelector("main"); expect(main).toBeInTheDocument(); }); it("should show loading skeleton initially", (): void => { setupMocks(); render(); expect(screen.getByTestId("loading-skeleton")).toBeInTheDocument(); }); it("should render summary cards after loading", async (): Promise => { setupMocks(); render(); await waitFor((): void => { expect(screen.getByTestId("summary-cards")).toBeInTheDocument(); }); // Check summary card values expect(screen.getByText("Total Tokens")).toBeInTheDocument(); expect(screen.getByText("245.8K")).toBeInTheDocument(); expect(screen.getByText("Estimated Cost")).toBeInTheDocument(); expect(screen.getByText("$3.42")).toBeInTheDocument(); expect(screen.getByText("Task Count")).toBeInTheDocument(); expect(screen.getByText("47")).toBeInTheDocument(); expect(screen.getByText("Quality Gate Pass Rate")).toBeInTheDocument(); expect(screen.getByText("87.0%")).toBeInTheDocument(); }); it("should render all chart sections after loading", async (): Promise => { setupMocks(); render(); await waitFor((): void => { expect(screen.getByTestId("token-usage-chart")).toBeInTheDocument(); expect(screen.getByTestId("cost-breakdown-chart")).toBeInTheDocument(); expect(screen.getByTestId("task-outcomes-chart")).toBeInTheDocument(); }); }); it("should render the time range selector with three options", (): void => { setupMocks(); render(); expect(screen.getByText("7 Days")).toBeInTheDocument(); expect(screen.getByText("30 Days")).toBeInTheDocument(); expect(screen.getByText("90 Days")).toBeInTheDocument(); }); it("should have 30 Days selected by default", (): void => { setupMocks(); render(); const button30d = screen.getByText("30 Days"); expect(button30d).toHaveAttribute("aria-pressed", "true"); }); it("should change time range when a different option is clicked", async (): Promise => { setupMocks(); render(); // Wait for initial load await waitFor((): void => { expect(screen.getByTestId("summary-cards")).toBeInTheDocument(); }); // Click 7 Days const button7d = screen.getByText("7 Days"); fireEvent.click(button7d); expect(button7d).toHaveAttribute("aria-pressed", "true"); expect(screen.getByText("30 Days")).toHaveAttribute("aria-pressed", "false"); }); it("should refetch data when time range changes", async (): Promise => { setupMocks(); render(); // Wait for initial load (30d default) await waitFor((): void => { expect(screen.getByTestId("summary-cards")).toBeInTheDocument(); }); // Initial call was with "30d" expect(fetchUsageSummary).toHaveBeenCalledWith("30d"); // Change to 7d fireEvent.click(screen.getByText("7 Days")); await waitFor((): void => { expect(fetchUsageSummary).toHaveBeenCalledWith("7d"); }); }); it("should show empty state when no tasks exist", async (): Promise => { setupMocks({ empty: true }); render(); await waitFor((): void => { expect(screen.getByTestId("empty-state")).toBeInTheDocument(); }); expect(screen.getByText("No usage data yet")).toBeInTheDocument(); }); it("should show error state on fetch failure", async (): Promise => { setupMocks({ error: true }); render(); await waitFor((): void => { expect(screen.getByText("Network error")).toBeInTheDocument(); }); expect(screen.getByText("Try again")).toBeInTheDocument(); }); it("should retry loading when Try again button is clicked after error", async (): Promise => { setupMocks({ error: true }); render(); await waitFor((): void => { expect(screen.getByText("Try again")).toBeInTheDocument(); }); // Now set up success mocks and click retry setupMocks(); fireEvent.click(screen.getByText("Try again")); await waitFor((): void => { expect(screen.getByTestId("summary-cards")).toBeInTheDocument(); }); }); it("should display chart section titles", async (): Promise => { setupMocks(); render(); await waitFor((): void => { expect(screen.getByText("Token Usage Over Time")).toBeInTheDocument(); expect(screen.getByText("Cost by Model")).toBeInTheDocument(); expect(screen.getByText("Task Outcomes")).toBeInTheDocument(); }); }); it("should render recharts components within chart containers", async (): Promise => { setupMocks(); render(); await waitFor((): void => { expect(screen.getByTestId("recharts-line-chart")).toBeInTheDocument(); expect(screen.getByTestId("recharts-bar-chart")).toBeInTheDocument(); expect(screen.getByTestId("recharts-pie-chart")).toBeInTheDocument(); }); }); });