diff --git a/ui/litellm-dashboard/src/components/DebugWarningBanner.test.tsx b/ui/litellm-dashboard/src/components/DebugWarningBanner.test.tsx new file mode 100644 index 00000000000..2412a84da3e --- /dev/null +++ b/ui/litellm-dashboard/src/components/DebugWarningBanner.test.tsx @@ -0,0 +1,41 @@ +import { renderWithProviders, screen } from "../../tests/test-utils"; +import { vi } from "vitest"; +import { DebugWarningBanner } from "./DebugWarningBanner"; + +vi.mock("@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness", () => ({ + useHealthReadiness: vi.fn(), +})); + +import { useHealthReadiness } from "@/app/(dashboard)/hooks/healthReadiness/useHealthReadiness"; + +describe("DebugWarningBanner", () => { + it("should render", () => { + vi.mocked(useHealthReadiness).mockReturnValue({ data: { is_detailed_debug: true } } as any); + renderWithProviders(); + expect(screen.getByRole("alert")).toBeInTheDocument(); + }); + + it("should show warning when detailed debug mode is active", () => { + vi.mocked(useHealthReadiness).mockReturnValue({ data: { is_detailed_debug: true } } as any); + renderWithProviders(); + expect(screen.getByText(/Performance Warning: Detailed Debug Mode Active/i)).toBeInTheDocument(); + }); + + it("should mention LITELLM_LOG=DEBUG in the description", () => { + vi.mocked(useHealthReadiness).mockReturnValue({ data: { is_detailed_debug: true } } as any); + renderWithProviders(); + expect(screen.getByText("LITELLM_LOG=DEBUG")).toBeInTheDocument(); + }); + + it("should render nothing when is_detailed_debug is false", () => { + vi.mocked(useHealthReadiness).mockReturnValue({ data: { is_detailed_debug: false } } as any); + const { container } = renderWithProviders(); + expect(container).toBeEmptyDOMElement(); + }); + + it("should render nothing when health data is undefined", () => { + vi.mocked(useHealthReadiness).mockReturnValue({ data: undefined } as any); + const { container } = renderWithProviders(); + expect(container).toBeEmptyDOMElement(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportFormatSelector.test.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportFormatSelector.test.tsx new file mode 100644 index 00000000000..8447df4a6b9 --- /dev/null +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportFormatSelector.test.tsx @@ -0,0 +1,20 @@ +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import { vi } from "vitest"; +import ExportFormatSelector from "./ExportFormatSelector"; + +describe("ExportFormatSelector", () => { + it("should render", () => { + renderWithProviders(); + expect(screen.getByText("Format")).toBeInTheDocument(); + }); + + it("should display the current value", () => { + renderWithProviders(); + expect(screen.getByText("CSV (Excel, Google Sheets)")).toBeInTheDocument(); + }); + + it("should display JSON label when json is selected", () => { + renderWithProviders(); + expect(screen.getByText("JSON (includes metadata)")).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportSummary.test.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportSummary.test.tsx new file mode 100644 index 00000000000..8ebeb33b1eb --- /dev/null +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportSummary.test.tsx @@ -0,0 +1,59 @@ +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import ExportSummary from "./ExportSummary"; + +describe("ExportSummary", () => { + it("should render", () => { + const dateRange = { + from: new Date("2024-01-01"), + to: new Date("2024-01-31"), + }; + const { container } = renderWithProviders( + + ); + expect(container).not.toBeEmptyDOMElement(); + }); + + it("should display the date range", () => { + const from = new Date(2024, 0, 1); + const to = new Date(2024, 0, 31); + const dateRange = { from, to }; + renderWithProviders( + + ); + expect(screen.getByText(new RegExp(from.toLocaleDateString()))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(to.toLocaleDateString()))).toBeInTheDocument(); + }); + + it("should show filter count when filters are selected", () => { + const dateRange = { + from: new Date("2024-01-01"), + to: new Date("2024-01-31"), + }; + renderWithProviders( + + ); + expect(screen.getByText(/3 filters/)).toBeInTheDocument(); + }); + + it("should show singular 'filter' for one filter", () => { + const dateRange = { + from: new Date("2024-01-01"), + to: new Date("2024-01-31"), + }; + renderWithProviders( + + ); + expect(screen.getByText(/1 filter$/)).toBeInTheDocument(); + }); + + it("should not show filter count when no filters selected", () => { + const dateRange = { + from: new Date("2024-01-01"), + to: new Date("2024-01-31"), + }; + renderWithProviders( + + ); + expect(screen.queryByText(/filter/)).not.toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.test.tsx b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.test.tsx new file mode 100644 index 00000000000..36d2e12b717 --- /dev/null +++ b/ui/litellm-dashboard/src/components/EntityUsageExport/ExportTypeSelector.test.tsx @@ -0,0 +1,46 @@ +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import userEvent from "@testing-library/user-event"; +import { vi } from "vitest"; +import ExportTypeSelector from "./ExportTypeSelector"; + +describe("ExportTypeSelector", () => { + it("should render", () => { + renderWithProviders( + + ); + expect(screen.getByText("Export type")).toBeInTheDocument(); + }); + + it("should display entity type in radio labels", () => { + renderWithProviders( + + ); + expect(screen.getByText(/Day-by-day breakdown by team$/)).toBeInTheDocument(); + expect(screen.getByText(/Day-by-day breakdown by team and key/)).toBeInTheDocument(); + expect(screen.getByText(/Day-by-day by team and model/)).toBeInTheDocument(); + }); + + it("should display the correct entity type for different entities", () => { + renderWithProviders( + + ); + expect(screen.getByText(/Day-by-day breakdown by organization$/)).toBeInTheDocument(); + }); + + it("should call onChange when a radio option is selected", async () => { + const user = userEvent.setup(); + const onChange = vi.fn(); + renderWithProviders( + + ); + await user.click(screen.getByRole("radio", { name: /Day-by-day breakdown by team and key/i })); + expect(onChange).toHaveBeenCalledWith("daily_with_keys"); + }); + + it("should have the correct radio checked", () => { + renderWithProviders( + + ); + expect(screen.getByRole("radio", { name: /Day-by-day by team and model/i })).toBeChecked(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/GuardrailsMonitor/MetricCard.test.tsx b/ui/litellm-dashboard/src/components/GuardrailsMonitor/MetricCard.test.tsx new file mode 100644 index 00000000000..d30df1a0062 --- /dev/null +++ b/ui/litellm-dashboard/src/components/GuardrailsMonitor/MetricCard.test.tsx @@ -0,0 +1,49 @@ +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import React from "react"; +import { MetricCard } from "./MetricCard"; + +describe("MetricCard", () => { + it("should render", () => { + renderWithProviders(); + expect(screen.getByText("Total Requests")).toBeInTheDocument(); + }); + + it("should display the label and value", () => { + renderWithProviders(); + expect(screen.getByText("Success Rate")).toBeInTheDocument(); + expect(screen.getByText("98.5%")).toBeInTheDocument(); + }); + + it("should display numeric values", () => { + renderWithProviders(); + expect(screen.getByText("42")).toBeInTheDocument(); + }); + + it("should render icon when provided", () => { + renderWithProviders( + icon} + /> + ); + expect(screen.getByTestId("test-icon")).toBeInTheDocument(); + }); + + it("should not render icon container when no icon provided", () => { + renderWithProviders(); + expect(screen.queryByTestId("test-icon")).not.toBeInTheDocument(); + }); + + it("should render subtitle when provided", () => { + renderWithProviders( + + ); + expect(screen.getByText("Last 24 hours")).toBeInTheDocument(); + }); + + it("should not render subtitle when not provided", () => { + renderWithProviders(); + expect(screen.queryByText("Last 24 hours")).not.toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/HelpLink.test.tsx b/ui/litellm-dashboard/src/components/HelpLink.test.tsx index 72033bd93a1..76c42b8159b 100644 --- a/ui/litellm-dashboard/src/components/HelpLink.test.tsx +++ b/ui/litellm-dashboard/src/components/HelpLink.test.tsx @@ -23,6 +23,11 @@ describe("HelpLink", () => { expect(screen.getByText("Custom docs link")).toBeInTheDocument(); }); + it("should have the correct href", () => { + renderWithProviders(); + expect(screen.getByRole("link")).toHaveAttribute("href", "https://docs.example.com/test"); + }); + it("should include a screen-reader-only label for accessibility", () => { renderWithProviders(); @@ -46,7 +51,21 @@ describe("HelpIcon", () => { expect(screen.getByText("Tooltip help text")).toBeInTheDocument(); }); + it("should hide tooltip content when not hovered", () => { + renderWithProviders(); + expect(screen.queryByText("Hidden tooltip")).not.toBeInTheDocument(); + }); + it("should show learn more link when learnMoreHref is provided", async () => { + const user = userEvent.setup(); + renderWithProviders( + + ); + await user.hover(screen.getByRole("button", { name: /help information/i })); + expect(screen.getByText("Learn more")).toBeInTheDocument(); + }); + + it("should use custom learn more text when provided", async () => { const user = userEvent.setup(); renderWithProviders( { expect(screen.getByRole("button", { name: /docs/i })).toBeInTheDocument(); }); + it("should hide menu items initially", () => { + renderWithProviders(); + expect(screen.queryByText("Custom pricing")).not.toBeInTheDocument(); + }); + it("should show menu items when button is clicked", async () => { const user = userEvent.setup(); renderWithProviders(); diff --git a/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.test.tsx b/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.test.tsx new file mode 100644 index 00000000000..cde59fe9c6d --- /dev/null +++ b/ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.test.tsx @@ -0,0 +1,84 @@ +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import { vi } from "vitest"; +import { + PolicySelect, + policyStyle, + INPUT_POLICY_OPTIONS, + OUTPUT_POLICY_OPTIONS, +} from "./PolicySelect"; + +describe("policyStyle", () => { + it("should return the matching option for a known policy", () => { + expect(policyStyle("trusted")).toEqual(INPUT_POLICY_OPTIONS[1]); + }); + + it("should return the matching option for blocked", () => { + expect(policyStyle("blocked")).toEqual(INPUT_POLICY_OPTIONS[2]); + }); + + it("should return the first option as fallback for unknown policy", () => { + expect(policyStyle("unknown")).toEqual(INPUT_POLICY_OPTIONS[0]); + }); +}); + +describe("PolicySelect", () => { + it("should render", () => { + renderWithProviders( + + ); + expect(screen.getByText("untrusted")).toBeInTheDocument(); + }); + + it("should show the current policy value", () => { + renderWithProviders( + + ); + expect(screen.getByText("trusted")).toBeInTheDocument(); + }); + + it("should be disabled when saving is true", () => { + renderWithProviders( + + ); + expect(screen.getByRole("combobox")).toHaveAttribute("aria-expanded", "false"); + expect(screen.getByRole("combobox").closest(".ant-select")).toHaveClass("ant-select-disabled"); + }); + + it("should not be disabled when saving is false", () => { + renderWithProviders( + + ); + expect(screen.getByRole("combobox").closest(".ant-select")).not.toHaveClass("ant-select-disabled"); + }); +}); + +describe("Policy option constants", () => { + it("should have 3 input policy options", () => { + expect(INPUT_POLICY_OPTIONS).toHaveLength(3); + }); + + it("should have 2 output policy options (no blocked)", () => { + expect(OUTPUT_POLICY_OPTIONS).toHaveLength(2); + expect(OUTPUT_POLICY_OPTIONS.map((o) => o.value)).toEqual(["untrusted", "trusted"]); + }); +}); diff --git a/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx new file mode 100644 index 00000000000..59d8721b143 --- /dev/null +++ b/ui/litellm-dashboard/src/components/add_model/ComplexityRouterConfig.test.tsx @@ -0,0 +1,82 @@ +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import { vi } from "vitest"; +import ComplexityRouterConfig from "./ComplexityRouterConfig"; + +const mockModelInfo = [ + { model_group: "gpt-4" }, + { model_group: "gpt-3.5-turbo" }, + { model_group: "claude-3-opus" }, +] as any[]; + +const defaultTiers = { + SIMPLE: "gpt-3.5-turbo", + MEDIUM: "gpt-3.5-turbo", + COMPLEX: "gpt-4", + REASONING: "claude-3-opus", +}; + +describe("ComplexityRouterConfig", () => { + it("should render", () => { + renderWithProviders( + + ); + expect(screen.getByText("Complexity Tier Configuration")).toBeInTheDocument(); + }); + + it("should display all four tier labels", () => { + renderWithProviders( + + ); + expect(screen.getByText("Simple Tier")).toBeInTheDocument(); + expect(screen.getByText("Medium Tier")).toBeInTheDocument(); + expect(screen.getByText("Complex Tier")).toBeInTheDocument(); + expect(screen.getByText("Reasoning Tier")).toBeInTheDocument(); + }); + + it("should show example queries for each tier", () => { + renderWithProviders( + + ); + expect(screen.getByText(/Hello!/)).toBeInTheDocument(); + expect(screen.getByText(/Explain how REST APIs work/)).toBeInTheDocument(); + expect(screen.getByText(/Design a microservices architecture/)).toBeInTheDocument(); + expect(screen.getByText(/Think step by step/)).toBeInTheDocument(); + }); + + it("should display the how classification works section", () => { + renderWithProviders( + + ); + expect(screen.getByText("How Classification Works")).toBeInTheDocument(); + }); + + it("should show score thresholds in the classification section", () => { + renderWithProviders( + + ); + expect(screen.getByText(/Score < 0.15/)).toBeInTheDocument(); + expect(screen.getByText(/Score 0.15 - 0.35/)).toBeInTheDocument(); + expect(screen.getByText(/Score 0.35 - 0.60/)).toBeInTheDocument(); + expect(screen.getByText(/Score > 0.60/)).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/agents/agent_card_grid.test.tsx b/ui/litellm-dashboard/src/components/agents/agent_card_grid.test.tsx new file mode 100644 index 00000000000..c8a21d1b6e8 --- /dev/null +++ b/ui/litellm-dashboard/src/components/agents/agent_card_grid.test.tsx @@ -0,0 +1,90 @@ +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import { vi } from "vitest"; +import AgentCardGrid from "./agent_card_grid"; +import type { Agent, AgentKeyInfo } from "./types"; + +vi.mock("./agent_card", () => ({ + default: ({ agent, onAgentClick }: any) => ( +
onAgentClick(agent.agent_id)}> + {agent.agent_name} +
+ ), +})); + +const mockAgents: Agent[] = [ + { + agent_id: "agent-1", + agent_name: "Test Agent 1", + litellm_params: { model: "gpt-4" }, + agent_card_params: { description: "First agent" }, + }, + { + agent_id: "agent-2", + agent_name: "Test Agent 2", + litellm_params: { model: "claude-3" }, + agent_card_params: { description: "Second agent" }, + }, +]; + +const mockKeyInfoMap: Record = { + "agent-1": { has_key: true, key_alias: "key-1" }, + "agent-2": { has_key: false }, +}; + +const defaultProps = { + agentsList: mockAgents, + keyInfoMap: mockKeyInfoMap, + isLoading: false, + onDeleteClick: vi.fn(), + accessToken: "test-token", + onAgentUpdated: vi.fn(), + isAdmin: true, + onAgentClick: vi.fn(), +}; + +describe("AgentCardGrid", () => { + it("should render", () => { + renderWithProviders(); + expect(screen.getByText("Test Agent 1")).toBeInTheDocument(); + }); + + it("should render all agent cards", () => { + renderWithProviders(); + expect(screen.getByText("Test Agent 1")).toBeInTheDocument(); + expect(screen.getByText("Test Agent 2")).toBeInTheDocument(); + }); + + it("should show loading skeletons when isLoading is true", () => { + renderWithProviders(); + expect(screen.queryByText("Test Agent 1")).not.toBeInTheDocument(); + }); + + it("should show admin empty state message when no agents and isAdmin", () => { + renderWithProviders( + + ); + expect( + screen.getByText("No agents found. Create one to get started.") + ).toBeInTheDocument(); + }); + + it("should show non-admin empty state message when no agents and not admin", () => { + renderWithProviders( + + ); + expect( + screen.getByText("No agents found. Contact an admin to create agents.") + ).toBeInTheDocument(); + }); + + it("should call onAgentClick when a card is clicked", async () => { + const onAgentClick = vi.fn(); + renderWithProviders( + + ); + const { default: userEvent } = await import("@testing-library/user-event"); + const user = userEvent.setup(); + await user.click(screen.getByTestId("agent-card-agent-1")); + expect(onAgentClick).toHaveBeenCalledWith("agent-1"); + }); +}); diff --git a/ui/litellm-dashboard/src/components/common_components/RateLimitTypeFormItem.test.tsx b/ui/litellm-dashboard/src/components/common_components/RateLimitTypeFormItem.test.tsx new file mode 100644 index 00000000000..49b8028c510 --- /dev/null +++ b/ui/litellm-dashboard/src/components/common_components/RateLimitTypeFormItem.test.tsx @@ -0,0 +1,61 @@ +import { renderWithProviders, screen } from "../../../tests/test-utils"; +import userEvent from "@testing-library/user-event"; +import { vi } from "vitest"; +import { Form } from "antd"; +import React from "react"; +import { RateLimitTypeFormItem } from "./RateLimitTypeFormItem"; + +const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( +
{children}
+); + +describe("RateLimitTypeFormItem", () => { + it("should render", () => { + renderWithProviders( + + + + ); + expect(screen.getByText(/TPM Rate Limit Type/)).toBeInTheDocument(); + }); + + it("should display TPM label for tpm type", () => { + renderWithProviders( + + + + ); + expect(screen.getByText(/TPM Rate Limit Type/)).toBeInTheDocument(); + }); + + it("should display RPM label for rpm type", () => { + renderWithProviders( + + + + ); + expect(screen.getByText(/RPM Rate Limit Type/)).toBeInTheDocument(); + }); + + it("should show the select placeholder by default", () => { + renderWithProviders( + + + + ); + expect(screen.getByText("Select rate limit type")).toBeInTheDocument(); + }); + + it("should call onChange when provided", async () => { + const user = userEvent.setup(); + const onChange = vi.fn(); + renderWithProviders( + + + + ); + await user.click(screen.getByRole("combobox")); + await user.click(screen.getByText("Guaranteed throughput")); + expect(onChange).toHaveBeenCalledWith("guaranteed_throughput"); + }); +});