Adding tests

This commit is contained in:
yuneng-jiang
2026-02-04 12:35:58 -08:00
parent 609c423314
commit cd9466a50c
7 changed files with 546 additions and 34 deletions
@@ -1,14 +1,14 @@
import React, { useState } from "react";
import { Modal, Tooltip, Form, Select, Input, Typography } from "antd";
import { InfoCircleOutlined } from "@ant-design/icons";
import { Button, TextInput } from "@tremor/react";
import { createSearchTool, fetchAvailableSearchProviders } from "../networking";
import { SearchTool, AvailableSearchProvider } from "./types";
import { isAdminRole } from "@/utils/roles";
import NotificationsManager from "../molecules/notifications_manager";
import { InfoCircleOutlined } from "@ant-design/icons";
import { useQuery } from "@tanstack/react-query";
import SearchConnectionTest from "./SearchConnectionTest";
import { Button, TextInput } from "@tremor/react";
import { Form, Input, Modal, Select, Tooltip, Typography } from "antd";
import Image from "next/image";
import React, { useState } from "react";
import NotificationsManager from "../molecules/notifications_manager";
import { createSearchTool, fetchAvailableSearchProviders } from "../networking";
import SearchConnectionTest from "./SearchConnectionTest";
import { AvailableSearchProvider, SearchTool } from "./types";
const { TextArea } = Input;
@@ -1,8 +1,8 @@
import { InfoCircleOutlined, WarningOutlined } from "@ant-design/icons";
import { Button, Divider, Typography } from "antd";
import React, { useEffect, useState } from "react";
import { testSearchToolConnection } from "../networking";
import { Button, Typography, Divider } from "antd";
import { WarningOutlined, InfoCircleOutlined } from "@ant-design/icons";
import NotificationsManager from "../molecules/notifications_manager";
import { testSearchToolConnection } from "../networking";
const { Text } = Typography;
@@ -1,7 +1,7 @@
import { ColumnsType } from "antd/es/table";
import { SearchTool } from "./types";
import { Tag } from "antd";
import { ColumnsType } from "antd/es/table";
import TableIconActionButton from "../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
import { SearchTool } from "./types";
export const searchToolColumns = (
onView: (searchToolId: string) => void,
@@ -0,0 +1,278 @@
import { render, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { SearchToolView } from "./SearchToolView";
import { AvailableSearchProvider, SearchTool } from "./types";
vi.mock("@/utils/dataUtils", () => ({
copyToClipboard: vi.fn().mockResolvedValue(true),
}));
vi.mock("./SearchToolTester", () => ({
SearchToolTester: ({ searchToolName, accessToken }: { searchToolName: string; accessToken: string }) => (
<div data-testid="search-tool-tester">
<span>Search Tool Tester for {searchToolName}</span>
<span>Access Token: {accessToken}</span>
</div>
),
}));
describe("SearchToolView", () => {
const mockSearchTool: SearchTool = {
search_tool_id: "test-tool-id-123",
search_tool_name: "Test Search Tool",
litellm_params: {
search_provider: "perplexity",
api_key: "sk-test-key",
},
search_tool_info: {
description: "Test description",
},
created_at: "2024-01-15T10:30:00Z",
};
const mockAvailableProviders: AvailableSearchProvider[] = [
{
provider_name: "perplexity",
ui_friendly_name: "Perplexity AI",
},
{
provider_name: "tavily",
ui_friendly_name: "Tavily Search",
},
];
const defaultProps = {
searchTool: mockSearchTool,
onBack: vi.fn(),
isEditing: false,
accessToken: "test-token",
availableProviders: mockAvailableProviders,
};
beforeEach(async () => {
vi.clearAllMocks();
const { copyToClipboard } = await import("@/utils/dataUtils");
vi.mocked(copyToClipboard).mockResolvedValue(true);
});
it("should render", () => {
render(<SearchToolView {...defaultProps} />);
expect(screen.getByText("Test Search Tool")).toBeInTheDocument();
});
it("should display search tool name", () => {
render(<SearchToolView {...defaultProps} />);
expect(screen.getByText("Test Search Tool")).toBeInTheDocument();
});
it("should display search tool ID", () => {
render(<SearchToolView {...defaultProps} />);
expect(screen.getByText("test-tool-id-123")).toBeInTheDocument();
});
it("should display provider name using UI-friendly name when available", () => {
render(<SearchToolView {...defaultProps} />);
expect(screen.getByText("Perplexity AI")).toBeInTheDocument();
});
it("should display provider name using provider_name when UI-friendly name is not available", () => {
const searchToolWithoutProvider: SearchTool = {
...mockSearchTool,
litellm_params: {
search_provider: "unknown-provider",
},
};
render(
<SearchToolView
{...defaultProps}
searchTool={searchToolWithoutProvider}
/>,
);
expect(screen.getByText("unknown-provider")).toBeInTheDocument();
});
it("should display masked API key when API key is set", () => {
render(<SearchToolView {...defaultProps} />);
expect(screen.getByText("****")).toBeInTheDocument();
});
it("should display 'Not set' when API key is not set", () => {
const searchToolWithoutApiKey: SearchTool = {
...mockSearchTool,
litellm_params: {
search_provider: "perplexity",
},
};
render(
<SearchToolView
{...defaultProps}
searchTool={searchToolWithoutApiKey}
/>,
);
expect(screen.getByText("Not set")).toBeInTheDocument();
});
it("should display formatted created_at date", () => {
render(<SearchToolView {...defaultProps} />);
const dateText = screen.getByText(/2024-01-15/);
expect(dateText).toBeInTheDocument();
});
it("should display 'Unknown' when created_at is not set", () => {
const searchToolWithoutDate: SearchTool = {
...mockSearchTool,
created_at: undefined,
};
render(
<SearchToolView
{...defaultProps}
searchTool={searchToolWithoutDate}
/>,
);
expect(screen.getByText("Unknown")).toBeInTheDocument();
});
it("should display description when search_tool_info.description is provided", () => {
render(<SearchToolView {...defaultProps} />);
expect(screen.getByText("Test description")).toBeInTheDocument();
});
it("should not display description card when search_tool_info.description is not provided", () => {
const searchToolWithoutDescription: SearchTool = {
...mockSearchTool,
search_tool_info: {},
};
render(
<SearchToolView
{...defaultProps}
searchTool={searchToolWithoutDescription}
/>,
);
expect(screen.queryByText("Description")).not.toBeInTheDocument();
});
it("should call onBack when back button is clicked", async () => {
const user = userEvent.setup({ delay: null });
const onBack = vi.fn();
render(<SearchToolView {...defaultProps} onBack={onBack} />);
const backButton = screen.getByRole("button", { name: /back to all search tools/i });
await user.click(backButton);
expect(onBack).toHaveBeenCalledTimes(1);
});
it("should copy search tool name to clipboard when copy button is clicked", async () => {
const user = userEvent.setup({ delay: null });
const { copyToClipboard } = await import("@/utils/dataUtils");
render(<SearchToolView {...defaultProps} />);
const toolNameContainer = screen.getByText("Test Search Tool").closest("div");
expect(toolNameContainer).toBeInTheDocument();
const copyButtons = within(toolNameContainer!).getAllByRole("button");
const nameCopyButton = copyButtons.find((button) => {
return button.querySelector("svg") !== null;
});
expect(nameCopyButton).toBeInTheDocument();
await user.click(nameCopyButton!);
await waitFor(() => {
expect(copyToClipboard).toHaveBeenCalledWith("Test Search Tool");
});
});
it("should copy search tool ID to clipboard when copy button is clicked", async () => {
const user = userEvent.setup({ delay: null });
const { copyToClipboard } = await import("@/utils/dataUtils");
render(<SearchToolView {...defaultProps} />);
const toolIdContainer = screen.getByText("test-tool-id-123").closest("div");
expect(toolIdContainer).toBeInTheDocument();
const copyButtons = within(toolIdContainer!).getAllByRole("button");
const idCopyButton = copyButtons.find((button) => {
return button.querySelector("svg") !== null;
});
expect(idCopyButton).toBeInTheDocument();
await user.click(idCopyButton!);
await waitFor(() => {
expect(copyToClipboard).toHaveBeenCalledWith("test-tool-id-123");
});
});
it("should show check icon after copying search tool name", async () => {
const user = userEvent.setup({ delay: null });
const { copyToClipboard } = await import("@/utils/dataUtils");
vi.mocked(copyToClipboard).mockResolvedValue(true);
render(<SearchToolView {...defaultProps} />);
const toolNameContainer = screen.getByText("Test Search Tool").closest("div");
const copyButtons = within(toolNameContainer!).getAllByRole("button");
const nameCopyButton = copyButtons.find((button) => {
return button.querySelector("svg") !== null;
});
expect(nameCopyButton).toBeInTheDocument();
const initialSvg = nameCopyButton!.querySelector("svg");
expect(initialSvg).toBeInTheDocument();
await user.click(nameCopyButton!);
await waitFor(() => {
const updatedSvg = nameCopyButton!.querySelector("svg");
expect(updatedSvg).toBeInTheDocument();
expect(nameCopyButton).toHaveClass("text-green-600");
});
});
it("should not show check icon when copy fails", async () => {
const user = userEvent.setup({ delay: null });
const { copyToClipboard } = await import("@/utils/dataUtils");
vi.mocked(copyToClipboard).mockResolvedValue(false);
render(<SearchToolView {...defaultProps} />);
const toolNameContainer = screen.getByText("Test Search Tool").closest("div");
const copyButtons = within(toolNameContainer!).getAllByRole("button");
const nameCopyButton = copyButtons.find((button) => {
return button.querySelector("svg") !== null;
});
expect(nameCopyButton).toBeInTheDocument();
await user.click(nameCopyButton!);
await waitFor(() => {
expect(copyToClipboard).toHaveBeenCalledWith("Test Search Tool");
}, { timeout: 3000 });
expect(nameCopyButton).not.toHaveClass("text-green-600");
});
it("should render SearchToolTester when accessToken is provided", () => {
render(<SearchToolView {...defaultProps} />);
expect(screen.getByTestId("search-tool-tester")).toBeInTheDocument();
expect(screen.getByText(/Search Tool Tester for Test Search Tool/)).toBeInTheDocument();
});
it("should not render SearchToolTester when accessToken is null", () => {
render(<SearchToolView {...defaultProps} accessToken={null} />);
expect(screen.queryByTestId("search-tool-tester")).not.toBeInTheDocument();
});
it("should pass correct props to SearchToolTester", () => {
render(<SearchToolView {...defaultProps} />);
expect(screen.getByText("Access Token: test-token")).toBeInTheDocument();
});
});
@@ -1,11 +1,11 @@
import React, { useState } from "react";
import { ArrowLeftIcon } from "@heroicons/react/outline";
import { Title, Card, Button, Text, Grid } from "@tremor/react";
import { SearchTool, AvailableSearchProvider } from "./types";
import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils";
import { CheckIcon, CopyIcon } from "lucide-react";
import { ArrowLeftIcon } from "@heroicons/react/outline";
import { Button, Card, Grid, Text, Title } from "@tremor/react";
import { Button as AntdButton } from "antd";
import { CheckIcon, CopyIcon } from "lucide-react";
import React, { useState } from "react";
import { SearchToolTester } from "./SearchToolTester";
import { AvailableSearchProvider, SearchTool } from "./types";
interface SearchToolViewProps {
searchTool: SearchTool;
@@ -54,8 +54,8 @@ export const SearchToolView: React.FC<SearchToolViewProps> = ({
icon={copiedStates["search-tool-name"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(searchTool.search_tool_name, "search-tool-name")}
className={`left-2 z-10 transition-all duration-200 ${copiedStates["search-tool-name"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
/>
</div>
@@ -67,8 +67,8 @@ export const SearchToolView: React.FC<SearchToolViewProps> = ({
icon={copiedStates["search-tool-id"] ? <CheckIcon size={12} /> : <CopyIcon size={12} />}
onClick={() => copyToClipboard(searchTool.search_tool_id, "search-tool-id")}
className={`left-2 z-10 transition-all duration-200 ${copiedStates["search-tool-id"]
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
? "text-green-600 bg-green-50 border-green-200"
: "text-gray-500 hover:text-gray-700 hover:bg-gray-100"
}`}
/>
</div>
@@ -0,0 +1,234 @@
import * as roles from "@/utils/roles";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import * as networking from "../networking";
import SearchTools from "./SearchTools";
import { AvailableSearchProvider, SearchTool } from "./types";
vi.mock("../networking", () => ({
fetchSearchTools: vi.fn(),
updateSearchTool: vi.fn(),
deleteSearchTool: vi.fn(),
fetchAvailableSearchProviders: vi.fn(),
}));
vi.mock("@/utils/roles", () => ({
isAdminRole: vi.fn(),
}));
vi.mock("./SearchToolView", () => ({
SearchToolView: ({ searchTool, onBack }: { searchTool: SearchTool; onBack: () => void }) => (
<div data-testid="search-tool-view">
<div>Search Tool View: {searchTool.search_tool_name}</div>
<button onClick={onBack}>Back</button>
</div>
),
}));
vi.mock("./CreateSearchTools", () => ({
default: ({
isModalVisible,
setModalVisible,
}: {
isModalVisible: boolean;
setModalVisible: (visible: boolean) => void;
}) =>
isModalVisible ? (
<div data-testid="create-search-tool-modal">
<button onClick={() => setModalVisible(false)}>Close Create Modal</button>
</div>
) : null,
}));
vi.mock("../common_components/DeleteResourceModal", () => ({
default: ({
isOpen,
onOk,
onCancel,
}: {
isOpen: boolean;
onOk: () => void;
onCancel: () => void;
}) =>
isOpen ? (
<div data-testid="delete-resource-modal">
<button onClick={onOk}>Confirm Delete</button>
<button onClick={onCancel}>Cancel Delete</button>
</div>
) : null,
}));
const mockSearchTools: SearchTool[] = [
{
search_tool_id: "tool-1",
search_tool_name: "Perplexity Search",
litellm_params: {
search_provider: "perplexity",
api_key: "sk-test-key",
},
search_tool_info: {
description: "Test description",
},
created_at: "2024-01-15T10:30:00Z",
},
{
search_tool_id: "tool-2",
search_tool_name: "Tavily Search",
litellm_params: {
search_provider: "tavily",
},
created_at: "2024-01-16T10:30:00Z",
},
];
const mockAvailableProviders: AvailableSearchProvider[] = [
{
provider_name: "perplexity",
ui_friendly_name: "Perplexity AI",
},
{
provider_name: "tavily",
ui_friendly_name: "Tavily Search",
},
];
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
describe("SearchTools", () => {
const defaultProps = {
accessToken: "test-token",
userRole: "Admin",
userID: "user-1",
};
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(networking.fetchSearchTools).mockResolvedValue({ search_tools: mockSearchTools });
vi.mocked(networking.fetchAvailableSearchProviders).mockResolvedValue({ providers: mockAvailableProviders });
vi.mocked(roles.isAdminRole).mockReturnValue(true);
});
it("should render", async () => {
render(<SearchTools {...defaultProps} />, { wrapper: createWrapper() });
await waitFor(() => {
expect(screen.getByText("Search Tools")).toBeInTheDocument();
});
});
it("should display missing authentication parameters message when accessToken is missing", () => {
render(<SearchTools {...defaultProps} accessToken={null} />, { wrapper: createWrapper() });
expect(screen.getByText("Missing required authentication parameters.")).toBeInTheDocument();
});
it("should display missing authentication parameters message when userRole is missing", () => {
render(<SearchTools {...defaultProps} userRole={null} />, { wrapper: createWrapper() });
expect(screen.getByText("Missing required authentication parameters.")).toBeInTheDocument();
});
it("should display missing authentication parameters message when userID is missing", () => {
render(<SearchTools {...defaultProps} userID={null} />, { wrapper: createWrapper() });
expect(screen.getByText("Missing required authentication parameters.")).toBeInTheDocument();
});
it("should display search tools table with tools", async () => {
render(<SearchTools {...defaultProps} />, { wrapper: createWrapper() });
await waitFor(() => {
expect(screen.getByText("Perplexity Search")).toBeInTheDocument();
});
expect(screen.getAllByText("Tavily Search").length).toBeGreaterThan(0);
});
it("should display empty state when no search tools are available", async () => {
vi.mocked(networking.fetchSearchTools).mockResolvedValue({ search_tools: [] });
render(<SearchTools {...defaultProps} />, { wrapper: createWrapper() });
await waitFor(() => {
expect(screen.getByText("No search tools configured")).toBeInTheDocument();
});
});
it("should show Add New Search Tool button when user is admin", async () => {
render(<SearchTools {...defaultProps} />, { wrapper: createWrapper() });
await waitFor(() => {
expect(screen.getByRole("button", { name: /add new search tool/i })).toBeInTheDocument();
});
});
it("should not show Add New Search Tool button when user is not admin", async () => {
vi.mocked(roles.isAdminRole).mockReturnValue(false);
render(<SearchTools {...defaultProps} />, { wrapper: createWrapper() });
await waitFor(() => {
expect(screen.getByText("Search Tools")).toBeInTheDocument();
});
expect(screen.queryByRole("button", { name: /add new search tool/i })).not.toBeInTheDocument();
});
it("should open create modal when Add New Search Tool button is clicked", async () => {
const user = userEvent.setup({ delay: null });
render(<SearchTools {...defaultProps} />, { wrapper: createWrapper() });
await waitFor(() => {
expect(screen.getByRole("button", { name: /add new search tool/i })).toBeInTheDocument();
});
const addButton = screen.getByRole("button", { name: /add new search tool/i });
await user.click(addButton);
expect(screen.getByTestId("create-search-tool-modal")).toBeInTheDocument();
});
it("should navigate to tool view when tool ID is clicked", async () => {
const user = userEvent.setup({ delay: null });
render(<SearchTools {...defaultProps} />, { wrapper: createWrapper() });
await waitFor(() => {
expect(screen.getByText("Perplexity Search")).toBeInTheDocument();
});
const toolIdButton = screen.getByRole("button", { name: /tool-1/i });
await user.click(toolIdButton);
await waitFor(() => {
expect(screen.getByTestId("search-tool-view")).toBeInTheDocument();
});
expect(screen.getByText(/Search Tool View: Perplexity Search/i)).toBeInTheDocument();
});
it("should navigate back from tool view to table", async () => {
const user = userEvent.setup({ delay: null });
render(<SearchTools {...defaultProps} />, { wrapper: createWrapper() });
await waitFor(() => {
expect(screen.getByText("Perplexity Search")).toBeInTheDocument();
});
const toolIdButton = screen.getByRole("button", { name: /tool-1/i });
await user.click(toolIdButton);
await waitFor(() => {
expect(screen.getByTestId("search-tool-view")).toBeInTheDocument();
});
const backButton = screen.getByRole("button", { name: /back/i });
await user.click(backButton);
await waitFor(() => {
expect(screen.queryByTestId("search-tool-view")).not.toBeInTheDocument();
expect(screen.getByText("Perplexity Search")).toBeInTheDocument();
});
});
});
@@ -1,21 +1,21 @@
import React, { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Modal, Form, Input, Select, Table, Spin } from "antd";
import { isAdminRole } from "@/utils/roles";
import { LoadingOutlined } from "@ant-design/icons";
import { Button, Title, Text } from "@tremor/react";
import { searchToolColumns } from "./SearchToolColumn";
import { useQuery } from "@tanstack/react-query";
import { Button, Text, Title } from "@tremor/react";
import { Form, Input, Modal, Select, Spin, Table } from "antd";
import React, { useState } from "react";
import DeleteResourceModal from "../common_components/DeleteResourceModal";
import NotificationsManager from "../molecules/notifications_manager";
import {
fetchSearchTools,
updateSearchTool,
deleteSearchTool,
fetchAvailableSearchProviders,
fetchSearchTools,
updateSearchTool,
} from "../networking";
import { SearchTool, AvailableSearchProvider } from "./types";
import { isAdminRole } from "@/utils/roles";
import NotificationsManager from "../molecules/notifications_manager";
import { SearchToolView } from "./SearchToolView";
import CreateSearchTool from "./CreateSearchTools";
import DeleteResourceModal from "../common_components/DeleteResourceModal";
import { searchToolColumns } from "./SearchToolColumn";
import { SearchToolView } from "./SearchToolView";
import { AvailableSearchProvider, SearchTool } from "./types";
interface SearchToolsProps {
accessToken: string | null;