diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/InputCard.test.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/InputCard.test.tsx new file mode 100644 index 00000000000..87b4bddb043 --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/InputCard.test.tsx @@ -0,0 +1,219 @@ +import React from "react"; +import { render, screen, waitFor, fireEvent, act } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { InputCard } from "./InputCard"; +import { ParsedMessage } from "./prettyMessagesTypes"; + +vi.mock("antd", async () => { + const actual = await vi.importActual("antd"); + return { + ...actual, + message: { + success: vi.fn(), + }, + }; +}); + +describe("InputCard", () => { + const mockWriteText = vi.fn().mockResolvedValue(undefined); + const mockMessages: ParsedMessage[] = [ + { + role: "user", + content: "Hello, how are you?", + }, + ]; + + beforeEach(() => { + vi.clearAllMocks(); + Object.defineProperty(navigator, "clipboard", { + value: { + writeText: mockWriteText, + }, + writable: true, + configurable: true, + }); + }); + + it("should render the InputCard component", () => { + render(); + expect(screen.getByText("Input")).toBeInTheDocument(); + }); + + it("should return null when messages array is empty", () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + it("should display system message when present", () => { + const messagesWithSystem: ParsedMessage[] = [ + { + role: "system", + content: "You are a helpful assistant", + }, + { + role: "user", + content: "Hello", + }, + ]; + render(); + expect(screen.getByText("SYSTEM")).toBeInTheDocument(); + expect(screen.getByText("You are a helpful assistant")).toBeInTheDocument(); + }); + + it("should display history messages when present", () => { + const messagesWithHistory: ParsedMessage[] = [ + { + role: "user", + content: "First message", + }, + { + role: "assistant", + content: "Response", + }, + { + role: "user", + content: "Last message", + }, + ]; + render(); + expect(screen.getByText("Last message")).toBeInTheDocument(); + }); + + it("should display last message content", () => { + const messages: ParsedMessage[] = [ + { + role: "user", + content: "What is the weather?", + }, + ]; + render(); + expect(screen.getByText("What is the weather?")).toBeInTheDocument(); + }); + + it("should display token count when provided", () => { + render(); + expect(screen.getByText(/Tokens: 150/)).toBeInTheDocument(); + }); + + it("should display cost when provided", () => { + render(); + expect(screen.getByText(/Cost: \$0\.001500/)).toBeInTheDocument(); + }); + + it("should copy last message content when copy button is clicked", async () => { + const messages: ParsedMessage[] = [ + { + role: "user", + content: "Copy this text", + }, + ]; + + render(); + + const copyButtons = screen.getAllByRole("button"); + const copyButton = copyButtons.find((button) => { + const icon = button.querySelector('[aria-label="copy"]'); + return icon !== null; + }); + + expect(copyButton).toBeInTheDocument(); + + await act(async () => { + fireEvent.click(copyButton!); + }); + + await waitFor(() => { + expect(mockWriteText).toHaveBeenCalledWith("Copy this text"); + }); + }); + + it("should toggle collapse state when header is clicked", async () => { + const user = userEvent.setup(); + render(); + + const header = screen.getByText("Input").closest("div"); + expect(header).toBeInTheDocument(); + + const content = screen.getByText("Hello, how are you?"); + expect(content).toBeInTheDocument(); + expect(content).toBeVisible(); + + if (header) { + await user.click(header); + await waitFor(() => { + expect(content).not.toBeVisible(); + }); + } + }); + + it("should handle messages without system message", () => { + const messages: ParsedMessage[] = [ + { + role: "user", + content: "User message only", + }, + ]; + render(); + expect(screen.queryByText("SYSTEM")).not.toBeInTheDocument(); + expect(screen.getByText("User message only")).toBeInTheDocument(); + }); + + it("should handle messages with only system message", () => { + const messages: ParsedMessage[] = [ + { + role: "system", + content: "System only", + }, + ]; + render(); + expect(screen.getByText("SYSTEM")).toBeInTheDocument(); + expect(screen.getByText("System only")).toBeInTheDocument(); + }); + + it("should display last message role label correctly", () => { + const messages: ParsedMessage[] = [ + { + role: "assistant", + content: "Assistant response", + }, + ]; + render(); + expect(screen.getByText("ASSISTANT")).toBeInTheDocument(); + expect(screen.getByText("Assistant response")).toBeInTheDocument(); + }); + + it("should handle tool calls in last message", () => { + const messages: ParsedMessage[] = [ + { + role: "user", + content: "Call a function", + toolCalls: [ + { + id: "call-1", + name: "get_weather", + arguments: { location: "NYC" }, + }, + ], + }, + ]; + render(); + expect(screen.getByText("Call a function")).toBeInTheDocument(); + }); + + it("should handle empty content in last message", () => { + const messages: ParsedMessage[] = [ + { + role: "user", + content: "", + }, + ]; + render(); + const copyButtons = screen.getAllByRole("button"); + const copyButton = copyButtons.find((button) => { + const icon = button.querySelector('[aria-label="copy"]'); + return icon !== null; + }); + expect(copyButton).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/InputCard.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/InputCard.tsx index 0451470f42c..1299f9c1685 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/InputCard.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/InputCard.tsx @@ -31,7 +31,7 @@ export function InputCard({ messages, promptTokens, inputCost }: InputCardProps) const historyMessages = nonSystemMessages.slice(0, -1); const handleCopy = () => { - const content = JSON.stringify(messages, null, 2); + const content = lastMessage?.content || ''; navigator.clipboard.writeText(content); message.success('Input copied'); }; diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/OutputCard.test.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/OutputCard.test.tsx new file mode 100644 index 00000000000..708e388366e --- /dev/null +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/OutputCard.test.tsx @@ -0,0 +1,173 @@ +import React from "react"; +import { render, screen, waitFor, fireEvent, act } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { OutputCard } from "./OutputCard"; +import { ParsedMessage } from "./prettyMessagesTypes"; + +vi.mock("antd", async () => { + const actual = await vi.importActual("antd"); + return { + ...actual, + message: { + success: vi.fn(), + }, + }; +}); + +describe("OutputCard", () => { + const mockWriteText = vi.fn().mockResolvedValue(undefined); + const mockMessage: ParsedMessage = { + role: "assistant", + content: "This is a test response", + }; + + beforeEach(() => { + vi.clearAllMocks(); + Object.defineProperty(navigator, "clipboard", { + value: { + writeText: mockWriteText, + }, + writable: true, + configurable: true, + }); + }); + + it("should render the OutputCard component", () => { + render(); + expect(screen.getByText("Output")).toBeInTheDocument(); + }); + + it("should display 'No response data available' when message is null", () => { + render(); + expect(screen.getByText("No response data available")).toBeInTheDocument(); + }); + + it("should display message content when message is present", () => { + render(); + expect(screen.getByText("This is a test response")).toBeInTheDocument(); + }); + + it("should display ASSISTANT label when message is present", () => { + render(); + expect(screen.getByText("ASSISTANT")).toBeInTheDocument(); + }); + + it("should display token count when provided", () => { + render(); + expect(screen.getByText(/Tokens: 250/)).toBeInTheDocument(); + }); + + it("should display cost when provided", () => { + render(); + expect(screen.getByText(/Cost: \$0\.002500/)).toBeInTheDocument(); + }); + + it("should copy message content when copy button is clicked", async () => { + render(); + + const copyButtons = screen.getAllByRole("button"); + const copyButton = copyButtons.find((button) => { + const icon = button.querySelector('[aria-label="copy"]'); + return icon !== null; + }); + + expect(copyButton).toBeInTheDocument(); + + await act(async () => { + fireEvent.click(copyButton!); + }); + + await waitFor(() => { + expect(mockWriteText).toHaveBeenCalledWith("This is a test response"); + }); + }); + + it("should not copy when message is null and copy button is clicked", async () => { + const user = userEvent.setup(); + + render(); + + const copyButtons = screen.getAllByRole("button"); + const copyButton = copyButtons.find((button) => { + const icon = button.querySelector('[aria-label="copy"]'); + return icon !== null; + }); + + expect(copyButton).toBeInTheDocument(); + await user.click(copyButton!); + + await waitFor(() => { + expect(mockWriteText).not.toHaveBeenCalled(); + }); + }); + + it("should toggle collapse state when header is clicked", async () => { + const user = userEvent.setup(); + render(); + + const header = screen.getByText("Output").closest("div"); + expect(header).toBeInTheDocument(); + + const content = screen.getByText("This is a test response"); + expect(content).toBeInTheDocument(); + expect(content).toBeVisible(); + + if (header) { + await user.click(header); + await waitFor(() => { + expect(content).not.toBeVisible(); + }); + } + }); + + it("should handle empty content in message", () => { + const messageWithEmptyContent: ParsedMessage = { + role: "assistant", + content: "", + }; + render(); + expect(screen.getByText("Output")).toBeInTheDocument(); + }); + + it("should handle tool calls in message", () => { + const messageWithToolCalls: ParsedMessage = { + role: "assistant", + content: "I'll call a function", + toolCalls: [ + { + id: "call-1", + name: "get_weather", + arguments: { location: "San Francisco" }, + }, + ], + }; + render(); + expect(screen.getByText("I'll call a function")).toBeInTheDocument(); + }); + + it("should display both token count and cost when both are provided", () => { + render(); + expect(screen.getByText(/Tokens: 300/)).toBeInTheDocument(); + expect(screen.getByText(/Cost: \$0\.003000/)).toBeInTheDocument(); + }); + + it("should handle collapse toggle when message is null", async () => { + const user = userEvent.setup(); + render(); + + const header = screen.getByText("Output").closest("div"); + expect(header).toBeInTheDocument(); + + const noDataText = screen.getByText("No response data available"); + expect(noDataText).toBeInTheDocument(); + expect(noDataText).toBeVisible(); + + if (header) { + await user.click(header); + await waitFor(() => { + expect(noDataText).not.toBeVisible(); + }); + } + }); +}); diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/OutputCard.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/OutputCard.tsx index 53deeba0659..eff8d83cbdf 100644 --- a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/OutputCard.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/OutputCard.tsx @@ -23,7 +23,7 @@ export function OutputCard({ message, completionTokens, outputCost }: OutputCard const handleCopy = () => { if (!message) return; - const content = JSON.stringify(message, null, 2); + const content = message.content || ''; navigator.clipboard.writeText(content); antdMessage.success('Output copied'); };