[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:
yuneng-jiang 2026-03-23 21:33:12 -07:00
parent 3292d02aa4
commit 5d9136bb71
5 changed files with 558 additions and 0 deletions

View file

@ -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");
});
});

View file

@ -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();
});
});

View file

@ -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();
});
});
});

View file

@ -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();
});
});

View file

@ -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();
});
});