mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-20 06:23:46 +00:00
[Test] UI: Add vitest coverage for 10 previously untested components
Add unit tests for: - SimpleToolCallBlock, SimpleMessageBlock, CollapsibleMessage, HistoryTree (log details drawer) - OnboardingForm (onboarding flow) - TeamsHeaderTabs, TeamsTable (teams page) - transform_key_info, filter_helpers (key/team helpers) - queryKeysFactory (query key generation utility) 47 new tests covering conditional rendering, user interactions, data transformation, and error handling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
e5baa2232f
commit
0b07f628ff
@@ -0,0 +1,34 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { createQueryKeys } from "./queryKeysFactory";
|
||||
|
||||
describe("createQueryKeys", () => {
|
||||
const keys = createQueryKeys("books");
|
||||
|
||||
it("should return the resource name as the base key", () => {
|
||||
expect(keys.all).toEqual(["books"]);
|
||||
});
|
||||
|
||||
it("should generate a lists key", () => {
|
||||
expect(keys.lists()).toEqual(["books", "list"]);
|
||||
});
|
||||
|
||||
it("should generate a list key with params", () => {
|
||||
expect(keys.list({ page: 1, limit: 10 })).toEqual([
|
||||
"books",
|
||||
"list",
|
||||
{ params: { page: 1, limit: 10 } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should generate a list key with undefined params when none provided", () => {
|
||||
expect(keys.list()).toEqual(["books", "list", { params: undefined }]);
|
||||
});
|
||||
|
||||
it("should generate a details key", () => {
|
||||
expect(keys.details()).toEqual(["books", "detail"]);
|
||||
});
|
||||
|
||||
it("should generate a detail key for a specific ID", () => {
|
||||
expect(keys.detail("123")).toEqual(["books", "detail", "123"]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import TeamsHeaderTabs from "./TeamsHeaderTabs";
|
||||
|
||||
vi.mock("@tremor/react", () => ({
|
||||
TabGroup: ({ children, ...props }: any) => <div data-testid="tab-group" {...props}>{children}</div>,
|
||||
TabList: ({ children, ...props }: any) => <div data-testid="tab-list" {...props}>{children}</div>,
|
||||
Tab: ({ children, ...props }: any) => <button {...props}>{children}</button>,
|
||||
TabPanels: ({ children, ...props }: any) => <div data-testid="tab-panels" {...props}>{children}</div>,
|
||||
Text: ({ children, ...props }: any) => <span {...props}>{children}</span>,
|
||||
Icon: ({ onClick, ...props }: any) => <button data-testid="refresh-icon" onClick={onClick} />,
|
||||
}));
|
||||
|
||||
vi.mock("@heroicons/react/outline", () => ({
|
||||
RefreshIcon: () => <svg data-testid="refresh-svg" />,
|
||||
}));
|
||||
|
||||
const renderTabs = (props: Partial<Parameters<typeof TeamsHeaderTabs>[0]> = {}) => {
|
||||
const defaults = {
|
||||
lastRefreshed: "",
|
||||
onRefresh: vi.fn(),
|
||||
userRole: "Internal User",
|
||||
children: <div data-testid="panel-content">Panel</div>,
|
||||
};
|
||||
return render(<TeamsHeaderTabs {...defaults} {...props} />);
|
||||
};
|
||||
|
||||
describe("TeamsHeaderTabs", () => {
|
||||
it("should render 'Your Teams' and 'Available Teams' tabs", () => {
|
||||
renderTabs();
|
||||
|
||||
expect(screen.getByText("Your Teams")).toBeInTheDocument();
|
||||
expect(screen.getByText("Available Teams")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render 'Default Team Settings' tab when user is Admin", () => {
|
||||
renderTabs({ userRole: "Admin" });
|
||||
|
||||
expect(screen.getByText("Default Team Settings")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render 'Default Team Settings' tab for non-admin users", () => {
|
||||
renderTabs({ userRole: "Internal User" });
|
||||
|
||||
expect(screen.queryByText("Default Team Settings")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display last refreshed time when provided", () => {
|
||||
renderTabs({ lastRefreshed: "2024-06-01 12:00:00" });
|
||||
|
||||
expect(screen.getByText("Last Refreshed: 2024-06-01 12:00:00")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { Team } from "@/components/key_team_helpers/key_list";
|
||||
import TeamsTable from "./TeamsTable";
|
||||
|
||||
vi.mock("@tremor/react", () => ({
|
||||
Button: React.forwardRef<HTMLButtonElement, any>(({ children, ...props }, ref) =>
|
||||
React.createElement("button", { ...props, ref }, children),
|
||||
),
|
||||
Icon: ({ onClick, ...props }: any) => <button data-testid={props["data-testid"] || "icon-btn"} onClick={onClick} aria-label={props["aria-label"]} />,
|
||||
Table: ({ children }: any) => <table>{children}</table>,
|
||||
TableHead: ({ children }: any) => <thead>{children}</thead>,
|
||||
TableBody: ({ children }: any) => <tbody>{children}</tbody>,
|
||||
TableRow: ({ children }: any) => <tr>{children}</tr>,
|
||||
TableHeaderCell: ({ children }: any) => <th>{children}</th>,
|
||||
TableCell: ({ children, ...props }: any) => <td {...props}>{children}</td>,
|
||||
Text: ({ children }: any) => <span>{children}</span>,
|
||||
}));
|
||||
|
||||
vi.mock("antd", () => ({
|
||||
Tooltip: ({ children }: any) => <>{children}</>,
|
||||
}));
|
||||
|
||||
vi.mock("@heroicons/react/outline", () => ({
|
||||
PencilAltIcon: () => <svg data-testid="pencil-icon" />,
|
||||
TrashIcon: () => <svg data-testid="trash-icon" />,
|
||||
}));
|
||||
|
||||
vi.mock("@/utils/dataUtils", () => ({
|
||||
formatNumberWithCommas: (val: number, decimals: number) =>
|
||||
val != null ? val.toFixed(decimals) : "N/A",
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(dashboard)/teams/components/TeamsTable/ModelsCell", () => ({
|
||||
default: ({ team }: any) => <td data-testid="models-cell">{team.models.join(",")}</td>,
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(dashboard)/teams/components/TeamsTable/YourRoleCell/YourRoleCell", () => ({
|
||||
default: ({ team }: any) => <td data-testid="role-cell">{team.team_id}</td>,
|
||||
}));
|
||||
|
||||
const makeTeam = (overrides: Partial<Team> = {}): Team => ({
|
||||
team_id: "team-abc1234",
|
||||
team_alias: "Platform",
|
||||
models: ["gpt-4"],
|
||||
max_budget: 500,
|
||||
budget_duration: null,
|
||||
tpm_limit: null,
|
||||
rpm_limit: null,
|
||||
organization_id: "org-1",
|
||||
created_at: "2024-06-01T00:00:00Z",
|
||||
keys: [],
|
||||
members_with_roles: [],
|
||||
spend: 123.4567,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const defaultPerTeamInfo = {
|
||||
"team-abc1234": {
|
||||
keys: [{ token: "tok-1" } as any, { token: "tok-2" } as any],
|
||||
team_info: {
|
||||
members_with_roles: [{ user_id: "u1", role: "admin" } as any],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const renderTable = (overrides: Partial<Parameters<typeof TeamsTable>[0]> = {}) => {
|
||||
const defaults = {
|
||||
teams: [makeTeam()],
|
||||
currentOrg: null,
|
||||
perTeamInfo: defaultPerTeamInfo,
|
||||
userRole: "Admin",
|
||||
userId: "user-1",
|
||||
setSelectedTeamId: vi.fn(),
|
||||
setEditTeam: vi.fn(),
|
||||
onDeleteTeam: vi.fn(),
|
||||
};
|
||||
return render(<TeamsTable {...defaults} {...overrides} />);
|
||||
};
|
||||
|
||||
describe("TeamsTable", () => {
|
||||
it("should render table headers", () => {
|
||||
renderTable();
|
||||
|
||||
expect(screen.getByText("Team Name")).toBeInTheDocument();
|
||||
expect(screen.getByText("Team ID")).toBeInTheDocument();
|
||||
expect(screen.getByText("Created")).toBeInTheDocument();
|
||||
expect(screen.getByText("Spend (USD)")).toBeInTheDocument();
|
||||
expect(screen.getByText("Budget (USD)")).toBeInTheDocument();
|
||||
expect(screen.getByText("Models")).toBeInTheDocument();
|
||||
expect(screen.getByText("Organization")).toBeInTheDocument();
|
||||
expect(screen.getByText("Your Role")).toBeInTheDocument();
|
||||
expect(screen.getByText("Info")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render team rows with team data", () => {
|
||||
renderTable();
|
||||
|
||||
expect(screen.getByText("Platform")).toBeInTheDocument();
|
||||
expect(screen.getByText("team-ab...")).toBeInTheDocument();
|
||||
expect(screen.getByText("org-1")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show edit and delete icons for Admin users", () => {
|
||||
renderTable({ userRole: "Admin" });
|
||||
|
||||
expect(screen.getAllByTestId("icon-btn").length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
it("should not show edit and delete icons for non-Admin users", () => {
|
||||
renderTable({ userRole: "Internal User" });
|
||||
|
||||
// Only the team ID button should be present, no icon-btn for edit/delete
|
||||
const iconBtns = screen.queryAllByTestId("icon-btn");
|
||||
expect(iconBtns).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should call setSelectedTeamId when team ID button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const setSelectedTeamId = vi.fn();
|
||||
renderTable({ setSelectedTeamId });
|
||||
|
||||
await user.click(screen.getByText("team-ab..."));
|
||||
|
||||
expect(setSelectedTeamId).toHaveBeenCalledWith("team-abc1234");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { OnboardingForm } from "./OnboardingForm";
|
||||
|
||||
const mockUseOnboardingCredentials = vi.fn();
|
||||
const mockClaimToken = vi.fn();
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useSearchParams: () => new URLSearchParams("invitation_id=inv-123"),
|
||||
}));
|
||||
|
||||
vi.mock("jwt-decode", () => ({
|
||||
jwtDecode: vi.fn(() => ({
|
||||
user_email: "alice@example.com",
|
||||
user_id: "user-1",
|
||||
key: "access-tok",
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/onboarding/useOnboarding", () => ({
|
||||
useOnboardingCredentials: (...args: unknown[]) => mockUseOnboardingCredentials(...args),
|
||||
useClaimOnboardingToken: () => ({ mutate: mockClaimToken, isPending: false }),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
getProxyBaseUrl: vi.fn(() => ""),
|
||||
}));
|
||||
|
||||
vi.mock("./OnboardingLoadingView", () => ({
|
||||
OnboardingLoadingView: () => <div data-testid="loading-view">Loading</div>,
|
||||
}));
|
||||
|
||||
vi.mock("./OnboardingErrorView", () => ({
|
||||
OnboardingErrorView: () => <div data-testid="error-view">Error</div>,
|
||||
}));
|
||||
|
||||
vi.mock("./OnboardingFormBody", () => ({
|
||||
OnboardingFormBody: ({ variant, userEmail }: { variant: string; userEmail: string }) => (
|
||||
<div data-testid="form-body" data-variant={variant} data-email={userEmail}>
|
||||
Form Body
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("OnboardingForm", () => {
|
||||
it("should render loading view when credentials are loading", () => {
|
||||
mockUseOnboardingCredentials.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<OnboardingForm variant="signup" />);
|
||||
|
||||
expect(screen.getByTestId("loading-view")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render error view when credentials fail to load", () => {
|
||||
mockUseOnboardingCredentials.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
});
|
||||
|
||||
render(<OnboardingForm variant="signup" />);
|
||||
|
||||
expect(screen.getByTestId("error-view")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render form body with decoded email when credentials are loaded", () => {
|
||||
mockUseOnboardingCredentials.mockReturnValue({
|
||||
data: { token: "fake-jwt-token" },
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<OnboardingForm variant="signup" />);
|
||||
|
||||
expect(screen.getByTestId("form-body")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("form-body")).toHaveAttribute("data-email", "alice@example.com");
|
||||
});
|
||||
|
||||
it("should pass variant prop to OnboardingFormBody", () => {
|
||||
mockUseOnboardingCredentials.mockReturnValue({
|
||||
data: { token: "fake-jwt-token" },
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
render(<OnboardingForm variant="reset_password" />);
|
||||
|
||||
expect(screen.getByTestId("form-body")).toHaveAttribute("data-variant", "reset_password");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { fetchTeamFilterOptions } from "./filter_helpers";
|
||||
|
||||
const mockKeyListCall = vi.fn();
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
keyListCall: (...args: unknown[]) => mockKeyListCall(...args),
|
||||
teamListCall: vi.fn(),
|
||||
organizationListCall: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("fetchTeamFilterOptions", () => {
|
||||
it("should return empty arrays when accessToken is null", async () => {
|
||||
const result = await fetchTeamFilterOptions(null, "team-1");
|
||||
|
||||
expect(result).toEqual({ keyAliases: [], organizationIds: [], userIds: [] });
|
||||
expect(mockKeyListCall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return empty arrays when teamId is empty", async () => {
|
||||
const result = await fetchTeamFilterOptions("tok-123", "");
|
||||
|
||||
expect(result).toEqual({ keyAliases: [], organizationIds: [], userIds: [] });
|
||||
expect(mockKeyListCall).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should return sorted key aliases from fetched keys", async () => {
|
||||
mockKeyListCall.mockResolvedValue({
|
||||
keys: [
|
||||
{ key_alias: "zeta-key" },
|
||||
{ key_alias: "alpha-key" },
|
||||
{ key_alias: "mid-key" },
|
||||
],
|
||||
total_pages: 1,
|
||||
});
|
||||
|
||||
const result = await fetchTeamFilterOptions("tok-123", "team-1");
|
||||
|
||||
expect(result.keyAliases).toEqual(["alpha-key", "mid-key", "zeta-key"]);
|
||||
});
|
||||
|
||||
it("should deduplicate organization IDs across pages", async () => {
|
||||
mockKeyListCall
|
||||
.mockResolvedValueOnce({
|
||||
keys: [
|
||||
{ organization_id: "org-b" },
|
||||
{ organization_id: "org-a" },
|
||||
],
|
||||
total_pages: 2,
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
keys: [
|
||||
{ organization_id: "org-a" },
|
||||
{ organization_id: "org-c" },
|
||||
],
|
||||
total_pages: 2,
|
||||
});
|
||||
|
||||
const result = await fetchTeamFilterOptions("tok-123", "team-1");
|
||||
|
||||
expect(result.organizationIds).toEqual(["org-a", "org-b", "org-c"]);
|
||||
});
|
||||
|
||||
it("should map user IDs with email addresses", async () => {
|
||||
mockKeyListCall.mockResolvedValue({
|
||||
keys: [
|
||||
{ user_id: "u1", user: { user_email: "alice@example.com" } },
|
||||
{ user_id: "u2", user: { user_email: "bob@example.com" } },
|
||||
],
|
||||
total_pages: 1,
|
||||
});
|
||||
|
||||
const result = await fetchTeamFilterOptions("tok-123", "team-1");
|
||||
|
||||
expect(result.userIds).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ id: "u1", email: "alice@example.com" },
|
||||
{ id: "u2", email: "bob@example.com" },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle API errors gracefully and return empty arrays", async () => {
|
||||
mockKeyListCall.mockRejectedValue(new Error("Network error"));
|
||||
|
||||
const result = await fetchTeamFilterOptions("tok-123", "team-1");
|
||||
|
||||
expect(result).toEqual({ keyAliases: [], organizationIds: [], userIds: [] });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { transformKeyInfo } from "./transform_key_info";
|
||||
|
||||
describe("transformKeyInfo", () => {
|
||||
it("should combine key and info fields into a single object", () => {
|
||||
const apiResponse = {
|
||||
key: "sk-abc123",
|
||||
info: {
|
||||
token_id: "tok_1",
|
||||
key_name: "my-key",
|
||||
spend: 10.5,
|
||||
},
|
||||
};
|
||||
const result = transformKeyInfo(apiResponse);
|
||||
expect(result).toEqual({
|
||||
token: "sk-abc123",
|
||||
token_id: "tok_1",
|
||||
key_name: "my-key",
|
||||
spend: 10.5,
|
||||
});
|
||||
});
|
||||
|
||||
it("should set the token field from the key property", () => {
|
||||
const apiResponse = {
|
||||
key: "sk-xyz789",
|
||||
info: { key_name: "test" },
|
||||
};
|
||||
const result = transformKeyInfo(apiResponse);
|
||||
expect(result.token).toBe("sk-xyz789");
|
||||
});
|
||||
|
||||
it("should preserve all info fields in the result", () => {
|
||||
const apiResponse = {
|
||||
key: "sk-abc",
|
||||
info: {
|
||||
token_id: "tok_2",
|
||||
key_name: "prod-key",
|
||||
spend: 42,
|
||||
models: ["gpt-4"],
|
||||
team_id: "team-1",
|
||||
metadata: { env: "production" },
|
||||
},
|
||||
};
|
||||
const result = transformKeyInfo(apiResponse);
|
||||
expect(result.token_id).toBe("tok_2");
|
||||
expect(result.key_name).toBe("prod-key");
|
||||
expect(result.spend).toBe(42);
|
||||
expect(result.models).toEqual(["gpt-4"]);
|
||||
expect(result.team_id).toBe("team-1");
|
||||
expect(result.metadata).toEqual({ env: "production" });
|
||||
});
|
||||
|
||||
it("should handle empty info object", () => {
|
||||
const apiResponse = {
|
||||
key: "sk-empty",
|
||||
info: {},
|
||||
};
|
||||
const result = transformKeyInfo(apiResponse);
|
||||
expect(result.token).toBe("sk-empty");
|
||||
expect(Object.keys(result)).toContain("token");
|
||||
});
|
||||
});
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { CollapsibleMessage } from "./CollapsibleMessage";
|
||||
|
||||
describe("CollapsibleMessage", () => {
|
||||
it("should return null when content is empty", () => {
|
||||
const { container } = render(
|
||||
<CollapsibleMessage label="SYSTEM" content="" />
|
||||
);
|
||||
expect(container.innerHTML).toBe("");
|
||||
});
|
||||
|
||||
it("should return null when content is undefined", () => {
|
||||
const { container } = render(<CollapsibleMessage label="SYSTEM" />);
|
||||
expect(container.innerHTML).toBe("");
|
||||
});
|
||||
|
||||
it("should render the label and char count", () => {
|
||||
render(<CollapsibleMessage label="SYSTEM" content="Hello" />);
|
||||
expect(screen.getByText("SYSTEM")).toBeInTheDocument();
|
||||
expect(screen.getByText("(5 chars)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show content when defaultExpanded is true", () => {
|
||||
render(
|
||||
<CollapsibleMessage
|
||||
label="SYSTEM"
|
||||
content="Visible text"
|
||||
defaultExpanded={true}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("Visible text")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should toggle expanded state when header is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<CollapsibleMessage
|
||||
label="SYSTEM"
|
||||
content="Toggle me"
|
||||
defaultExpanded={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// Content is rendered in DOM but collapsed by default
|
||||
expect(screen.getByText("Toggle me")).toBeInTheDocument();
|
||||
|
||||
// Click the header to expand - should still show content
|
||||
await user.click(screen.getByText("SYSTEM"));
|
||||
expect(screen.getByText("Toggle me")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { HistoryTree } from "./HistoryTree";
|
||||
import { ParsedMessage } from "./prettyMessagesTypes";
|
||||
|
||||
describe("HistoryTree", () => {
|
||||
it("should return null when messages array is empty", () => {
|
||||
const { container } = render(<HistoryTree messages={[]} />);
|
||||
expect(container.innerHTML).toBe("");
|
||||
});
|
||||
|
||||
it('should render message count with plural "messages" for multiple messages', () => {
|
||||
const messages: ParsedMessage[] = [
|
||||
{ role: "user", content: "Hello" },
|
||||
{ role: "assistant", content: "Hi there" },
|
||||
{ role: "user", content: "How are you?" },
|
||||
];
|
||||
render(<HistoryTree messages={messages} />);
|
||||
expect(
|
||||
screen.getByText("HISTORY (3 messages)")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render message count with singular "message" for one message', () => {
|
||||
const messages: ParsedMessage[] = [
|
||||
{ role: "user", content: "Hello" },
|
||||
];
|
||||
render(<HistoryTree messages={messages} />);
|
||||
expect(
|
||||
screen.getByText("HISTORY (1 message)")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should expand and show messages when header is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const messages: ParsedMessage[] = [
|
||||
{ role: "user", content: "Hello" },
|
||||
{ role: "assistant", content: "Hi there" },
|
||||
];
|
||||
render(<HistoryTree messages={messages} />);
|
||||
|
||||
// Click to expand
|
||||
await user.click(screen.getByText("HISTORY (2 messages)"));
|
||||
|
||||
expect(screen.getByText("Hello")).toBeInTheDocument();
|
||||
expect(screen.getByText("Hi there")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { SimpleMessageBlock } from "./SimpleMessageBlock";
|
||||
|
||||
describe("SimpleMessageBlock", () => {
|
||||
it("should render the label and content", () => {
|
||||
render(<SimpleMessageBlock label="USER" content="Hello world" />);
|
||||
expect(screen.getByText("USER")).toBeInTheDocument();
|
||||
expect(screen.getByText("Hello world")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should return null when content is empty and no tool calls", () => {
|
||||
const { container } = render(
|
||||
<SimpleMessageBlock label="USER" content="" />
|
||||
);
|
||||
expect(container.innerHTML).toBe("");
|
||||
});
|
||||
|
||||
it('should return null when content is "null" string and no tool calls', () => {
|
||||
const { container } = render(
|
||||
<SimpleMessageBlock label="USER" content="null" />
|
||||
);
|
||||
expect(container.innerHTML).toBe("");
|
||||
});
|
||||
|
||||
it("should render tool calls when present", () => {
|
||||
render(
|
||||
<SimpleMessageBlock
|
||||
label="ASSISTANT"
|
||||
toolCalls={[
|
||||
{ id: "tc1", name: "get_weather", arguments: { city: "Paris" } },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("ASSISTANT")).toBeInTheDocument();
|
||||
expect(screen.getByText("get_weather")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render content and tool calls together", () => {
|
||||
render(
|
||||
<SimpleMessageBlock
|
||||
label="ASSISTANT"
|
||||
content="Let me check the weather."
|
||||
toolCalls={[
|
||||
{ id: "tc1", name: "get_weather", arguments: { city: "Paris" } },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
screen.getByText("Let me check the weather.")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("get_weather")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import React from "react";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { SimpleToolCallBlock } from "./SimpleToolCallBlock";
|
||||
|
||||
describe("SimpleToolCallBlock", () => {
|
||||
it("should render the tool name", () => {
|
||||
render(
|
||||
<SimpleToolCallBlock
|
||||
tool={{ id: "1", name: "get_weather", arguments: {} }}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("get_weather")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should display "function" badge', () => {
|
||||
render(
|
||||
<SimpleToolCallBlock
|
||||
tool={{ id: "1", name: "get_weather", arguments: {} }}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("function")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render arguments when present", () => {
|
||||
render(
|
||||
<SimpleToolCallBlock
|
||||
tool={{
|
||||
id: "1",
|
||||
name: "get_weather",
|
||||
arguments: { city: "London", units: "metric" },
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("city:")).toBeInTheDocument();
|
||||
expect(screen.getByText('"London"')).toBeInTheDocument();
|
||||
expect(screen.getByText("units:")).toBeInTheDocument();
|
||||
expect(screen.getByText('"metric"')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render arguments section when arguments are empty", () => {
|
||||
const { container } = render(
|
||||
<SimpleToolCallBlock
|
||||
tool={{ id: "1", name: "get_weather", arguments: {} }}
|
||||
/>
|
||||
);
|
||||
// The tool name and "function" badge should be there, but no key: value pairs
|
||||
expect(screen.getByText("get_weather")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/:$/)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user