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:
Yuneng Jiang 2026-07-23 22:25:05 -07:00
parent 64aad5877a
commit 212421207e
No known key found for this signature in database
2 changed files with 206 additions and 4 deletions

View file

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

View file

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