diff --git a/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.test.tsx b/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.test.tsx new file mode 100644 index 0000000000..61968294e1 --- /dev/null +++ b/ui/litellm-dashboard/src/components/UsagePage/components/KeyModelUsageView.test.tsx @@ -0,0 +1,298 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it } from "vitest"; +import KeyModelUsageView from "./KeyModelUsageView"; +import { TopModelData } from "../types"; + +describe("KeyModelUsageView", () => { + const mockTopModels: TopModelData[] = [ + { + model: "gpt-4", + spend: 150.5, + requests: 105, + successful_requests: 100, + failed_requests: 5, + tokens: 50000, + }, + { + model: "gpt-3.5-turbo", + spend: 75.25, + requests: 200, + successful_requests: 195, + failed_requests: 5, + tokens: 100000, + }, + ]; + + it("should render", () => { + render(); + expect(screen.getByText("Model Usage")).toBeInTheDocument(); + }); + + it("should return null when topModels is empty", () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("should display Model Usage title", () => { + render(); + expect(screen.getByText("Model Usage")).toBeInTheDocument(); + }); + + it("should display Table view button", () => { + render(); + expect(screen.getByRole("button", { name: "Table" })).toBeInTheDocument(); + }); + + it("should display Chart view button", () => { + render(); + expect(screen.getByRole("button", { name: "Chart" })).toBeInTheDocument(); + }); + + it("should default to table view", () => { + render(); + const tableButton = screen.getByRole("button", { name: "Table" }); + expect(tableButton).toHaveClass("bg-blue-100"); + }); + + it("should display all table column headers", () => { + render(); + expect(screen.getByText("Model")).toBeInTheDocument(); + expect(screen.getByText("Spend (USD)")).toBeInTheDocument(); + expect(screen.getByText("Successful")).toBeInTheDocument(); + expect(screen.getByText("Failed")).toBeInTheDocument(); + expect(screen.getByText("Tokens")).toBeInTheDocument(); + }); + + it("should display model data in table view", () => { + render(); + expect(screen.getByText("gpt-4")).toBeInTheDocument(); + expect(screen.getByText("gpt-3.5-turbo")).toBeInTheDocument(); + expect(screen.getByText("$150.50")).toBeInTheDocument(); + expect(screen.getByText("$75.25")).toBeInTheDocument(); + }); + + it("should format spend values with two decimal places", () => { + const modelsWithDecimalSpend: TopModelData[] = [ + { + model: "test-model", + spend: 123.456, + requests: 10, + successful_requests: 10, + failed_requests: 0, + tokens: 1000, + }, + ]; + render(); + expect(screen.getByText("$123.46")).toBeInTheDocument(); + }); + + it("should format large spend values with commas", () => { + const modelsWithLargeSpend: TopModelData[] = [ + { + model: "test-model", + spend: 1234567.89, + requests: 10, + successful_requests: 10, + failed_requests: 0, + tokens: 1000, + }, + ]; + render(); + expect(screen.getByText("$1,234,567.89")).toBeInTheDocument(); + }); + + it("should display successful requests with green styling", () => { + render(); + const successfulElements = screen.getAllByText("100"); + const greenElement = successfulElements.find((el) => el.closest("span")?.classList.contains("text-green-600")); + expect(greenElement).toBeDefined(); + }); + + it("should display failed requests with red styling", () => { + render(); + const failedElements = screen.getAllByText("5"); + const redElement = failedElements.find((el) => el.closest("span")?.classList.contains("text-red-600")); + expect(redElement).toBeDefined(); + }); + + it("should format token numbers with commas", () => { + const modelsWithLargeTokens: TopModelData[] = [ + { + model: "test-model", + spend: 100, + requests: 10, + successful_requests: 10, + failed_requests: 0, + tokens: 1234567, + }, + ]; + render(); + expect(screen.getByText("1,234,567")).toBeInTheDocument(); + }); + + it("should display dash for missing model value", () => { + const modelsWithMissingModel: TopModelData[] = [ + { + model: "", + spend: 100, + requests: 10, + successful_requests: 10, + failed_requests: 0, + tokens: 1000, + }, + ]; + render(); + expect(screen.getByText("-")).toBeInTheDocument(); + }); + + it("should display zero values correctly", () => { + const modelsWithZeros: TopModelData[] = [ + { + model: "test-model", + spend: 0, + requests: 0, + successful_requests: 0, + failed_requests: 0, + tokens: 0, + }, + ]; + render(); + expect(screen.getByText("$0.00")).toBeInTheDocument(); + expect(screen.getAllByText("0").length).toBeGreaterThan(0); + }); + + it("should switch to chart view when chart button is clicked", async () => { + const user = userEvent.setup(); + render(); + + const chartButton = screen.getByRole("button", { name: "Chart" }); + await user.click(chartButton); + + expect(chartButton).toHaveClass("bg-blue-100"); + const tableButton = screen.getByRole("button", { name: "Table" }); + expect(tableButton).not.toHaveClass("bg-blue-100"); + }); + + it("should switch back to table view when table button is clicked", async () => { + const user = userEvent.setup(); + render(); + + const chartButton = screen.getByRole("button", { name: "Chart" }); + const tableButton = screen.getByRole("button", { name: "Table" }); + + await user.click(chartButton); + await user.click(tableButton); + + expect(tableButton).toHaveClass("bg-blue-100"); + expect(chartButton).not.toHaveClass("bg-blue-100"); + }); + + it("should display chart when chart view is selected", async () => { + const user = userEvent.setup(); + render(); + + const chartButton = screen.getByRole("button", { name: "Chart" }); + await user.click(chartButton); + + expect(screen.queryByText("Model")).not.toBeInTheDocument(); + }); + + it("should display table when table view is selected", () => { + render(); + expect(screen.getByText("Model")).toBeInTheDocument(); + expect(screen.getByText("gpt-4")).toBeInTheDocument(); + }); + + it("should handle multiple model entries", () => { + const manyModels: TopModelData[] = Array.from({ length: 10 }, (_, i) => ({ + model: `model-${i + 1}`, + spend: 100 + i, + requests: 50 + i, + successful_requests: 45 + i, + failed_requests: 5, + tokens: 10000 + i * 1000, + })); + + render(); + expect(screen.getByText("model-1")).toBeInTheDocument(); + expect(screen.getByText("model-10")).toBeInTheDocument(); + }); + + it("should format successful requests with toLocaleString", () => { + const modelsWithLargeNumbers: TopModelData[] = [ + { + model: "test-model", + spend: 100, + requests: 1000, + successful_requests: 999999, + failed_requests: 1, + tokens: 1000, + }, + ]; + render(); + expect(screen.getByText("999,999")).toBeInTheDocument(); + }); + + it("should format failed requests with toLocaleString", () => { + const modelsWithLargeNumbers: TopModelData[] = [ + { + model: "test-model", + spend: 100, + requests: 1000, + successful_requests: 1, + failed_requests: 999999, + tokens: 1000, + }, + ]; + render(); + expect(screen.getByText("999,999")).toBeInTheDocument(); + }); + + it("should display zero for missing successful_requests", () => { + const modelsWithMissingFields: TopModelData[] = [ + { + model: "test-model", + spend: 100, + requests: 10, + successful_requests: undefined as any, + failed_requests: 0, + tokens: 1000, + }, + ]; + render(); + const zeroElements = screen.getAllByText("0"); + const successfulZero = zeroElements.find((el) => el.closest("span")?.classList.contains("text-green-600")); + expect(successfulZero).toBeDefined(); + }); + + it("should display zero for missing failed_requests", () => { + const modelsWithMissingFields: TopModelData[] = [ + { + model: "test-model", + spend: 100, + requests: 10, + successful_requests: 10, + failed_requests: undefined as any, + tokens: 1000, + }, + ]; + render(); + expect(screen.getAllByText("0").length).toBeGreaterThan(0); + }); + + it("should display zero for missing tokens", () => { + const modelsWithMissingFields: TopModelData[] = [ + { + model: "test-model", + spend: 100, + requests: 10, + successful_requests: 10, + failed_requests: 0, + tokens: undefined as any, + }, + ]; + render(); + expect(screen.getAllByText("0").length).toBeGreaterThan(0); + }); +});