From eb23dc2e8151d8e983816245d0379115ced1ee8e Mon Sep 17 00:00:00 2001 From: Yuneng Jiang Date: Thu, 13 Aug 2026 09:37:43 -0700 Subject: [PATCH] test(ui): pin SectionHeader and ToolsSection behaviour before migration --- .../LogDetailsDrawer/SectionHeader.test.tsx | 64 +++++++++++++++++ .../ToolsSection/ToolsSection.test.tsx | 69 +++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/SectionHeader.test.tsx diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/SectionHeader.test.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/SectionHeader.test.tsx new file mode 100644 index 00000000000..5aee6b33ec5 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/SectionHeader.test.tsx @@ -0,0 +1,64 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it, vi } from "vitest"; +import { SectionHeader } from "./SectionHeader"; + +describe("SectionHeader", () => { + it("renders the input label with token, cost and turn metrics", () => { + render(); + + expect(screen.getByText("Input")).toBeInTheDocument(); + expect(screen.getByText("Tokens: 1,234")).toBeInTheDocument(); + expect(screen.getByText("Cost: $0.000123")).toBeInTheDocument(); + expect(screen.getByText("Turns: 3")).toBeInTheDocument(); + }); + + it("renders the output label", () => { + render(); + + expect(screen.getByText("Output")).toBeInTheDocument(); + }); + + it("omits metrics that were not provided", () => { + render(); + + expect(screen.queryByText(/^Tokens:/)).not.toBeInTheDocument(); + expect(screen.queryByText(/^Cost:/)).not.toBeInTheDocument(); + expect(screen.queryByText(/^Turns:/)).not.toBeInTheDocument(); + }); + + it("omits the turn count when there are no turns", () => { + render(); + + expect(screen.queryByText(/^Turns:/)).not.toBeInTheDocument(); + }); + + it("copies without toggling the section", async () => { + const onCopy = vi.fn(); + const onToggleCollapse = vi.fn(); + render(); + + await userEvent.click(screen.getByRole("button", { name: /copy/i })); + + expect(onCopy).toHaveBeenCalledTimes(1); + expect(onToggleCollapse).not.toHaveBeenCalled(); + }); + + it("toggles the section when the header is clicked", async () => { + const onToggleCollapse = vi.fn(); + render(); + + await userEvent.click(screen.getByText("Input")); + + expect(onToggleCollapse).toHaveBeenCalledTimes(1); + }); + + it("stays inert when no toggle handler is given", async () => { + const onCopy = vi.fn(); + render(); + + await userEvent.click(screen.getByText("Input")); + + expect(onCopy).not.toHaveBeenCalled(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.test.tsx b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.test.tsx index 753a552b6db..2e9d83d9c6d 100644 --- a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.test.tsx @@ -2,10 +2,46 @@ * Core tests for Tools section */ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { describe, it, expect } from "vitest"; import { parseToolsFromLog } from "./utils"; +import { ToolsSection } from "./ToolsSection"; import { LogEntry } from "../columns"; +const logWithTools = (toolNames: string[], calledName?: string): LogEntry => ({ + request_id: "render-1", + api_key: "key", + team_id: "team", + model: "gpt-4", + model_id: "gpt-4", + call_type: "completion", + spend: 0.01, + total_tokens: 100, + prompt_tokens: 50, + completion_tokens: 50, + startTime: "2024-01-01T00:00:00Z", + endTime: "2024-01-01T00:00:01Z", + cache_hit: "none", + messages: JSON.stringify({ + model: "gpt-4", + messages: [{ role: "user", content: "hi" }], + tools: toolNames.map((name) => ({ + type: "function", + function: { name, description: `${name} description`, parameters: { type: "object", properties: {} } }, + })), + }), + response: JSON.stringify({ + choices: [ + { + message: calledName + ? { tool_calls: [{ id: "call_1", type: "function", function: { name: calledName, arguments: "{}" } }] } + : { content: "done" }, + }, + ], + }), +}); + describe("ToolsSection", () => { it("should parse tools from request and match with response tool calls", () => { const mockLog: LogEntry = { @@ -115,3 +151,36 @@ describe("ToolsSection", () => { expect(tools).toHaveLength(0); }); }); + +describe("ToolsSection rendering", () => { + it("summarises how many tools were provided and called", () => { + render(); + + expect(screen.getByText("Tools")).toBeInTheDocument(); + expect(screen.getByText("2 provided, 1 called")).toBeInTheDocument(); + }); + + it("previews the first two tool names", () => { + render(); + + expect(screen.getByText(/alpha, beta/)).toBeInTheDocument(); + }); + + it("renders nothing when the log has no tools", () => { + const { container } = render(); + + expect(container).toBeEmptyDOMElement(); + }); + + it("reveals the tool list only after the section is expanded", async () => { + render(); + + expect(screen.queryByText("called")).not.toBeInTheDocument(); + expect(screen.queryByText("not called")).not.toBeInTheDocument(); + + await userEvent.click(screen.getByText("Tools")); + + expect(await screen.findByText("called")).toBeInTheDocument(); + expect(screen.getByText("not called")).toBeInTheDocument(); + }); +});