diff --git a/ui/litellm-dashboard/src/app/(dashboard)/skills/_components/ClaudeCodePluginsPanel.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/skills/_components/ClaudeCodePluginsPanel.test.tsx
index 52f3dc21b7a..67bab398bd2 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/skills/_components/ClaudeCodePluginsPanel.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/skills/_components/ClaudeCodePluginsPanel.test.tsx
@@ -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 }) => (
-
{isLoading ? "table-loading" : "table-loaded"}
+ default: ({
+ isLoading,
+ pluginsList,
+ onDeleteClick,
+ }: {
+ isLoading: boolean;
+ pluginsList: Plugin[];
+ onDeleteClick: (pluginName: string, displayName: string) => void;
+ }) => (
+
+ {isLoading ? "table-loading" : "table-loaded"}
+ {pluginsList.map((plugin) => (
+
+ ))}
+
),
}));
@@ -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();
+
+ 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();
+
+ 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();
+
+ 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();
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/ui-theme/UIThemeSettings.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/ui-theme/UIThemeSettings.test.tsx
new file mode 100644
index 00000000000..20f8960ae3a
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/ui-theme/UIThemeSettings.test.tsx
@@ -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 = {}) =>
+ Promise.resolve({ ok: true, json: () => Promise.resolve({ values }) } as Response);
+
+const fetchMock = vi.fn();
+
+const patchCalls = () => fetchMock.mock.calls.filter(([, init]) => init?.method === "PATCH");
+
+const bodyOf = (call: Parameters) => 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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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();
+
+ 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!"));
+ });
+});