mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-09 22:31:41 +00:00
Merge pull request #23773 from BerriAI/litellm_/reverent-panini
[Test] UI Dashboard - Add unit tests for 5 untested files
This commit is contained in:
commit
eba8df5235
5 changed files with 231 additions and 0 deletions
|
|
@ -0,0 +1,29 @@
|
|||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import HashicorpVaultEmptyPlaceholder from "./HashicorpVaultEmptyPlaceholder";
|
||||
|
||||
describe("HashicorpVaultEmptyPlaceholder", () => {
|
||||
it("should render the empty state message and configure button", () => {
|
||||
render(<HashicorpVaultEmptyPlaceholder onAdd={vi.fn()} />);
|
||||
expect(screen.getByText("No Vault Configuration Found")).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /configure vault/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onAdd when the configure button is clicked", async () => {
|
||||
const onAdd = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
render(<HashicorpVaultEmptyPlaceholder onAdd={onAdd} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: /configure vault/i }));
|
||||
|
||||
expect(onAdd).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should display the description text about Vault purpose", () => {
|
||||
render(<HashicorpVaultEmptyPlaceholder onAdd={vi.fn()} />);
|
||||
expect(
|
||||
screen.getByText(/Configure Hashicorp Vault to securely manage provider API keys/),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import PageVisibilitySettings from "./PageVisibilitySettings";
|
||||
|
||||
vi.mock("@/components/page_utils", () => ({
|
||||
getAvailablePages: () => [
|
||||
{ page: "usage", label: "Usage", description: "View usage stats", group: "Analytics" },
|
||||
{ page: "models", label: "Models", description: "Manage models", group: "Analytics" },
|
||||
{ page: "keys", label: "API Keys", description: "Manage API keys", group: "Access" },
|
||||
],
|
||||
}));
|
||||
|
||||
describe("PageVisibilitySettings", () => {
|
||||
it("should render the not-set tag when enabledPagesInternalUsers is null", () => {
|
||||
render(
|
||||
<PageVisibilitySettings
|
||||
enabledPagesInternalUsers={null}
|
||||
isUpdating={false}
|
||||
onUpdate={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("Not set (all pages visible)")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the selected page count tag when pages are configured", () => {
|
||||
render(
|
||||
<PageVisibilitySettings
|
||||
enabledPagesInternalUsers={["usage", "keys"]}
|
||||
isUpdating={false}
|
||||
onUpdate={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("2 pages selected")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show singular 'page' when exactly one page is selected", () => {
|
||||
render(
|
||||
<PageVisibilitySettings
|
||||
enabledPagesInternalUsers={["usage"]}
|
||||
isUpdating={false}
|
||||
onUpdate={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("1 page selected")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call onUpdate with null when reset button is clicked", async () => {
|
||||
const onUpdate = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<PageVisibilitySettings
|
||||
enabledPagesInternalUsers={["usage"]}
|
||||
isUpdating={false}
|
||||
onUpdate={onUpdate}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Expand the collapse panel first to reveal the reset button
|
||||
await user.click(screen.getByRole("button", { name: /configure page visibility/i }));
|
||||
await user.click(await screen.findByRole("button", { name: /reset to default/i }));
|
||||
|
||||
expect(onUpdate).toHaveBeenCalledWith({ enabled_ui_pages_internal_users: null });
|
||||
});
|
||||
|
||||
it("should display the property description when provided", () => {
|
||||
render(
|
||||
<PageVisibilitySettings
|
||||
enabledPagesInternalUsers={null}
|
||||
enabledPagesPropertyDescription="Controls which pages are visible"
|
||||
isUpdating={false}
|
||||
onUpdate={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText("Controls which pages are visible")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { UiLoadingSpinner } from "./ui-loading-spinner";
|
||||
|
||||
describe("UiLoadingSpinner", () => {
|
||||
it("should render an SVG element", () => {
|
||||
render(<UiLoadingSpinner data-testid="spinner" />);
|
||||
expect(screen.getByTestId("spinner")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should apply custom className alongside default classes", () => {
|
||||
render(<UiLoadingSpinner data-testid="spinner" className="text-red-500" />);
|
||||
const svg = screen.getByTestId("spinner");
|
||||
expect(svg).toHaveClass("text-red-500");
|
||||
expect(svg).toHaveClass("animate-spin");
|
||||
});
|
||||
|
||||
it("should spread additional SVG props onto the element", () => {
|
||||
render(<UiLoadingSpinner data-testid="spinner" aria-label="Loading" />);
|
||||
expect(screen.getByLabelText("Loading")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
40
ui/litellm-dashboard/src/utils/errorUtils.test.ts
Normal file
40
ui/litellm-dashboard/src/utils/errorUtils.test.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { extractErrorMessage } from "./errorUtils";
|
||||
|
||||
describe("extractErrorMessage", () => {
|
||||
it("should return the message from an Error instance", () => {
|
||||
expect(extractErrorMessage(new Error("Something broke"))).toBe("Something broke");
|
||||
});
|
||||
|
||||
it("should return detail when it is a string", () => {
|
||||
expect(extractErrorMessage({ detail: "Not found" })).toBe("Not found");
|
||||
});
|
||||
|
||||
it("should join msg fields from a FastAPI 422 detail array", () => {
|
||||
const err = {
|
||||
detail: [
|
||||
{ msg: "field required", loc: ["body", "name"], type: "value_error" },
|
||||
{ msg: "invalid type", loc: ["body", "age"], type: "type_error" },
|
||||
],
|
||||
};
|
||||
expect(extractErrorMessage(err)).toBe("field required; invalid type");
|
||||
});
|
||||
|
||||
it("should extract error from nested detail object", () => {
|
||||
expect(extractErrorMessage({ detail: { error: "bad request" } })).toBe("bad request");
|
||||
});
|
||||
|
||||
it("should fall back to message property on plain objects", () => {
|
||||
expect(extractErrorMessage({ message: "fallback msg" })).toBe("fallback msg");
|
||||
});
|
||||
|
||||
it("should JSON.stringify unknown object shapes", () => {
|
||||
expect(extractErrorMessage({ foo: "bar" })).toBe('{"foo":"bar"}');
|
||||
});
|
||||
|
||||
it("should stringify primitive non-object values", () => {
|
||||
expect(extractErrorMessage(42)).toBe("42");
|
||||
expect(extractErrorMessage(null)).toBe("null");
|
||||
expect(extractErrorMessage(undefined)).toBe("undefined");
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { classifyToolOp, groupToolsByCrud } from "./mcpToolCrudClassification";
|
||||
|
||||
describe("classifyToolOp", () => {
|
||||
it("should classify read operations by name", () => {
|
||||
expect(classifyToolOp("get-users")).toBe("read");
|
||||
expect(classifyToolOp("list-items")).toBe("read");
|
||||
expect(classifyToolOp("search documents")).toBe("read");
|
||||
});
|
||||
|
||||
it("should classify delete operations by name", () => {
|
||||
expect(classifyToolOp("delete-user")).toBe("delete");
|
||||
expect(classifyToolOp("remove-item")).toBe("delete");
|
||||
expect(classifyToolOp("purge-cache")).toBe("delete");
|
||||
});
|
||||
|
||||
it("should classify create operations by name", () => {
|
||||
expect(classifyToolOp("create-user")).toBe("create");
|
||||
expect(classifyToolOp("add-item")).toBe("create");
|
||||
expect(classifyToolOp("upload-file")).toBe("create");
|
||||
});
|
||||
|
||||
it("should classify update operations by name", () => {
|
||||
expect(classifyToolOp("update-settings")).toBe("update");
|
||||
expect(classifyToolOp("edit-profile")).toBe("update");
|
||||
expect(classifyToolOp("rename-file")).toBe("update");
|
||||
});
|
||||
|
||||
it("should prioritize read over delete for names like get-removed-entries", () => {
|
||||
expect(classifyToolOp("get-removed-entries")).toBe("read");
|
||||
expect(classifyToolOp("list-deleted-items")).toBe("read");
|
||||
});
|
||||
|
||||
it("should fall back to description when name is unrecognised", () => {
|
||||
expect(classifyToolOp("mytool", "This will delete the record")).toBe("delete");
|
||||
expect(classifyToolOp("mytool", "fetch data from the API")).toBe("read");
|
||||
});
|
||||
|
||||
it("should return unknown when neither name nor description match", () => {
|
||||
expect(classifyToolOp("my_tool")).toBe("unknown");
|
||||
expect(classifyToolOp("my_tool", "does something")).toBe("unknown");
|
||||
});
|
||||
});
|
||||
|
||||
describe("groupToolsByCrud", () => {
|
||||
it("should group tools into their CRUD categories", () => {
|
||||
const tools = [
|
||||
{ name: "get-user", description: "" },
|
||||
{ name: "create-item", description: "" },
|
||||
{ name: "delete-record", description: "" },
|
||||
{ name: "update-settings", description: "" },
|
||||
{ name: "mysteryop", description: "" },
|
||||
];
|
||||
|
||||
const groups = groupToolsByCrud(tools);
|
||||
|
||||
expect(groups.read).toHaveLength(1);
|
||||
expect(groups.create).toHaveLength(1);
|
||||
expect(groups.delete).toHaveLength(1);
|
||||
expect(groups.update).toHaveLength(1);
|
||||
expect(groups.unknown).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue