mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
[Test] UI: Add unit tests for 5 untested dashboard components
Add Vitest + RTL unit tests for WorkerDropdown, AccessGroupSelector, MemberTable, ModelSelector, and AutoRotationView (37 tests total). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3292d02aa4
commit
5d9136bb71
5 changed files with 558 additions and 0 deletions
|
|
@ -0,0 +1,78 @@
|
|||
import { renderWithProviders, screen } from "../../../../tests/test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
import WorkerDropdown from "./WorkerDropdown";
|
||||
|
||||
const mockUseWorker = vi.fn();
|
||||
|
||||
vi.mock("@/hooks/useWorker", () => ({
|
||||
useWorker: () => mockUseWorker(),
|
||||
}));
|
||||
|
||||
const workers = [
|
||||
{ worker_id: "w1", name: "Worker 1", url: "http://w1" },
|
||||
{ worker_id: "w2", name: "Worker 2", url: "http://w2" },
|
||||
{ worker_id: "w3", name: "Worker 3", url: "http://w3" },
|
||||
];
|
||||
|
||||
describe("WorkerDropdown", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
mockUseWorker.mockReturnValue({
|
||||
isControlPlane: true,
|
||||
selectedWorker: workers[0],
|
||||
workers,
|
||||
});
|
||||
|
||||
renderWithProviders(<WorkerDropdown onWorkerSwitch={vi.fn()} />);
|
||||
expect(screen.getByRole("combobox")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should return null when not a control plane", () => {
|
||||
mockUseWorker.mockReturnValue({
|
||||
isControlPlane: false,
|
||||
selectedWorker: null,
|
||||
workers: [],
|
||||
});
|
||||
|
||||
const { container } = renderWithProviders(
|
||||
<WorkerDropdown onWorkerSwitch={vi.fn()} />
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("should return null when there is no selected worker", () => {
|
||||
mockUseWorker.mockReturnValue({
|
||||
isControlPlane: true,
|
||||
selectedWorker: null,
|
||||
workers,
|
||||
});
|
||||
|
||||
const { container } = renderWithProviders(
|
||||
<WorkerDropdown onWorkerSwitch={vi.fn()} />
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it("should call onWorkerSwitch when a different worker is selected", async () => {
|
||||
mockUseWorker.mockReturnValue({
|
||||
isControlPlane: true,
|
||||
selectedWorker: workers[0],
|
||||
workers,
|
||||
});
|
||||
const onWorkerSwitch = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderWithProviders(<WorkerDropdown onWorkerSwitch={onWorkerSwitch} />);
|
||||
|
||||
// Open the dropdown
|
||||
await user.click(screen.getByRole("combobox"));
|
||||
// Select Worker 2
|
||||
await user.click(await screen.findByText("Worker 2"));
|
||||
|
||||
expect(onWorkerSwitch).toHaveBeenCalledWith("w2");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
import { renderWithProviders, screen } from "../../../tests/test-utils";
|
||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
import AccessGroupSelector from "./AccessGroupSelector";
|
||||
|
||||
const mockAccessGroups = [
|
||||
{
|
||||
access_group_id: "ag-1",
|
||||
access_group_name: "Engineering",
|
||||
description: null,
|
||||
access_model_names: [],
|
||||
access_mcp_server_ids: [],
|
||||
access_agent_ids: [],
|
||||
assigned_team_ids: [],
|
||||
assigned_key_ids: [],
|
||||
created_at: "2024-01-01",
|
||||
created_by: null,
|
||||
updated_at: "2024-01-01",
|
||||
updated_by: null,
|
||||
},
|
||||
{
|
||||
access_group_id: "ag-2",
|
||||
access_group_name: "Product",
|
||||
description: null,
|
||||
access_model_names: [],
|
||||
access_mcp_server_ids: [],
|
||||
access_agent_ids: [],
|
||||
assigned_team_ids: [],
|
||||
assigned_key_ids: [],
|
||||
created_at: "2024-01-01",
|
||||
created_by: null,
|
||||
updated_at: "2024-01-01",
|
||||
updated_by: null,
|
||||
},
|
||||
];
|
||||
|
||||
const mockUseAccessGroups = vi.fn();
|
||||
|
||||
vi.mock("@/app/(dashboard)/hooks/accessGroups/useAccessGroups", () => ({
|
||||
useAccessGroups: () => mockUseAccessGroups(),
|
||||
// Re-export the type for imports to work
|
||||
get AccessGroupResponse() {
|
||||
return {};
|
||||
},
|
||||
}));
|
||||
|
||||
describe("AccessGroupSelector", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
mockUseAccessGroups.mockReturnValue({
|
||||
data: mockAccessGroups,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
renderWithProviders(<AccessGroupSelector />);
|
||||
expect(screen.getByRole("combobox")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show a loading skeleton when data is loading", () => {
|
||||
mockUseAccessGroups.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
renderWithProviders(<AccessGroupSelector />);
|
||||
// Ant Design Skeleton renders with class ant-skeleton
|
||||
expect(screen.queryByRole("combobox")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the label text when showLabel is true", () => {
|
||||
mockUseAccessGroups.mockReturnValue({
|
||||
data: mockAccessGroups,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<AccessGroupSelector showLabel labelText="My Groups" />
|
||||
);
|
||||
expect(screen.getByText("My Groups")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not show the label by default", () => {
|
||||
mockUseAccessGroups.mockReturnValue({
|
||||
data: mockAccessGroups,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
|
||||
renderWithProviders(<AccessGroupSelector />);
|
||||
expect(screen.queryByText("Access Group")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show error content when data fails to load", async () => {
|
||||
mockUseAccessGroups.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: false,
|
||||
isError: true,
|
||||
});
|
||||
|
||||
const user = (await import("@testing-library/user-event")).default.setup();
|
||||
renderWithProviders(<AccessGroupSelector />);
|
||||
|
||||
// Open the dropdown to see notFoundContent
|
||||
await user.click(screen.getByRole("combobox"));
|
||||
expect(
|
||||
await screen.findByText("Failed to load access groups")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
import { renderWithProviders, screen } from "../../../tests/test-utils";
|
||||
import { vi, describe, it, expect } from "vitest";
|
||||
import AutoRotationView from "./AutoRotationView";
|
||||
|
||||
// Mock heroicons since jsdom doesn't support SVG rendering
|
||||
vi.mock("@heroicons/react/outline", () => ({
|
||||
RefreshIcon: (props: Record<string, unknown>) => <svg data-testid="refresh-icon" {...props} />,
|
||||
ClockIcon: (props: Record<string, unknown>) => <svg data-testid="clock-icon" {...props} />,
|
||||
}));
|
||||
|
||||
describe("AutoRotationView", () => {
|
||||
it("should render", () => {
|
||||
renderWithProviders(<AutoRotationView />);
|
||||
expect(screen.getAllByText("Auto-Rotation").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should show Disabled badge when autoRotate is false", () => {
|
||||
renderWithProviders(<AutoRotationView autoRotate={false} />);
|
||||
expect(screen.getByText("Disabled")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show Enabled badge when autoRotate is true", () => {
|
||||
renderWithProviders(<AutoRotationView autoRotate={true} />);
|
||||
expect(screen.getByText("Enabled")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the rotation interval when autoRotate is true", () => {
|
||||
renderWithProviders(
|
||||
<AutoRotationView autoRotate={true} rotationInterval="7d" />
|
||||
);
|
||||
expect(screen.getByText("Every 7d")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show 'No rotation history available' when autoRotate is enabled but no timestamps", () => {
|
||||
renderWithProviders(<AutoRotationView autoRotate={true} />);
|
||||
expect(
|
||||
screen.getByText("No rotation history available")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show disabled message when autoRotate is off and no rotation data", () => {
|
||||
renderWithProviders(<AutoRotationView autoRotate={false} />);
|
||||
expect(
|
||||
screen.getByText("Auto-rotation is not enabled for this key")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show last rotation timestamp when provided", () => {
|
||||
renderWithProviders(
|
||||
<AutoRotationView
|
||||
autoRotate={true}
|
||||
lastRotationAt="2024-06-15T10:30:00Z"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("Last Rotation")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show next scheduled rotation when nextRotationAt is provided", () => {
|
||||
renderWithProviders(
|
||||
<AutoRotationView
|
||||
autoRotate={true}
|
||||
nextRotationAt="2025-01-01T00:00:00Z"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("Next Scheduled Rotation")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show next scheduled rotation when keyRotationAt is provided", () => {
|
||||
renderWithProviders(
|
||||
<AutoRotationView
|
||||
autoRotate={true}
|
||||
keyRotationAt="2025-01-01T00:00:00Z"
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("Next Scheduled Rotation")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe("when variant is 'inline'", () => {
|
||||
it("should render without the card wrapper", () => {
|
||||
renderWithProviders(
|
||||
<AutoRotationView variant="inline" autoRotate={false} />
|
||||
);
|
||||
expect(screen.getByText("Disabled")).toBeInTheDocument();
|
||||
// The card variant has a subtitle; inline does not
|
||||
expect(
|
||||
screen.queryByText(
|
||||
"Automatic key rotation settings and status for this key"
|
||||
)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when variant is 'card'", () => {
|
||||
it("should render with the card subtitle", () => {
|
||||
renderWithProviders(
|
||||
<AutoRotationView variant="card" autoRotate={false} />
|
||||
);
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Automatic key rotation settings and status for this key"
|
||||
)
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
import { renderWithProviders, screen, within } from "../../../tests/test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
import MemberTable from "./MemberTable";
|
||||
import type { Member } from "@/components/networking";
|
||||
|
||||
vi.mock(
|
||||
"@/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton",
|
||||
() => ({
|
||||
default: ({
|
||||
onClick,
|
||||
dataTestId,
|
||||
variant,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
dataTestId?: string;
|
||||
variant: string;
|
||||
}) => (
|
||||
<button data-testid={dataTestId} onClick={onClick}>
|
||||
{variant}
|
||||
</button>
|
||||
),
|
||||
})
|
||||
);
|
||||
|
||||
const members: Member[] = [
|
||||
{ user_id: "user-1", user_email: "alice@example.com", role: "admin" },
|
||||
{ user_id: "user-2", user_email: "bob@example.com", role: "user" },
|
||||
{
|
||||
user_id: "default_user_id",
|
||||
user_email: "proxy@admin.com",
|
||||
role: "admin",
|
||||
},
|
||||
];
|
||||
|
||||
describe("MemberTable", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
renderWithProviders(
|
||||
<MemberTable
|
||||
members={members}
|
||||
canEdit={false}
|
||||
onEdit={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("3 Members")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display member emails in the table", () => {
|
||||
renderWithProviders(
|
||||
<MemberTable
|
||||
members={members}
|
||||
canEdit={false}
|
||||
onEdit={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("alice@example.com")).toBeInTheDocument();
|
||||
expect(screen.getByText("bob@example.com")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show Default Proxy Admin tag for default_user_id", () => {
|
||||
renderWithProviders(
|
||||
<MemberTable
|
||||
members={members}
|
||||
canEdit={false}
|
||||
onEdit={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("Default Proxy Admin")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show singular 'Member' label when there is only one member", () => {
|
||||
renderWithProviders(
|
||||
<MemberTable
|
||||
members={[members[0]]}
|
||||
canEdit={false}
|
||||
onEdit={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText("1 Member")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onEdit when the edit button is clicked", async () => {
|
||||
const onEdit = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderWithProviders(
|
||||
<MemberTable
|
||||
members={[members[0]]}
|
||||
canEdit={true}
|
||||
onEdit={onEdit}
|
||||
onDelete={vi.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId("edit-member"));
|
||||
expect(onEdit).toHaveBeenCalledWith(members[0]);
|
||||
});
|
||||
|
||||
it("should call onDelete when the delete button is clicked", async () => {
|
||||
const onDelete = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderWithProviders(
|
||||
<MemberTable
|
||||
members={[members[0]]}
|
||||
canEdit={true}
|
||||
onEdit={vi.fn()}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByTestId("delete-member"));
|
||||
expect(onDelete).toHaveBeenCalledWith(members[0]);
|
||||
});
|
||||
|
||||
it("should not show action buttons when canEdit is false", () => {
|
||||
renderWithProviders(
|
||||
<MemberTable
|
||||
members={[members[0]]}
|
||||
canEdit={false}
|
||||
onEdit={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByTestId("edit-member")).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId("delete-member")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the Add Member button when onAddMember is provided and canEdit is true", () => {
|
||||
renderWithProviders(
|
||||
<MemberTable
|
||||
members={members}
|
||||
canEdit={true}
|
||||
onEdit={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
onAddMember={vi.fn()}
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
screen.getByRole("button", { name: /add member/i })
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onAddMember when the Add Member button is clicked", async () => {
|
||||
const onAddMember = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderWithProviders(
|
||||
<MemberTable
|
||||
members={members}
|
||||
canEdit={true}
|
||||
onEdit={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
onAddMember={onAddMember}
|
||||
/>
|
||||
);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /add member/i }));
|
||||
expect(onAddMember).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should hide delete for a member when showDeleteForMember returns false", () => {
|
||||
renderWithProviders(
|
||||
<MemberTable
|
||||
members={[members[0]]}
|
||||
canEdit={true}
|
||||
onEdit={vi.fn()}
|
||||
onDelete={vi.fn()}
|
||||
showDeleteForMember={() => false}
|
||||
/>
|
||||
);
|
||||
expect(screen.queryByTestId("delete-member")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("edit-member")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import { renderWithProviders, screen, waitFor } from "../../../tests/test-utils";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
import ModelSelector from "./ModelSelector";
|
||||
import { fetchAvailableModels } from "@/components/playground/llm_calls/fetch_models";
|
||||
|
||||
const mockModels = [
|
||||
{ model_group: "gpt-4" },
|
||||
{ model_group: "claude-3-opus" },
|
||||
{ model_group: "gemini-pro" },
|
||||
];
|
||||
|
||||
vi.mock("@/components/playground/llm_calls/fetch_models", () => ({
|
||||
fetchAvailableModels: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("ModelSelector", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(fetchAvailableModels).mockResolvedValue(mockModels);
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
renderWithProviders(<ModelSelector accessToken="test-token" />);
|
||||
expect(screen.getByRole("combobox")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the label by default", () => {
|
||||
renderWithProviders(<ModelSelector accessToken="test-token" />);
|
||||
expect(screen.getByText("Select Model")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should hide the label when showLabel is false", () => {
|
||||
renderWithProviders(
|
||||
<ModelSelector accessToken="test-token" showLabel={false} />
|
||||
);
|
||||
expect(screen.queryByText("Select Model")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show custom label text", () => {
|
||||
renderWithProviders(
|
||||
<ModelSelector accessToken="test-token" labelText="Pick a Model" />
|
||||
);
|
||||
expect(screen.getByText("Pick a Model")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the custom model input when 'Enter custom model' is selected", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderWithProviders(<ModelSelector accessToken="test-token" />);
|
||||
|
||||
// Wait for models to load
|
||||
await waitFor(() => {
|
||||
expect(fetchAvailableModels).toHaveBeenCalledWith("test-token");
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole("combobox"));
|
||||
await user.click(await screen.findByText("Enter custom model"));
|
||||
|
||||
expect(
|
||||
screen.getByPlaceholderText("Enter custom model name")
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should fetch models when accessToken is provided", async () => {
|
||||
renderWithProviders(<ModelSelector accessToken="test-token" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchAvailableModels).toHaveBeenCalledWith("test-token");
|
||||
});
|
||||
});
|
||||
|
||||
it("should not fetch models when accessToken is empty", () => {
|
||||
renderWithProviders(<ModelSelector accessToken="" />);
|
||||
|
||||
expect(fetchAvailableModels).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue