[Test] UI - Logs: Add unit tests for 5 untested view_logs components

Add vitest tests for TypeBadges, ErrorViewer, ConfigInfoMessage, TimeCell, and TruncatedValue covering rendering, user interactions, and edge cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
yuneng-jiang 2026-03-10 13:42:30 -07:00
parent 6fe82d3886
commit bead0b7d90
5 changed files with 242 additions and 0 deletions

View file

@ -0,0 +1,41 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { ConfigInfoMessage } from "./ConfigInfoMessage";
describe("ConfigInfoMessage", () => {
it("should render the info message when show is true", () => {
render(<ConfigInfoMessage show={true} />);
expect(screen.getByText("Request/Response Data Not Available")).toBeInTheDocument();
});
it("should render nothing when show is false", () => {
const { container } = render(<ConfigInfoMessage show={false} />);
expect(container.innerHTML).toBe("");
});
it("should display the YAML config snippet", () => {
render(<ConfigInfoMessage show={true} />);
expect(screen.getByText(/store_prompts_in_spend_logs: true/)).toBeInTheDocument();
});
it("should render the settings button when onOpenSettings is provided", () => {
render(<ConfigInfoMessage show={true} onOpenSettings={() => {}} />);
expect(screen.getByText("open the settings")).toBeInTheDocument();
});
it("should not render the settings button when onOpenSettings is omitted", () => {
render(<ConfigInfoMessage show={true} />);
expect(screen.queryByText("open the settings")).not.toBeInTheDocument();
});
it("should call onOpenSettings when the settings button is clicked", async () => {
const user = userEvent.setup();
const onOpenSettings = vi.fn();
render(<ConfigInfoMessage show={true} onOpenSettings={onOpenSettings} />);
await user.click(screen.getByText("open the settings"));
expect(onOpenSettings).toHaveBeenCalledOnce();
});
});

View file

@ -0,0 +1,87 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import userEvent from "@testing-library/user-event";
import { ErrorViewer } from "./ErrorViewer";
const basicError = {
error_class: "NotFoundError",
error_message: "Model gpt-5 not found",
};
const errorWithTraceback = {
error_class: "AuthenticationError",
error_message: "Invalid API key",
traceback: `Traceback (most recent call last):
File "/app/main.py", line 42, in handle_request
result = await client.chat(model="gpt-4")
File "/app/llms/openai.py", line 100, in chat
response = self._make_request(payload)
File "/app/llms/base.py", line 55, in _make_request
raise AuthenticationError("Invalid API key")`,
};
describe("ErrorViewer", () => {
it("should render error type and message", () => {
render(<ErrorViewer errorInfo={basicError} />);
expect(screen.getByText("NotFoundError")).toBeInTheDocument();
expect(screen.getByText("Model gpt-5 not found")).toBeInTheDocument();
});
it("should show 'Unknown Error' when error_class is missing", () => {
render(<ErrorViewer errorInfo={{ error_message: "something broke" }} />);
expect(screen.getByText("Unknown Error")).toBeInTheDocument();
});
it("should show 'Unknown error occurred' when error_message is missing", () => {
render(<ErrorViewer errorInfo={{ error_class: "RuntimeError" }} />);
expect(screen.getByText("Unknown error occurred")).toBeInTheDocument();
});
it("should render traceback frames when traceback is present", () => {
render(<ErrorViewer errorInfo={errorWithTraceback} />);
expect(screen.getByText("Traceback")).toBeInTheDocument();
expect(screen.getByText("main.py")).toBeInTheDocument();
expect(screen.getByText("openai.py")).toBeInTheDocument();
expect(screen.getByText("base.py")).toBeInTheDocument();
});
it("should expand a frame when clicked", async () => {
const user = userEvent.setup();
render(<ErrorViewer errorInfo={errorWithTraceback} />);
await user.click(screen.getByText("main.py"));
expect(
screen.getByText("result = await client.chat(model=\"gpt-4\")")
).toBeInTheDocument();
});
it("should expand all frames when 'Expand All' is clicked", async () => {
const user = userEvent.setup();
render(<ErrorViewer errorInfo={errorWithTraceback} />);
await user.click(screen.getByText("Expand All"));
expect(screen.getByText("Collapse All")).toBeInTheDocument();
});
it("should copy traceback to clipboard when copy button is clicked", async () => {
const user = userEvent.setup();
const mockWriteText = vi.fn().mockResolvedValue(undefined);
Object.defineProperty(navigator, "clipboard", {
value: { writeText: mockWriteText },
writable: true,
configurable: true,
});
render(<ErrorViewer errorInfo={errorWithTraceback} />);
await user.click(screen.getByTitle("Copy traceback"));
expect(mockWriteText).toHaveBeenCalledWith(errorWithTraceback.traceback);
});
it("should not render traceback section when traceback is absent", () => {
render(<ErrorViewer errorInfo={basicError} />);
expect(screen.queryByText("Traceback")).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,32 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { TruncatedValue } from "./TruncatedValue";
describe("TruncatedValue", () => {
it("should render the value text", () => {
render(<TruncatedValue value="chatcmpl-abc123" />);
expect(screen.getByText("chatcmpl-abc123")).toBeInTheDocument();
});
it("should render a dash when value is undefined", () => {
render(<TruncatedValue />);
expect(screen.getByText("-")).toBeInTheDocument();
});
it("should render a dash when value is empty string", () => {
render(<TruncatedValue value="" />);
expect(screen.getByText("-")).toBeInTheDocument();
});
it("should apply the default maxWidth when not specified", () => {
render(<TruncatedValue value="some-long-id-value" />);
const el = screen.getByText("some-long-id-value");
expect(el).toHaveStyle({ maxWidth: "180px" });
});
it("should apply custom maxWidth when provided", () => {
render(<TruncatedValue value="test-value" maxWidth={300} />);
const el = screen.getByText("test-value");
expect(el).toHaveStyle({ maxWidth: "300px" });
});
});

View file

@ -0,0 +1,46 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { LlmBadge, McpBadge, AgentBadge } from "./TypeBadges";
describe("TypeBadges", () => {
describe("LlmBadge", () => {
it("should render with default 'LLM' text when no count is provided", () => {
render(<LlmBadge />);
expect(screen.getByText("LLM")).toBeInTheDocument();
});
it("should render the count when provided", () => {
render(<LlmBadge count={5} />);
expect(screen.getByText("5")).toBeInTheDocument();
});
it("should render count of 0 instead of default text", () => {
render(<LlmBadge count={0} />);
expect(screen.getByText("0")).toBeInTheDocument();
});
});
describe("McpBadge", () => {
it("should render with default 'MCP' text when no count is provided", () => {
render(<McpBadge />);
expect(screen.getByText("MCP")).toBeInTheDocument();
});
it("should render the count when provided", () => {
render(<McpBadge count={3} />);
expect(screen.getByText("3")).toBeInTheDocument();
});
});
describe("AgentBadge", () => {
it("should render with default 'Agent' text when no count is provided", () => {
render(<AgentBadge />);
expect(screen.getByText("Agent")).toBeInTheDocument();
});
it("should render the count when provided", () => {
render(<AgentBadge count={12} />);
expect(screen.getByText("12")).toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,36 @@
import { render, screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { TimeCell, getTimeZone } from "./time_cell";
describe("TimeCell", () => {
it("should render a formatted time string", () => {
render(<TimeCell utcTime="2025-06-15T14:30:00Z" />);
// The global toLocaleString mock in setupTests returns "YYYY-MM-DD HH:MM:SS"
expect(screen.getByText(/2025/)).toBeInTheDocument();
});
it("should render 'Error converting time' for invalid dates", () => {
// toLocaleString on an Invalid Date returns "Invalid Date", not throwing,
// but the component catches exceptions. Force an error by passing something
// that causes Date constructor to produce NaN.
render(<TimeCell utcTime="not-a-date" />);
// The mock returns "NaN-NaN-NaN NaN:NaN:NaN" for invalid dates
// The component has a try/catch that returns "Error converting time" on exception
const el = screen.getByText(/NaN|Error/);
expect(el).toBeInTheDocument();
});
it("should render with monospace font", () => {
render(<TimeCell utcTime="2025-06-15T14:30:00Z" />);
const span = screen.getByText(/2025/);
expect(span).toHaveStyle({ fontFamily: "monospace" });
});
});
describe("getTimeZone", () => {
it("should return a non-empty timezone string", () => {
const tz = getTimeZone();
expect(typeof tz).toBe("string");
expect(tz.length).toBeGreaterThan(0);
});
});