mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-05 08:23:17 +00:00
test(ui): add unit tests for 5 untested frontend components
- AntDLoadingSpinner: rendering, prop forwarding, icon styling - MessageManager: static fallback, custom instance delegation - claude_code_plugins/helpers: all pure utility functions (15 describe blocks, 55 tests) - AgentSelector: fetch behavior, loading states, error handling, disabled state - WorkerDropdown: conditional rendering, worker options, selection changes
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
|
||||
// Mock the useWorker hook
|
||||
const mockUseWorker = vi.fn();
|
||||
vi.mock("@/hooks/useWorker", () => ({
|
||||
useWorker: () => mockUseWorker(),
|
||||
}));
|
||||
|
||||
// Mock antd Select
|
||||
vi.mock("antd", () => ({
|
||||
Select: ({ value, options, onChange, style, disabled, ...props }: any) => (
|
||||
<select
|
||||
data-testid="worker-select"
|
||||
value={value}
|
||||
style={style}
|
||||
onChange={(e) => onChange?.(e.target.value)}
|
||||
>
|
||||
{options?.map((opt: any) => (
|
||||
<option key={opt.value} value={opt.value} disabled={opt.disabled}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock icon
|
||||
vi.mock("@ant-design/icons", () => ({
|
||||
CloudServerOutlined: () => <span data-testid="cloud-icon" />,
|
||||
}));
|
||||
|
||||
import WorkerDropdown from "./WorkerDropdown";
|
||||
|
||||
describe("WorkerDropdown", () => {
|
||||
const mockOnWorkerSwitch = vi.fn();
|
||||
const workers = [
|
||||
{ worker_id: "w1", name: "Worker 1" },
|
||||
{ worker_id: "w2", name: "Worker 2" },
|
||||
{ worker_id: "w3", name: "Worker 3" },
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("renders null when isControlPlane is false", () => {
|
||||
mockUseWorker.mockReturnValue({
|
||||
isControlPlane: false,
|
||||
selectedWorker: workers[0],
|
||||
workers,
|
||||
});
|
||||
|
||||
const { container } = render(<WorkerDropdown onWorkerSwitch={mockOnWorkerSwitch} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it("renders null when selectedWorker is null", () => {
|
||||
mockUseWorker.mockReturnValue({
|
||||
isControlPlane: true,
|
||||
selectedWorker: null,
|
||||
workers,
|
||||
});
|
||||
|
||||
const { container } = render(<WorkerDropdown onWorkerSwitch={mockOnWorkerSwitch} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it("renders the select when isControlPlane and selectedWorker exist", () => {
|
||||
mockUseWorker.mockReturnValue({
|
||||
isControlPlane: true,
|
||||
selectedWorker: workers[0],
|
||||
workers,
|
||||
});
|
||||
|
||||
render(<WorkerDropdown onWorkerSwitch={mockOnWorkerSwitch} />);
|
||||
expect(screen.getByTestId("worker-select")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders all worker options", () => {
|
||||
mockUseWorker.mockReturnValue({
|
||||
isControlPlane: true,
|
||||
selectedWorker: workers[0],
|
||||
workers,
|
||||
});
|
||||
|
||||
render(<WorkerDropdown onWorkerSwitch={mockOnWorkerSwitch} />);
|
||||
expect(screen.getByText("Worker 1")).toBeInTheDocument();
|
||||
expect(screen.getByText("Worker 2")).toBeInTheDocument();
|
||||
expect(screen.getByText("Worker 3")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("sets current worker as selected value", () => {
|
||||
mockUseWorker.mockReturnValue({
|
||||
isControlPlane: true,
|
||||
selectedWorker: workers[1],
|
||||
workers,
|
||||
});
|
||||
|
||||
render(<WorkerDropdown onWorkerSwitch={mockOnWorkerSwitch} />);
|
||||
const select = screen.getByTestId("worker-select") as HTMLSelectElement;
|
||||
expect(select.value).toBe("w2");
|
||||
});
|
||||
|
||||
it("disables the currently selected worker in options", () => {
|
||||
mockUseWorker.mockReturnValue({
|
||||
isControlPlane: true,
|
||||
selectedWorker: workers[0],
|
||||
workers,
|
||||
});
|
||||
|
||||
render(<WorkerDropdown onWorkerSwitch={mockOnWorkerSwitch} />);
|
||||
const options = screen.getAllByRole("option");
|
||||
const selectedOption = options.find((opt) => (opt as HTMLOptionElement).value === "w1");
|
||||
expect(selectedOption).toBeDisabled();
|
||||
});
|
||||
|
||||
it("calls onWorkerSwitch when selection changes", async () => {
|
||||
mockUseWorker.mockReturnValue({
|
||||
isControlPlane: true,
|
||||
selectedWorker: workers[0],
|
||||
workers,
|
||||
});
|
||||
|
||||
render(<WorkerDropdown onWorkerSwitch={mockOnWorkerSwitch} />);
|
||||
const select = screen.getByTestId("worker-select");
|
||||
|
||||
const { default: userEvent } = await import("@testing-library/user-event");
|
||||
const user = userEvent.setup();
|
||||
await user.selectOptions(select, "w2");
|
||||
|
||||
expect(mockOnWorkerSwitch).toHaveBeenCalledWith("w2");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
|
||||
// Mock networking module
|
||||
const mockGetAgentsList = vi.fn();
|
||||
vi.mock("../networking", () => ({
|
||||
getAgentsList: (...args: any[]) => mockGetAgentsList(...args),
|
||||
}));
|
||||
|
||||
// Mock antd Select
|
||||
vi.mock("antd", () => {
|
||||
const SelectComponent = ({ children, onChange, value, mode, placeholder, loading, disabled, ...props }: any) => (
|
||||
<div data-testid="agent-select" data-loading={loading} data-disabled={disabled}>
|
||||
<select
|
||||
data-testid="select-input"
|
||||
multiple={mode === "multiple"}
|
||||
value={value || []}
|
||||
onChange={(e) => {
|
||||
const selected = Array.from(e.target.selectedOptions, (opt: any) => opt.value);
|
||||
onChange?.(selected);
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
</select>
|
||||
{loading && <span data-testid="loading-indicator">Loading</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
SelectComponent.Option = ({ children, value, ...props }: any) => (
|
||||
<option value={value} {...props}>
|
||||
{children}
|
||||
</option>
|
||||
);
|
||||
|
||||
return { Select: SelectComponent };
|
||||
});
|
||||
|
||||
import AgentSelector from "./AgentSelector";
|
||||
|
||||
describe("AgentSelector", () => {
|
||||
const defaultProps = {
|
||||
onChange: vi.fn(),
|
||||
accessToken: "test-token",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockGetAgentsList.mockResolvedValue({
|
||||
agents: [
|
||||
{ agent_id: "agent-1", agent_name: "Agent One" },
|
||||
{ agent_id: "agent-2", agent_name: "Agent Two", agent_access_groups: ["group-a", "group-b"] },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("renders the selector", () => {
|
||||
render(<AgentSelector {...defaultProps} />);
|
||||
expect(screen.getByTestId("agent-select")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("fetches agents on mount with access token", async () => {
|
||||
render(<AgentSelector {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(mockGetAgentsList).toHaveBeenCalledWith("test-token");
|
||||
});
|
||||
});
|
||||
|
||||
it("does not fetch when accessToken is empty", () => {
|
||||
render(<AgentSelector {...defaultProps} accessToken="" />);
|
||||
expect(mockGetAgentsList).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows loading state while fetching", async () => {
|
||||
// Keep the promise pending
|
||||
let resolve: any;
|
||||
mockGetAgentsList.mockReturnValue(new Promise((r) => { resolve = r; }));
|
||||
|
||||
render(<AgentSelector {...defaultProps} />);
|
||||
expect(screen.getByTestId("agent-select")).toHaveAttribute("data-loading", "true");
|
||||
|
||||
// Resolve to clean up
|
||||
resolve({ agents: [] });
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("agent-select")).toHaveAttribute("data-loading", "false");
|
||||
});
|
||||
});
|
||||
|
||||
it("renders agent options after fetch", async () => {
|
||||
render(<AgentSelector {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Agent One")).toBeInTheDocument();
|
||||
expect(screen.getByText("Agent Two")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("renders access group options with group prefix", async () => {
|
||||
render(<AgentSelector {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("group-a")).toBeInTheDocument();
|
||||
expect(screen.getByText("group-b")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("respects disabled prop", () => {
|
||||
render(<AgentSelector {...defaultProps} disabled />);
|
||||
expect(screen.getByTestId("agent-select")).toHaveAttribute("data-disabled", "true");
|
||||
});
|
||||
|
||||
it("handles API error gracefully", async () => {
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
mockGetAgentsList.mockRejectedValue(new Error("API error"));
|
||||
|
||||
render(<AgentSelector {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(consoleSpy).toHaveBeenCalledWith("Error fetching agents:", expect.any(Error));
|
||||
});
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("passes value as flattened selectedValues", async () => {
|
||||
render(
|
||||
<AgentSelector
|
||||
{...defaultProps}
|
||||
value={{ agents: ["agent-1"], accessGroups: ["group-a"] }}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => {
|
||||
const select = screen.getByTestId("select-input");
|
||||
// The value should contain agent-1 and group:group-a
|
||||
expect(select).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("handles null response from API", async () => {
|
||||
mockGetAgentsList.mockResolvedValue(null);
|
||||
render(<AgentSelector {...defaultProps} />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("agent-select")).toHaveAttribute("data-loading", "false");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,329 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
formatInstallCommand,
|
||||
extractCategories,
|
||||
validatePluginName,
|
||||
getSourceDisplayText,
|
||||
getSourceLink,
|
||||
getCategoryBadgeColor,
|
||||
formatDateString,
|
||||
truncateText,
|
||||
filterPluginsBySearch,
|
||||
filterPluginsByCategory,
|
||||
isValidSemanticVersion,
|
||||
isValidEmail,
|
||||
isValidUrl,
|
||||
parseKeywords,
|
||||
formatKeywords,
|
||||
} from "./helpers";
|
||||
import { MarketplacePluginEntry, PluginSource } from "./types";
|
||||
|
||||
describe("formatInstallCommand", () => {
|
||||
it("formats github source with repo", () => {
|
||||
const plugin = { name: "my-plugin", source: { source: "github" as const, repo: "org/repo" } };
|
||||
expect(formatInstallCommand(plugin)).toBe("/plugin marketplace add org/repo");
|
||||
});
|
||||
|
||||
it("formats url source", () => {
|
||||
const plugin = { name: "my-plugin", source: { source: "url" as const, url: "https://example.com/plugin" } };
|
||||
expect(formatInstallCommand(plugin)).toBe("/plugin marketplace add https://example.com/plugin");
|
||||
});
|
||||
|
||||
it("falls back to plugin name when no repo or url", () => {
|
||||
const plugin = { name: "my-plugin", source: { source: "github" as const } };
|
||||
expect(formatInstallCommand(plugin)).toBe("/plugin marketplace add my-plugin");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractCategories", () => {
|
||||
it("returns All and Other for empty list", () => {
|
||||
expect(extractCategories([])).toEqual(["All", "Other"]);
|
||||
});
|
||||
|
||||
it("extracts and sorts unique categories", () => {
|
||||
const plugins = [
|
||||
{ category: "Development" },
|
||||
{ category: "Analytics" },
|
||||
{ category: "Development" },
|
||||
];
|
||||
expect(extractCategories(plugins)).toEqual(["All", "Analytics", "Development", "Other"]);
|
||||
});
|
||||
|
||||
it("ignores empty/whitespace categories", () => {
|
||||
const plugins = [{ category: "" }, { category: " " }, { category: "Tools" }];
|
||||
expect(extractCategories(plugins)).toEqual(["All", "Tools", "Other"]);
|
||||
});
|
||||
|
||||
it("handles undefined category", () => {
|
||||
const plugins = [{ category: undefined }, { category: "Security" }];
|
||||
expect(extractCategories(plugins)).toEqual(["All", "Security", "Other"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validatePluginName", () => {
|
||||
it("accepts valid kebab-case names", () => {
|
||||
expect(validatePluginName("my-plugin")).toBe(true);
|
||||
expect(validatePluginName("plugin123")).toBe(true);
|
||||
expect(validatePluginName("a-b-c")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects names with uppercase", () => {
|
||||
expect(validatePluginName("MyPlugin")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects names with spaces", () => {
|
||||
expect(validatePluginName("my plugin")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects empty/whitespace names", () => {
|
||||
expect(validatePluginName("")).toBe(false);
|
||||
expect(validatePluginName(" ")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects names with special characters", () => {
|
||||
expect(validatePluginName("my_plugin")).toBe(false);
|
||||
expect(validatePluginName("my.plugin")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSourceDisplayText", () => {
|
||||
it("shows github repo", () => {
|
||||
expect(getSourceDisplayText({ source: "github", repo: "org/repo" })).toBe("GitHub: org/repo");
|
||||
});
|
||||
|
||||
it("shows url", () => {
|
||||
expect(getSourceDisplayText({ source: "url", url: "https://example.com" })).toBe("https://example.com");
|
||||
});
|
||||
|
||||
it("returns unknown for missing data", () => {
|
||||
expect(getSourceDisplayText({ source: "github" })).toBe("Unknown source");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSourceLink", () => {
|
||||
it("returns github link for github source", () => {
|
||||
expect(getSourceLink({ source: "github", repo: "org/repo" })).toBe("https://github.com/org/repo");
|
||||
});
|
||||
|
||||
it("returns url for url source", () => {
|
||||
expect(getSourceLink({ source: "url", url: "https://example.com" })).toBe("https://example.com");
|
||||
});
|
||||
|
||||
it("returns null when no repo or url", () => {
|
||||
expect(getSourceLink({ source: "github" })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCategoryBadgeColor", () => {
|
||||
it("returns blue for development categories", () => {
|
||||
expect(getCategoryBadgeColor("Development")).toBe("blue");
|
||||
expect(getCategoryBadgeColor("dev-tools")).toBe("blue");
|
||||
});
|
||||
|
||||
it("returns green for productivity categories", () => {
|
||||
expect(getCategoryBadgeColor("Productivity")).toBe("green");
|
||||
expect(getCategoryBadgeColor("Workflow")).toBe("green");
|
||||
});
|
||||
|
||||
it("returns purple for learning categories", () => {
|
||||
expect(getCategoryBadgeColor("Learning")).toBe("purple");
|
||||
expect(getCategoryBadgeColor("Education")).toBe("purple");
|
||||
});
|
||||
|
||||
it("returns red for security categories", () => {
|
||||
expect(getCategoryBadgeColor("Security")).toBe("red");
|
||||
expect(getCategoryBadgeColor("Safety")).toBe("red");
|
||||
});
|
||||
|
||||
it("returns orange for data categories", () => {
|
||||
expect(getCategoryBadgeColor("Data")).toBe("orange");
|
||||
expect(getCategoryBadgeColor("Analytics")).toBe("orange");
|
||||
});
|
||||
|
||||
it("returns yellow for integration categories", () => {
|
||||
expect(getCategoryBadgeColor("Integration")).toBe("yellow");
|
||||
expect(getCategoryBadgeColor("API")).toBe("yellow");
|
||||
});
|
||||
|
||||
it("returns gray for unknown or undefined categories", () => {
|
||||
expect(getCategoryBadgeColor("Unknown")).toBe("gray");
|
||||
expect(getCategoryBadgeColor(undefined)).toBe("gray");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatDateString", () => {
|
||||
it("formats valid date strings", () => {
|
||||
const result = formatDateString("2024-01-15T12:00:00Z");
|
||||
expect(result).toContain("2024");
|
||||
expect(result).toContain("Jan");
|
||||
expect(result).toContain("15");
|
||||
});
|
||||
|
||||
it("returns N/A for undefined", () => {
|
||||
expect(formatDateString(undefined)).toBe("N/A");
|
||||
});
|
||||
|
||||
it("returns N/A for empty string", () => {
|
||||
expect(formatDateString("")).toBe("N/A");
|
||||
});
|
||||
});
|
||||
|
||||
describe("truncateText", () => {
|
||||
it("returns text unchanged if shorter than max", () => {
|
||||
expect(truncateText("hello", 10)).toBe("hello");
|
||||
});
|
||||
|
||||
it("truncates and adds ellipsis", () => {
|
||||
expect(truncateText("hello world", 5)).toBe("hello...");
|
||||
});
|
||||
|
||||
it("handles exact length", () => {
|
||||
expect(truncateText("hello", 5)).toBe("hello");
|
||||
});
|
||||
|
||||
it("handles empty text", () => {
|
||||
expect(truncateText("", 5)).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("filterPluginsBySearch", () => {
|
||||
const plugins: MarketplacePluginEntry[] = [
|
||||
{
|
||||
name: "code-formatter",
|
||||
source: { source: "github", repo: "org/formatter" },
|
||||
description: "Formats code nicely",
|
||||
keywords: ["format", "lint"],
|
||||
},
|
||||
{
|
||||
name: "data-viewer",
|
||||
source: { source: "github", repo: "org/viewer" },
|
||||
description: "View data",
|
||||
keywords: ["analytics"],
|
||||
},
|
||||
];
|
||||
|
||||
it("returns all plugins for empty search", () => {
|
||||
expect(filterPluginsBySearch(plugins, "")).toEqual(plugins);
|
||||
expect(filterPluginsBySearch(plugins, " ")).toEqual(plugins);
|
||||
});
|
||||
|
||||
it("matches by name", () => {
|
||||
expect(filterPluginsBySearch(plugins, "formatter")).toHaveLength(1);
|
||||
expect(filterPluginsBySearch(plugins, "formatter")[0].name).toBe("code-formatter");
|
||||
});
|
||||
|
||||
it("matches by description", () => {
|
||||
expect(filterPluginsBySearch(plugins, "nicely")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("matches by keyword", () => {
|
||||
expect(filterPluginsBySearch(plugins, "analytics")).toHaveLength(1);
|
||||
expect(filterPluginsBySearch(plugins, "analytics")[0].name).toBe("data-viewer");
|
||||
});
|
||||
|
||||
it("is case insensitive", () => {
|
||||
expect(filterPluginsBySearch(plugins, "FORMATTER")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("filterPluginsByCategory", () => {
|
||||
const plugins: MarketplacePluginEntry[] = [
|
||||
{ name: "a", source: { source: "github" }, category: "Dev" },
|
||||
{ name: "b", source: { source: "github" }, category: "Security" },
|
||||
{ name: "c", source: { source: "github" }, category: "" },
|
||||
{ name: "d", source: { source: "github" } },
|
||||
];
|
||||
|
||||
it("returns all plugins for 'All'", () => {
|
||||
expect(filterPluginsByCategory(plugins, "All")).toEqual(plugins);
|
||||
});
|
||||
|
||||
it("returns uncategorized plugins for 'Other'", () => {
|
||||
const result = filterPluginsByCategory(plugins, "Other");
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((p) => p.name)).toEqual(["c", "d"]);
|
||||
});
|
||||
|
||||
it("filters by specific category", () => {
|
||||
const result = filterPluginsByCategory(plugins, "Dev");
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe("a");
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidSemanticVersion", () => {
|
||||
it("accepts valid semver", () => {
|
||||
expect(isValidSemanticVersion("1.0.0")).toBe(true);
|
||||
expect(isValidSemanticVersion("0.1.0-alpha")).toBe(true);
|
||||
expect(isValidSemanticVersion("2.3.4+build.1")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects invalid semver", () => {
|
||||
expect(isValidSemanticVersion("1.0")).toBe(false);
|
||||
expect(isValidSemanticVersion("abc")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for undefined (optional)", () => {
|
||||
expect(isValidSemanticVersion(undefined)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidEmail", () => {
|
||||
it("accepts valid emails", () => {
|
||||
expect(isValidEmail("user@example.com")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects invalid emails", () => {
|
||||
expect(isValidEmail("not-an-email")).toBe(false);
|
||||
expect(isValidEmail("@example.com")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for undefined (optional)", () => {
|
||||
expect(isValidEmail(undefined)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isValidUrl", () => {
|
||||
it("accepts valid urls", () => {
|
||||
expect(isValidUrl("https://example.com")).toBe(true);
|
||||
expect(isValidUrl("http://localhost:3000")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects invalid urls", () => {
|
||||
expect(isValidUrl("not a url")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for undefined (optional)", () => {
|
||||
expect(isValidUrl(undefined)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseKeywords", () => {
|
||||
it("splits comma-separated keywords", () => {
|
||||
expect(parseKeywords("a, b, c")).toEqual(["a", "b", "c"]);
|
||||
});
|
||||
|
||||
it("trims whitespace", () => {
|
||||
expect(parseKeywords(" foo , bar ")).toEqual(["foo", "bar"]);
|
||||
});
|
||||
|
||||
it("filters empty entries", () => {
|
||||
expect(parseKeywords("a,,b,")).toEqual(["a", "b"]);
|
||||
});
|
||||
|
||||
it("returns empty array for empty string", () => {
|
||||
expect(parseKeywords("")).toEqual([]);
|
||||
expect(parseKeywords(" ")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatKeywords", () => {
|
||||
it("joins keywords with comma and space", () => {
|
||||
expect(formatKeywords(["a", "b", "c"])).toBe("a, b, c");
|
||||
});
|
||||
|
||||
it("returns empty string for empty/undefined array", () => {
|
||||
expect(formatKeywords([])).toBe("");
|
||||
expect(formatKeywords(undefined)).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
|
||||
// Use vi.hoisted so the mock object is available when vi.mock is hoisted
|
||||
const mockStaticMessage = vi.hoisted(() => ({
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: vi.fn(),
|
||||
loading: vi.fn(),
|
||||
destroy: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("antd", () => ({
|
||||
message: mockStaticMessage,
|
||||
}));
|
||||
|
||||
import MessageManager, { setMessageInstance } from "./message_manager";
|
||||
|
||||
describe("MessageManager", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("when no instance is set (falls back to static message)", () => {
|
||||
it("delegates success to static message", () => {
|
||||
MessageManager.success("done!");
|
||||
expect(mockStaticMessage.success).toHaveBeenCalledWith("done!", undefined);
|
||||
});
|
||||
|
||||
it("delegates error to static message", () => {
|
||||
MessageManager.error("failed!", 5);
|
||||
expect(mockStaticMessage.error).toHaveBeenCalledWith("failed!", 5);
|
||||
});
|
||||
|
||||
it("delegates warning to static message", () => {
|
||||
MessageManager.warning("watch out");
|
||||
expect(mockStaticMessage.warning).toHaveBeenCalledWith("watch out", undefined);
|
||||
});
|
||||
|
||||
it("delegates info to static message", () => {
|
||||
MessageManager.info("fyi");
|
||||
expect(mockStaticMessage.info).toHaveBeenCalledWith("fyi", undefined);
|
||||
});
|
||||
|
||||
it("delegates loading to static message", () => {
|
||||
MessageManager.loading("loading...", 3);
|
||||
expect(mockStaticMessage.loading).toHaveBeenCalledWith("loading...", 3);
|
||||
});
|
||||
|
||||
it("delegates destroy to static message", () => {
|
||||
MessageManager.destroy();
|
||||
expect(mockStaticMessage.destroy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when a custom instance is set", () => {
|
||||
const mockInstance = {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
info: vi.fn(),
|
||||
loading: vi.fn(),
|
||||
destroy: vi.fn(),
|
||||
open: vi.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
setMessageInstance(mockInstance as any);
|
||||
});
|
||||
|
||||
it("delegates success to custom instance", () => {
|
||||
MessageManager.success("done!");
|
||||
expect(mockInstance.success).toHaveBeenCalledWith("done!", undefined);
|
||||
expect(mockStaticMessage.success).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("delegates error with duration to custom instance", () => {
|
||||
MessageManager.error("failed!", 5);
|
||||
expect(mockInstance.error).toHaveBeenCalledWith("failed!", 5);
|
||||
expect(mockStaticMessage.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("delegates warning to custom instance", () => {
|
||||
MessageManager.warning("watch out");
|
||||
expect(mockInstance.warning).toHaveBeenCalledWith("watch out", undefined);
|
||||
});
|
||||
|
||||
it("delegates info to custom instance", () => {
|
||||
MessageManager.info("fyi", 2);
|
||||
expect(mockInstance.info).toHaveBeenCalledWith("fyi", 2);
|
||||
});
|
||||
|
||||
it("delegates loading to custom instance and returns result", () => {
|
||||
const mockReturn = { then: vi.fn() };
|
||||
mockInstance.loading.mockReturnValue(mockReturn);
|
||||
const result = MessageManager.loading("loading...", 3);
|
||||
expect(mockInstance.loading).toHaveBeenCalledWith("loading...", 3);
|
||||
expect(result).toBe(mockReturn);
|
||||
});
|
||||
|
||||
it("delegates destroy to custom instance", () => {
|
||||
MessageManager.destroy();
|
||||
expect(mockInstance.destroy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
// Mock antd Spin component
|
||||
vi.mock("antd", () => ({
|
||||
Spin: ({ indicator, size, ...props }: any) => (
|
||||
<div data-testid="spin" data-size={size} {...props}>
|
||||
{indicator}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock the icon
|
||||
vi.mock("@ant-design/icons", () => ({
|
||||
LoadingOutlined: ({ style, spin, ...props }: any) => (
|
||||
<span
|
||||
data-testid="loading-icon"
|
||||
data-spin={spin}
|
||||
style={style}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
}));
|
||||
|
||||
import { AntDLoadingSpinner } from "./AntDLoadingSpinner";
|
||||
|
||||
describe("AntDLoadingSpinner", () => {
|
||||
it("renders without props", () => {
|
||||
render(<AntDLoadingSpinner />);
|
||||
expect(screen.getByTestId("spin")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("loading-icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("passes size prop to Spin", () => {
|
||||
render(<AntDLoadingSpinner size="large" />);
|
||||
expect(screen.getByTestId("spin")).toHaveAttribute("data-size", "large");
|
||||
});
|
||||
|
||||
it("passes small size to Spin", () => {
|
||||
render(<AntDLoadingSpinner size="small" />);
|
||||
expect(screen.getByTestId("spin")).toHaveAttribute("data-size", "small");
|
||||
});
|
||||
|
||||
it("applies custom fontSize to the icon", () => {
|
||||
render(<AntDLoadingSpinner fontSize={32} />);
|
||||
const icon = screen.getByTestId("loading-icon");
|
||||
expect(icon).toHaveStyle({ fontSize: "32px" });
|
||||
});
|
||||
|
||||
it("does not set style when fontSize is not provided", () => {
|
||||
render(<AntDLoadingSpinner />);
|
||||
const icon = screen.getByTestId("loading-icon");
|
||||
expect(icon.style.fontSize).toBe("");
|
||||
});
|
||||
|
||||
it("sets spin attribute on icon", () => {
|
||||
render(<AntDLoadingSpinner />);
|
||||
const icon = screen.getByTestId("loading-icon");
|
||||
expect(icon).toHaveAttribute("data-spin", "true");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user