mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
test(ui): characterise budgets, skills and ui-theme panels before migration
Adds a role/text-based characterisation test for UIThemeSettings, which had none, and extends the skills panel test to cover the delete confirmation. Both are green against the current antd/Tremor components so they can prove the shadcn migration keeps behaviour identical without being edited.
This commit is contained in:
parent
64aad5877a
commit
212421207e
2 changed files with 206 additions and 4 deletions
|
|
@ -1,7 +1,9 @@
|
|||
import { render, screen } from "@testing-library/react";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { getClaudeCodePluginsList } from "@/components/networking";
|
||||
import { getClaudeCodePluginsList, deleteClaudeCodePlugin } from "@/components/networking";
|
||||
import type { Plugin } from "@/components/claude_code_plugins/types";
|
||||
|
||||
import ClaudeCodePluginsPanel from "./ClaudeCodePluginsPanel";
|
||||
|
||||
|
|
@ -12,8 +14,27 @@ vi.mock("@/components/networking", () => ({
|
|||
|
||||
vi.mock("./PluginTable", () => ({
|
||||
__esModule: true,
|
||||
default: ({ isLoading }: { isLoading: boolean }) => (
|
||||
<div data-testid="plugin-table">{isLoading ? "table-loading" : "table-loaded"}</div>
|
||||
default: ({
|
||||
isLoading,
|
||||
pluginsList,
|
||||
onDeleteClick,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
pluginsList: Plugin[];
|
||||
onDeleteClick: (pluginName: string, displayName: string) => void;
|
||||
}) => (
|
||||
<div data-testid="plugin-table">
|
||||
{isLoading ? "table-loading" : "table-loaded"}
|
||||
{pluginsList.map((plugin) => (
|
||||
<button
|
||||
key={plugin.id}
|
||||
data-testid={`row-delete-${plugin.id}`}
|
||||
onClick={() => onDeleteClick(plugin.name, plugin.name)}
|
||||
>
|
||||
row delete
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
|
|
@ -21,6 +42,14 @@ vi.mock("./add_plugin_form", () => ({ __esModule: true, default: () => null }));
|
|||
vi.mock("@/components/claude_code_plugins/skill_detail", () => ({ __esModule: true, default: () => null }));
|
||||
|
||||
const mockGetClaudeCodePluginsList = vi.mocked(getClaudeCodePluginsList);
|
||||
const mockDeleteClaudeCodePlugin = vi.mocked(deleteClaudeCodePlugin);
|
||||
|
||||
const skill: Plugin = {
|
||||
id: "plugin-1",
|
||||
name: "my-skill",
|
||||
source: { source: "github", repo: "acme/my-skill" },
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
describe("ClaudeCodePluginsPanel loading state", () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -48,3 +77,48 @@ describe("ClaudeCodePluginsPanel loading state", () => {
|
|||
expect(mockGetClaudeCodePluginsList).toHaveBeenCalledWith("sk-test", false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ClaudeCodePluginsPanel delete confirmation", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockGetClaudeCodePluginsList.mockResolvedValue({ plugins: [skill], count: 1 });
|
||||
});
|
||||
|
||||
it("should ask for confirmation before deleting and name the skill", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ClaudeCodePluginsPanel accessToken="sk-test" userRole="Admin" />);
|
||||
|
||||
await user.click(await screen.findByTestId("row-delete-plugin-1"));
|
||||
|
||||
expect(await screen.findByText(/are you sure you want to delete skill/i)).toBeInTheDocument();
|
||||
expect(screen.getByText("my-skill")).toBeInTheDocument();
|
||||
expect(screen.getByText("This action cannot be undone.")).toBeInTheDocument();
|
||||
expect(mockDeleteClaudeCodePlugin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should delete the skill and refresh the list once confirmed", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockDeleteClaudeCodePlugin.mockResolvedValue({});
|
||||
render(<ClaudeCodePluginsPanel accessToken="sk-test" userRole="Admin" />);
|
||||
|
||||
await user.click(await screen.findByTestId("row-delete-plugin-1"));
|
||||
await screen.findByText(/are you sure you want to delete skill/i);
|
||||
await user.click(screen.getByRole("button", { name: "Delete" }));
|
||||
|
||||
await waitFor(() => expect(mockDeleteClaudeCodePlugin).toHaveBeenCalledWith("sk-test", "my-skill"));
|
||||
await waitFor(() => expect(mockGetClaudeCodePluginsList).toHaveBeenCalledTimes(2));
|
||||
await waitFor(() => expect(screen.queryByText(/are you sure you want to delete skill/i)).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it("should not delete the skill when the confirmation is cancelled", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ClaudeCodePluginsPanel accessToken="sk-test" userRole="Admin" />);
|
||||
|
||||
await user.click(await screen.findByTestId("row-delete-plugin-1"));
|
||||
await screen.findByText(/are you sure you want to delete skill/i);
|
||||
await user.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
|
||||
await waitFor(() => expect(screen.queryByText(/are you sure you want to delete skill/i)).not.toBeInTheDocument());
|
||||
expect(mockDeleteClaudeCodePlugin).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import NotificationsManager from "@/components/molecules/notifications_manager";
|
||||
|
||||
import UIThemeSettings from "./UIThemeSettings";
|
||||
|
||||
const setLogoUrl = vi.fn();
|
||||
const setFaviconUrl = vi.fn();
|
||||
|
||||
vi.mock("@/contexts/ThemeContext", () => ({
|
||||
useTheme: () => ({ logoUrl: null, setLogoUrl, faviconUrl: null, setFaviconUrl }),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/networking", () => ({
|
||||
getProxyBaseUrl: () => "",
|
||||
getGlobalLitellmHeaderName: () => "Authorization",
|
||||
}));
|
||||
|
||||
vi.mock("@/components/molecules/notifications_manager", () => ({
|
||||
__esModule: true,
|
||||
default: { success: vi.fn(), fromBackend: vi.fn() },
|
||||
}));
|
||||
|
||||
const LOGO_PLACEHOLDER = "https://example.com/logo.png";
|
||||
const FAVICON_PLACEHOLDER = "https://example.com/favicon.ico";
|
||||
|
||||
const okResponse = (values: Record<string, string | null> = {}) =>
|
||||
Promise.resolve({ ok: true, json: () => Promise.resolve({ values }) } as Response);
|
||||
|
||||
const fetchMock = vi.fn<typeof fetch>();
|
||||
|
||||
const patchCalls = () => fetchMock.mock.calls.filter(([, init]) => init?.method === "PATCH");
|
||||
|
||||
const bodyOf = (call: Parameters<typeof fetch>) => JSON.parse(String(call[1]?.body));
|
||||
|
||||
describe("UIThemeSettings", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
fetchMock.mockImplementation(() => okResponse());
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("should render nothing without an access token", () => {
|
||||
const { container } = render(<UIThemeSettings userID="user-1" userRole="Admin" accessToken={null} />);
|
||||
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should load the saved logo and favicon urls into the inputs", async () => {
|
||||
fetchMock.mockImplementation(() =>
|
||||
okResponse({ logo_url: "https://cdn.example.com/logo.svg", favicon_url: "https://cdn.example.com/fav.ico" }),
|
||||
);
|
||||
|
||||
render(<UIThemeSettings userID="user-1" userRole="Admin" accessToken="sk-test" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText(LOGO_PLACEHOLDER)).toHaveValue("https://cdn.example.com/logo.svg");
|
||||
});
|
||||
expect(screen.getByPlaceholderText(FAVICON_PLACEHOLDER)).toHaveValue("https://cdn.example.com/fav.ico");
|
||||
expect(setLogoUrl).toHaveBeenCalledWith("https://cdn.example.com/logo.svg");
|
||||
expect(setFaviconUrl).toHaveBeenCalledWith("https://cdn.example.com/fav.ico");
|
||||
});
|
||||
|
||||
it("should save the entered urls and report success", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<UIThemeSettings userID="user-1" userRole="Admin" accessToken="sk-test" />);
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalled());
|
||||
|
||||
await user.type(screen.getByPlaceholderText(LOGO_PLACEHOLDER), "https://a.test/logo.png");
|
||||
await user.type(screen.getByPlaceholderText(FAVICON_PLACEHOLDER), "https://a.test/fav.ico");
|
||||
await user.click(screen.getByRole("button", { name: "Save Changes" }));
|
||||
|
||||
await waitFor(() => expect(patchCalls()).toHaveLength(1));
|
||||
expect(bodyOf(patchCalls()[0])).toEqual({
|
||||
logo_url: "https://a.test/logo.png",
|
||||
favicon_url: "https://a.test/fav.ico",
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(NotificationsManager.success).toHaveBeenCalledWith("Theme settings updated successfully!"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should surface a backend failure when saving fails", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<UIThemeSettings userID="user-1" userRole="Admin" accessToken="sk-test" />);
|
||||
|
||||
await waitFor(() => expect(fetchMock).toHaveBeenCalled());
|
||||
fetchMock.mockImplementation(() => Promise.resolve({ ok: false } as Response));
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Save Changes" }));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(NotificationsManager.fromBackend).toHaveBeenCalledWith("Failed to update theme settings"),
|
||||
);
|
||||
expect(NotificationsManager.success).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should clear both inputs and persist nulls when resetting to default", async () => {
|
||||
const user = userEvent.setup();
|
||||
fetchMock.mockImplementation(() =>
|
||||
okResponse({ logo_url: "https://cdn.example.com/logo.svg", favicon_url: "https://cdn.example.com/fav.ico" }),
|
||||
);
|
||||
|
||||
render(<UIThemeSettings userID="user-1" userRole="Admin" accessToken="sk-test" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByPlaceholderText(LOGO_PLACEHOLDER)).toHaveValue("https://cdn.example.com/logo.svg");
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Reset to Default" }));
|
||||
|
||||
await waitFor(() => expect(patchCalls()).toHaveLength(1));
|
||||
expect(bodyOf(patchCalls()[0])).toEqual({ logo_url: null, favicon_url: null });
|
||||
expect(screen.getByPlaceholderText(LOGO_PLACEHOLDER)).toHaveValue("");
|
||||
expect(screen.getByPlaceholderText(FAVICON_PLACEHOLDER)).toHaveValue("");
|
||||
expect(setLogoUrl).toHaveBeenLastCalledWith(null);
|
||||
expect(setFaviconUrl).toHaveBeenLastCalledWith(null);
|
||||
await waitFor(() => expect(NotificationsManager.success).toHaveBeenCalledWith("Theme settings reset to default!"));
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue