Merge pull request #18618 from BerriAI/litellm_ui_unit_test_coverage

[Infra] UI - Unit Tests: Adding Tests to Increase Coverage
This commit is contained in:
yuneng-jiang 2026-01-03 17:55:53 -08:00 committed by GitHub
commit 280085edaa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1965 additions and 10 deletions

View file

@ -0,0 +1,620 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { describe, it, expect, vi, beforeEach, Mock } from "vitest";
import EditSSOSettingsModal from "./EditSSOSettingsModal";
import { useSSOSettings } from "@/app/(dashboard)/hooks/sso/useSSOSettings";
import { useEditSSOSettings } from "@/app/(dashboard)/hooks/sso/useEditSSOSettings";
import NotificationsManager from "@/components/molecules/notifications_manager";
import { parseErrorMessage } from "@/components/shared/errorUtils";
import { processSSOSettingsPayload } from "../utils";
// Constants
const SSO_PROVIDERS = {
GOOGLE: "google",
MICROSOFT: "microsoft",
OKTA: "okta",
AUTH0: "auth0",
GENERIC: "generic",
} as const;
const TEST_DATA = {
MODAL_TITLE: "Edit SSO Settings",
MODAL_WIDTH: "800",
SUCCESS_MESSAGE: "SSO settings updated successfully",
ERROR_MESSAGE_PREFIX: "Failed to save SSO settings:",
BUTTON_TEXT: {
CANCEL: "Cancel",
SAVE: "Save",
SAVING: "Saving...",
},
} as const;
const TEST_IDS = {
MODAL: "modal",
BUTTON: "button",
BASE_SSO_FORM: "base-sso-form",
TRIGGER_FORM_SUBMIT: "trigger-form-submit",
} as const;
// Mock form instance
const mockForm = {
resetFields: vi.fn(),
setFieldsValue: vi.fn(),
getFieldsValue: vi.fn(),
submit: vi.fn(),
};
// Types
type SSOData = {
values: Record<string, any>;
} & Record<string, any>;
type SSOSettingsHookReturn = {
data: SSOData | null;
isLoading: boolean;
error: any;
};
type EditSSOSettingsHookReturn = {
mutateAsync: ReturnType<typeof vi.fn>;
isPending: boolean;
};
// Test data factories
const createSSOData = (overrides: Record<string, any> = {}): SSOData => ({
values: {
user_email: "test@example.com",
...overrides,
},
});
const createGoogleSSOData = (overrides: Record<string, any> = {}) =>
createSSOData({
google_client_id: "test-google-id",
google_client_secret: "test-google-secret",
...overrides,
});
const createMicrosoftSSOData = (overrides: Record<string, any> = {}) =>
createSSOData({
microsoft_client_id: "test-microsoft-id",
microsoft_client_secret: "test-microsoft-secret",
microsoft_tenant: "test-tenant",
...overrides,
});
const createGenericSSOData = (overrides: Record<string, any> = {}) =>
createSSOData({
generic_client_id: "test-generic-id",
generic_client_secret: "test-generic-secret",
generic_authorization_endpoint: overrides.authorization_endpoint || "https://custom.example.com/oauth",
...overrides,
});
const createRoleMappingsSSOData = (overrides: Record<string, any> = {}) =>
createGoogleSSOData({
role_mappings: {
group_claim: "groups",
default_role: "internal_user",
roles: {
proxy_admin: overrides.proxy_admin || ["admin-group"],
proxy_admin_viewer: overrides.proxy_admin_viewer || ["viewer-group"],
internal_user: overrides.internal_user || ["user-group"],
internal_user_viewer: overrides.internal_user_viewer || ["readonly-group"],
},
},
...overrides,
});
// Mock utilities
const createMockHooks = (): {
useSSOSettings: SSOSettingsHookReturn;
useEditSSOSettings: EditSSOSettingsHookReturn;
} => ({
useSSOSettings: {
data: null,
isLoading: false,
error: null,
},
useEditSSOSettings: {
mutateAsync: vi.fn(),
isPending: false,
},
});
vi.mock("antd", () => ({
Modal: ({ children, open, title, footer, onCancel, width, ...props }: any) => (
<div data-testid={TEST_IDS.MODAL} data-open={open} data-title={title} data-width={width} {...props}>
<div data-testid="modal-content">{children}</div>
<div data-testid="modal-footer">{footer}</div>
<button data-testid="modal-cancel" onClick={onCancel} />
</div>
),
Button: ({ children, onClick, loading, disabled, ...props }: any) => (
<button data-testid={TEST_IDS.BUTTON} onClick={onClick} data-loading={loading} disabled={disabled} {...props}>
{children}
</button>
),
Form: {
useForm: () => [mockForm],
},
Space: ({ children, ...props }: any) => (
<div data-testid="space" {...props}>
{children}
</div>
),
}));
vi.mock("./BaseSSOSettingsForm", () => ({
default: ({ form, onFormSubmit }: any) => (
<div data-testid={TEST_IDS.BASE_SSO_FORM}>
<button data-testid={TEST_IDS.TRIGGER_FORM_SUBMIT} onClick={() => onFormSubmit({ testField: "testValue" })}>
Trigger Form Submit
</button>
</div>
),
}));
vi.mock("@/app/(dashboard)/hooks/sso/useSSOSettings", () => ({
useSSOSettings: vi.fn(),
}));
vi.mock("@/app/(dashboard)/hooks/sso/useEditSSOSettings", () => ({
useEditSSOSettings: vi.fn(),
}));
vi.mock("@/components/molecules/notifications_manager", () => ({
default: {
success: vi.fn(),
fromBackend: vi.fn(),
},
}));
vi.mock("@/components/shared/errorUtils", () => ({
parseErrorMessage: vi.fn(),
}));
vi.mock("../utils", () => ({
processSSOSettingsPayload: vi.fn(),
}));
// Test helpers
const setupMocks = (
overrides: Partial<{
useSSOSettings: Partial<SSOSettingsHookReturn>;
useEditSSOSettings: Partial<EditSSOSettingsHookReturn>;
}> = {},
) => {
const defaultMocks = createMockHooks();
const mocks = {
useSSOSettings: { ...defaultMocks.useSSOSettings, ...overrides.useSSOSettings },
useEditSSOSettings: { ...defaultMocks.useEditSSOSettings, ...overrides.useEditSSOSettings },
};
(useSSOSettings as Mock).mockReturnValue(mocks.useSSOSettings);
(useEditSSOSettings as Mock).mockReturnValue(mocks.useEditSSOSettings);
return mocks;
};
const renderComponent = (props: Partial<React.ComponentProps<typeof EditSSOSettingsModal>> = {}) => {
const defaultProps = {
isVisible: true,
onCancel: vi.fn(),
onSuccess: vi.fn(),
};
return {
...render(<EditSSOSettingsModal {...defaultProps} {...props} />),
mockOnCancel: defaultProps.onCancel,
mockOnSuccess: defaultProps.onSuccess,
};
};
const getButtons = () => screen.getAllByTestId(TEST_IDS.BUTTON);
const getCancelButton = () => getButtons()[0];
const getSaveButton = () => getButtons()[1];
describe("EditSSOSettingsModal", () => {
beforeEach(() => {
vi.clearAllMocks();
setupMocks();
});
describe("Rendering", () => {
it("renders without crashing", () => {
expect(() => renderComponent()).not.toThrow();
});
it("displays modal with correct configuration", () => {
renderComponent();
const modal = screen.getByTestId(TEST_IDS.MODAL);
expect(modal).toHaveAttribute("data-open", "true");
expect(modal).toHaveAttribute("data-title", TEST_DATA.MODAL_TITLE);
expect(modal).toHaveAttribute("data-width", TEST_DATA.MODAL_WIDTH);
});
it("displays modal as closed when not visible", () => {
renderComponent({ isVisible: false });
const modal = screen.getByTestId(TEST_IDS.MODAL);
expect(modal).toHaveAttribute("data-open", "false");
});
});
describe("Footer Actions", () => {
it("renders cancel and save buttons", () => {
renderComponent();
const buttons = getButtons();
expect(buttons).toHaveLength(2);
expect(buttons[0]).toHaveTextContent(TEST_DATA.BUTTON_TEXT.CANCEL);
expect(buttons[1]).toHaveTextContent(TEST_DATA.BUTTON_TEXT.SAVE);
});
it("calls onCancel and resets form when cancel button is clicked", () => {
const { mockOnCancel } = renderComponent();
fireEvent.click(getCancelButton());
expect(mockForm.resetFields).toHaveBeenCalled();
expect(mockOnCancel).toHaveBeenCalled();
});
it("calls form.submit when save button is clicked", () => {
renderComponent();
fireEvent.click(getSaveButton());
expect(mockForm.submit).toHaveBeenCalled();
});
describe("Loading States", () => {
it("disables cancel button during submission", () => {
setupMocks({
useEditSSOSettings: { mutateAsync: vi.fn(), isPending: true },
});
renderComponent();
expect(getCancelButton()).toBeDisabled();
});
it("shows loading state on save button during submission", () => {
setupMocks({
useEditSSOSettings: { mutateAsync: vi.fn(), isPending: true },
});
renderComponent();
expect(getSaveButton()).toHaveAttribute("data-loading", "true");
expect(getSaveButton()).toHaveTextContent(TEST_DATA.BUTTON_TEXT.SAVING);
});
});
});
describe("Form Submission", () => {
const formValues = { testField: "testValue" };
const processedPayload = { processed: "payload" };
beforeEach(() => {
(processSSOSettingsPayload as any).mockReturnValue(processedPayload);
});
it("processes form values and submits successfully", async () => {
const mockMutateAsync = vi.fn().mockImplementation((payload, options) => {
options.onSuccess();
return Promise.resolve({ success: true });
});
setupMocks({
useEditSSOSettings: { mutateAsync: mockMutateAsync, isPending: false },
});
const { mockOnSuccess } = renderComponent();
fireEvent.click(screen.getByTestId(TEST_IDS.TRIGGER_FORM_SUBMIT));
expect(processSSOSettingsPayload).toHaveBeenCalledWith(formValues);
expect(mockMutateAsync).toHaveBeenCalledWith(
processedPayload,
expect.objectContaining({
onSuccess: expect.any(Function),
onError: expect.any(Function),
}),
);
});
it("shows success notification and calls onSuccess callback", async () => {
const mockMutateAsync = vi.fn().mockImplementation((payload, options) => {
options.onSuccess();
return Promise.resolve({ success: true });
});
setupMocks({
useEditSSOSettings: { mutateAsync: mockMutateAsync, isPending: false },
});
const { mockOnSuccess } = renderComponent();
fireEvent.click(screen.getByTestId(TEST_IDS.TRIGGER_FORM_SUBMIT));
expect(NotificationsManager.success).toHaveBeenCalledWith(TEST_DATA.SUCCESS_MESSAGE);
expect(mockOnSuccess).toHaveBeenCalled();
});
it("handles submission errors gracefully", async () => {
const error = new Error("Submission failed");
const mockMutateAsync = vi.fn().mockImplementation((payload, options) => {
options.onError(error);
return Promise.reject(error);
});
setupMocks({
useEditSSOSettings: { mutateAsync: mockMutateAsync, isPending: false },
});
(parseErrorMessage as any).mockReturnValue("Parsed error message");
renderComponent();
fireEvent.click(screen.getByTestId(TEST_IDS.TRIGGER_FORM_SUBMIT));
expect(parseErrorMessage).toHaveBeenCalledWith(error);
expect(NotificationsManager.fromBackend).toHaveBeenCalledWith(
`${TEST_DATA.ERROR_MESSAGE_PREFIX} Parsed error message`,
);
});
});
describe("Form Initialization", () => {
describe("Provider Detection", () => {
const testProviderDetection = (testName: string, ssoData: SSOData, expectedProvider: string) => {
it(`detects ${testName} provider`, async () => {
setupMocks({
useSSOSettings: { data: ssoData, isLoading: false, error: null },
});
renderComponent();
await waitFor(() => {
expect(mockForm.setFieldsValue).toHaveBeenCalledWith({
sso_provider: expectedProvider,
...ssoData.values,
});
});
});
};
testProviderDetection("Google", createGoogleSSOData(), SSO_PROVIDERS.GOOGLE);
testProviderDetection("Microsoft", createMicrosoftSSOData(), SSO_PROVIDERS.MICROSOFT);
testProviderDetection(
"Okta",
createGenericSSOData({
authorization_endpoint: "https://okta.example.com/oauth2/authorize",
}),
SSO_PROVIDERS.OKTA,
);
testProviderDetection(
"Auth0 (detected as Okta)",
createGenericSSOData({
authorization_endpoint: "https://auth0.example.com/authorize",
}),
SSO_PROVIDERS.OKTA, // Auth0 URLs are detected as Okta provider
);
testProviderDetection("generic", createGenericSSOData(), SSO_PROVIDERS.GENERIC);
});
describe("Role Mappings", () => {
it("processes role mappings with all roles assigned", async () => {
const ssoData = createRoleMappingsSSOData();
setupMocks({
useSSOSettings: { data: ssoData, isLoading: false, error: null },
});
renderComponent();
await waitFor(() => {
expect(mockForm.setFieldsValue).toHaveBeenCalledWith({
sso_provider: SSO_PROVIDERS.GOOGLE,
...ssoData.values,
use_role_mappings: true,
group_claim: "groups",
default_role: "internal_user",
proxy_admin_teams: "admin-group",
admin_viewer_teams: "viewer-group",
internal_user_teams: "user-group",
internal_viewer_teams: "readonly-group",
});
});
});
it("handles empty role mapping arrays", async () => {
const ssoData = createRoleMappingsSSOData({
proxy_admin: [],
proxy_admin_viewer: [],
internal_user_viewer: [],
});
setupMocks({
useSSOSettings: { data: ssoData, isLoading: false, error: null },
});
renderComponent();
await waitFor(() => {
expect(mockForm.setFieldsValue).toHaveBeenCalledWith({
sso_provider: SSO_PROVIDERS.GOOGLE,
...ssoData.values,
use_role_mappings: true,
group_claim: "groups",
default_role: "internal_user",
proxy_admin_teams: "",
admin_viewer_teams: "",
internal_user_teams: "user-group",
internal_viewer_teams: "",
});
});
});
});
describe("Initialization Guards", () => {
it("resets form before setting values", async () => {
const ssoData = createGoogleSSOData();
setupMocks({
useSSOSettings: { data: ssoData, isLoading: false, error: null },
});
renderComponent();
await waitFor(() => {
expect(mockForm.resetFields).toHaveBeenCalled();
expect(mockForm.setFieldsValue).toHaveBeenCalled();
});
});
it("skips initialization when modal is not visible", () => {
const ssoData = createGoogleSSOData();
setupMocks({
useSSOSettings: { data: ssoData, isLoading: false, error: null },
});
renderComponent({ isVisible: false });
expect(mockForm.setFieldsValue).not.toHaveBeenCalled();
});
it("skips initialization when SSO data is unavailable", () => {
setupMocks({
useSSOSettings: { data: null, isLoading: false, error: null },
});
renderComponent();
expect(mockForm.setFieldsValue).not.toHaveBeenCalled();
});
});
});
describe("Error Handling", () => {
it("handles form submission errors with undefined error message", async () => {
const error = new Error("Network error");
const mockMutateAsync = vi.fn().mockImplementation((payload, options) => {
options.onError(error);
return Promise.reject(error);
});
setupMocks({
useEditSSOSettings: { mutateAsync: mockMutateAsync, isPending: false },
});
(parseErrorMessage as any).mockReturnValue(undefined);
renderComponent();
fireEvent.click(screen.getByTestId(TEST_IDS.TRIGGER_FORM_SUBMIT));
expect(NotificationsManager.fromBackend).toHaveBeenCalledWith(`${TEST_DATA.ERROR_MESSAGE_PREFIX} undefined`);
});
it("handles form submission with malformed data", async () => {
const mockMutateAsync = vi.fn().mockImplementation((payload, options) => {
options.onError(new Error("Invalid data"));
return Promise.reject(new Error("Invalid data"));
});
setupMocks({
useEditSSOSettings: { mutateAsync: mockMutateAsync, isPending: false },
});
(processSSOSettingsPayload as any).mockImplementation(() => {
throw new Error("Processing failed");
});
renderComponent();
fireEvent.click(screen.getByTestId(TEST_IDS.TRIGGER_FORM_SUBMIT));
expect(processSSOSettingsPayload).toHaveBeenCalled();
expect(mockMutateAsync).not.toHaveBeenCalled();
});
});
describe("Edge Cases", () => {
it("handles role mappings with undefined roles object", async () => {
const ssoData = createGoogleSSOData({
role_mappings: {
group_claim: "groups",
default_role: "internal_user",
// roles is undefined
},
});
setupMocks({
useSSOSettings: { data: ssoData, isLoading: false, error: null },
});
renderComponent();
await waitFor(() => {
expect(mockForm.setFieldsValue).toHaveBeenCalledWith({
sso_provider: SSO_PROVIDERS.GOOGLE,
...ssoData.values,
use_role_mappings: true,
group_claim: "groups",
default_role: "internal_user",
proxy_admin_teams: "",
admin_viewer_teams: "",
internal_user_teams: "",
internal_viewer_teams: "",
});
});
});
it("handles provider detection with partial SSO data", async () => {
const ssoData = createSSOData({
// Only has generic fields, no specific provider identifiers
generic_client_id: "test-id",
generic_authorization_endpoint: "https://unknown.provider.com/auth",
});
setupMocks({
useSSOSettings: { data: ssoData, isLoading: false, error: null },
});
renderComponent();
await waitFor(() => {
expect(mockForm.setFieldsValue).toHaveBeenCalledWith({
sso_provider: SSO_PROVIDERS.GENERIC,
...ssoData.values,
});
});
});
it("handles form submission when processing throws error", async () => {
setupMocks({
useEditSSOSettings: { mutateAsync: vi.fn(), isPending: false },
});
(processSSOSettingsPayload as any).mockImplementation(() => {
throw new Error("Processing error");
});
renderComponent();
expect(() => {
fireEvent.click(screen.getByTestId(TEST_IDS.TRIGGER_FORM_SUBMIT));
}).not.toThrow();
expect(processSSOSettingsPayload).toHaveBeenCalled();
});
});
});

View file

@ -88,17 +88,22 @@ const EditSSOSettingsModal: React.FC<EditSSOSettingsModalProps> = ({ isVisible,
// Enhanced form submission handler
const handleFormSubmit = async (formValues: Record<string, any>) => {
const payload = processSSOSettingsPayload(formValues);
try {
const payload = processSSOSettingsPayload(formValues);
await mutateAsync(payload, {
onSuccess: () => {
NotificationsManager.success("SSO settings updated successfully");
onSuccess();
},
onError: (error) => {
NotificationsManager.fromBackend("Failed to save SSO settings: " + parseErrorMessage(error));
},
});
await mutateAsync(payload, {
onSuccess: () => {
NotificationsManager.success("SSO settings updated successfully");
onSuccess();
},
onError: (error) => {
NotificationsManager.fromBackend("Failed to save SSO settings: " + parseErrorMessage(error));
},
});
} catch (error) {
// Handle processing errors gracefully
NotificationsManager.fromBackend("Failed to process SSO settings: " + parseErrorMessage(error));
}
};
const handleCancel = () => {

View file

@ -0,0 +1,222 @@
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import SSOSettingsLoadingSkeleton from "./SSOSettingsLoadingSkeleton";
// Mock lucide-react icons
vi.mock("lucide-react", () => ({
Shield: ({ className }: any) => <div data-testid="shield-icon" className={className} />,
}));
// Mock Ant Design components
vi.mock("antd", () => ({
Card: ({ children, ...props }: any) => (
<div data-testid="card" {...props}>
{children}
</div>
),
Descriptions: Object.assign(
({ children, bordered, column, ...props }: any) => (
<div data-testid="descriptions" data-bordered={bordered} data-column={JSON.stringify(column)} {...props}>
{children}
</div>
),
{
Item: ({ children, label, ...props }: any) => (
<div data-testid="descriptions-item" {...props}>
<div data-testid="descriptions-item-label">{label}</div>
<div data-testid="descriptions-item-content">{children}</div>
</div>
),
},
),
Typography: {
Title: ({ children, level, ...props }: any) => (
<div data-testid="typography-title" data-level={level} {...props}>
{children}
</div>
),
Text: ({ children, type, ...props }: any) => (
<div data-testid="typography-text" data-type={type} {...props}>
{children}
</div>
),
},
Space: ({ children, direction, size, className, ...props }: any) => (
<div data-testid="space" data-direction={direction} data-size={size} className={className} {...props}>
{children}
</div>
),
Skeleton: {
Button: ({ active, size, style, ...props }: any) => (
<div
data-testid="skeleton-button"
data-active={active}
data-size={size}
data-style={JSON.stringify(style)}
{...props}
>
Button Skeleton
</div>
),
Node: ({ active, style, ...props }: any) => (
<div data-testid="skeleton-node" data-active={active} data-style={JSON.stringify(style)} {...props}>
Node Skeleton
</div>
),
},
}));
describe("SSOSettingsLoadingSkeleton", () => {
it("should render without crashing", () => {
expect(() => render(<SSOSettingsLoadingSkeleton />)).not.toThrow();
});
it("should render Card component", () => {
render(<SSOSettingsLoadingSkeleton />);
expect(screen.getByTestId("card")).toBeInTheDocument();
});
it("should render Space component with correct props", () => {
render(<SSOSettingsLoadingSkeleton />);
const space = screen.getByTestId("space");
expect(space).toBeInTheDocument();
expect(space).toHaveAttribute("data-direction", "vertical");
expect(space).toHaveAttribute("data-size", "large");
expect(space).toHaveClass("w-full");
});
describe("Header Section", () => {
it("should render Shield icon", () => {
render(<SSOSettingsLoadingSkeleton />);
const shieldIcon = screen.getByTestId("shield-icon");
expect(shieldIcon).toBeInTheDocument();
expect(shieldIcon).toHaveClass("w-6 h-6 text-gray-400");
});
it("should render title with correct text and level", () => {
render(<SSOSettingsLoadingSkeleton />);
const title = screen.getByTestId("typography-title");
expect(title).toBeInTheDocument();
expect(title).toHaveAttribute("data-level", "3");
expect(title).toHaveTextContent("SSO Configuration");
});
it("should render subtitle text", () => {
render(<SSOSettingsLoadingSkeleton />);
const text = screen.getByTestId("typography-text");
expect(text).toBeInTheDocument();
expect(text).toHaveAttribute("data-type", "secondary");
expect(text).toHaveTextContent("Manage Single Sign-On authentication settings");
});
it("should render two skeleton buttons with correct styles", () => {
render(<SSOSettingsLoadingSkeleton />);
const buttons = screen.getAllByTestId("skeleton-button");
expect(buttons).toHaveLength(2);
// First button
expect(buttons[0]).toHaveAttribute("data-active", "true");
expect(buttons[0]).toHaveAttribute("data-size", "default");
expect(buttons[0]).toHaveAttribute("data-style", JSON.stringify({ width: 170, height: 32 }));
// Second button
expect(buttons[1]).toHaveAttribute("data-active", "true");
expect(buttons[1]).toHaveAttribute("data-size", "default");
expect(buttons[1]).toHaveAttribute("data-style", JSON.stringify({ width: 190, height: 32 }));
});
});
describe("Descriptions Table", () => {
it("should render Descriptions component with bordered prop", () => {
render(<SSOSettingsLoadingSkeleton />);
const descriptions = screen.getByTestId("descriptions");
expect(descriptions).toBeInTheDocument();
expect(descriptions).toHaveAttribute("data-bordered", "true");
});
it("should apply correct column configuration", () => {
render(<SSOSettingsLoadingSkeleton />);
const descriptions = screen.getByTestId("descriptions");
const expectedColumn = {
xxl: 1,
xl: 1,
lg: 1,
md: 1,
sm: 1,
xs: 1,
};
expect(descriptions).toHaveAttribute("data-column", JSON.stringify(expectedColumn));
});
it("should render exactly 5 description items", () => {
render(<SSOSettingsLoadingSkeleton />);
const items = screen.getAllByTestId("descriptions-item");
expect(items).toHaveLength(5);
});
describe("Description Items Structure", () => {
it("should render exactly 10 skeleton nodes total", () => {
render(<SSOSettingsLoadingSkeleton />);
const skeletonNodes = screen.getAllByTestId("skeleton-node");
expect(skeletonNodes).toHaveLength(10);
});
it("should render 5 skeleton nodes for labels with width 80", () => {
render(<SSOSettingsLoadingSkeleton />);
const skeletonNodes = screen.getAllByTestId("skeleton-node");
const labelNodes = skeletonNodes.filter(
(node) => node.getAttribute("data-style") === JSON.stringify({ width: 80, height: 16 }),
);
expect(labelNodes).toHaveLength(5);
labelNodes.forEach((node) => {
expect(node).toHaveAttribute("data-active", "true");
});
});
it("should render skeleton nodes for content with correct widths", () => {
render(<SSOSettingsLoadingSkeleton />);
const skeletonNodes = screen.getAllByTestId("skeleton-node");
// Expected content widths: [100, 200, 250, 180, 220]
const expectedWidths = [100, 200, 250, 180, 220];
expectedWidths.forEach((width) => {
const contentNode = skeletonNodes.find(
(node) => node.getAttribute("data-style") === JSON.stringify({ width, height: 16 }),
);
expect(contentNode).toBeInTheDocument();
expect(contentNode).toHaveAttribute("data-active", "true");
});
});
});
});
describe("Accessibility and Structure", () => {
it("should have proper semantic structure", () => {
render(<SSOSettingsLoadingSkeleton />);
// Card contains Space
const card = screen.getByTestId("card");
const space = screen.getByTestId("space");
expect(card).toContainElement(space);
// Space contains header section and descriptions
const descriptions = screen.getByTestId("descriptions");
expect(space).toContainElement(descriptions);
});
it("should render all skeleton elements as active", () => {
render(<SSOSettingsLoadingSkeleton />);
const skeletonNodes = screen.getAllByTestId("skeleton-node");
const skeletonButtons = screen.getAllByTestId("skeleton-button");
skeletonNodes.forEach((node) => {
expect(node).toHaveAttribute("data-active", "true");
});
skeletonButtons.forEach((button) => {
expect(button).toHaveAttribute("data-active", "true");
});
});
});
});

View file

@ -0,0 +1,522 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { VectorStore } from "./types";
import VectorStoreSelector from "./VectorStoreSelector";
// Mock dependencies
const mockVectorStoreListCall = vi.fn();
vi.mock("../networking", () => ({
vectorStoreListCall: (...args: any[]) => mockVectorStoreListCall(...args),
}));
// Mock antd Select component
vi.mock("antd", () => ({
Select: vi.fn(),
}));
// Import the mocked Select
import { Select as MockedSelect } from "antd";
// Configure the mock to render a simple div with data attributes
(MockedSelect as any).mockImplementation((props: any) => {
const {
onChange,
value,
placeholder,
loading,
className,
disabled,
options,
mode,
showSearch,
optionFilterProp,
style,
} = props;
return (
<div
data-testid="vector-store-select"
data-loading={loading}
data-disabled={disabled}
data-mode={mode}
data-show-search={showSearch}
data-option-filter-prop={optionFilterProp}
data-placeholder={placeholder}
data-value={value !== undefined ? JSON.stringify(value) : undefined}
data-options={JSON.stringify(options)}
className={className}
style={style}
onClick={(e: any) => {
// For testing purposes, allow simulating different selection behaviors
// The test can control this by setting data attributes on the element
const testSelection = e.target.getAttribute("data-test-selection");
if (testSelection && onChange) {
onChange(JSON.parse(testSelection));
} else if (onChange && options?.length > 0) {
// Default behavior: select first option
onChange([options[0].value]);
}
}}
>
{options?.map((opt: any) => (
<div
key={opt.value}
data-option-value={opt.value}
data-option-label={opt.label}
data-option-title={opt.title}
data-testid={`option-${opt.value}`}
>
{opt.label}
</div>
))}
</div>
);
});
// Test helpers
const mockOnChange = vi.fn();
const mockAccessToken = "test-token";
const mockVectorStores: VectorStore[] = [
{
vector_store_id: "store-1",
custom_llm_provider: "openai",
vector_store_name: "My Store",
vector_store_description: "A test store",
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
},
{
vector_store_id: "store-2",
custom_llm_provider: "azure",
vector_store_name: "Another Store",
vector_store_description: "Another test store",
created_at: "2024-01-02T00:00:00Z",
updated_at: "2024-01-02T00:00:00Z",
},
{
vector_store_id: "store-3",
custom_llm_provider: "pg_vector",
// No vector_store_name to test fallback to vector_store_id
vector_store_description: "Store without name",
created_at: "2024-01-03T00:00:00Z",
updated_at: "2024-01-03T00:00:00Z",
},
];
const defaultProps = {
onChange: mockOnChange,
accessToken: mockAccessToken,
};
// Helper functions
const renderComponent = (props = {}) => {
return render(<VectorStoreSelector {...defaultProps} {...props} />);
};
const waitForDataFetch = async () => {
await waitFor(() => {
expect(mockVectorStoreListCall).toHaveBeenCalled();
});
};
const getSelectElement = () => screen.getByTestId("vector-store-select");
const getOptionElements = () =>
screen.getAllByTestId(/^vector-store-select/).filter((el) => el.hasAttribute("data-option-value"));
describe("VectorStoreSelector", () => {
beforeEach(() => {
vi.clearAllMocks();
mockVectorStoreListCall.mockResolvedValue({
data: mockVectorStores,
});
});
describe("Rendering", () => {
it("should render the select component", () => {
renderComponent();
expect(getSelectElement()).toBeInTheDocument();
});
it("should render with default placeholder", () => {
renderComponent();
const select = getSelectElement();
expect(select).toHaveAttribute("data-placeholder", "Select vector stores");
});
it("should render with custom placeholder", () => {
renderComponent({ placeholder: "Choose stores" });
const select = getSelectElement();
expect(select).toHaveAttribute("data-placeholder", "Choose stores");
});
it("should apply custom className", () => {
renderComponent({ className: "custom-class" });
const select = getSelectElement();
expect(select).toHaveClass("custom-class");
});
it("should render with disabled state", () => {
renderComponent({ disabled: true });
const select = getSelectElement();
expect(select).toHaveAttribute("data-disabled", "true");
});
it("should render with enabled state by default", () => {
renderComponent();
const select = getSelectElement();
expect(select).toHaveAttribute("data-disabled", "false");
});
it("should render with multiple mode", () => {
renderComponent();
const select = getSelectElement();
expect(select).toHaveAttribute("data-mode", "multiple");
});
it("should render with showSearch enabled", () => {
renderComponent();
const select = getSelectElement();
expect(select).toHaveAttribute("data-show-search", "true");
});
it("should render with optionFilterProp set to label", () => {
renderComponent();
const select = getSelectElement();
expect(select).toHaveAttribute("data-option-filter-prop", "label");
});
it("should render with full width style", () => {
renderComponent();
const select = getSelectElement();
expect(select).toHaveStyle({ width: "100%" });
});
});
describe("Data fetching", () => {
it("should fetch vector stores on mount when accessToken is provided", async () => {
renderComponent();
await waitFor(() => {
expect(mockVectorStoreListCall).toHaveBeenCalledWith(mockAccessToken);
});
});
it("should not fetch vector stores when accessToken is falsy", () => {
const { rerender } = render(<VectorStoreSelector {...defaultProps} accessToken="" />);
expect(mockVectorStoreListCall).not.toHaveBeenCalled();
rerender(<VectorStoreSelector {...defaultProps} accessToken={null as any} />);
expect(mockVectorStoreListCall).not.toHaveBeenCalled();
rerender(<VectorStoreSelector {...defaultProps} accessToken={undefined as any} />);
expect(mockVectorStoreListCall).not.toHaveBeenCalled();
});
it("should fetch vector stores again when accessToken changes", async () => {
const { rerender } = render(<VectorStoreSelector {...defaultProps} accessToken="token-1" />);
await waitFor(() => {
expect(mockVectorStoreListCall).toHaveBeenCalledWith("token-1");
});
vi.clearAllMocks();
rerender(<VectorStoreSelector {...defaultProps} accessToken="token-2" />);
await waitFor(() => {
expect(mockVectorStoreListCall).toHaveBeenCalledWith("token-2");
});
});
it("should set loading state while fetching", async () => {
let resolvePromise: (value: any) => void;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
mockVectorStoreListCall.mockReturnValue(promise);
renderComponent();
const select = getSelectElement();
expect(select).toHaveAttribute("data-loading", "true");
resolvePromise!({ data: mockVectorStores });
await waitFor(() => {
expect(select).toHaveAttribute("data-loading", "false");
});
});
it("should clear loading state after successful fetch", async () => {
renderComponent();
await waitForDataFetch();
const select = getSelectElement();
expect(select).toHaveAttribute("data-loading", "false");
});
it("should clear loading state after failed fetch", async () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
mockVectorStoreListCall.mockRejectedValueOnce(new Error("Network error"));
renderComponent();
await waitForDataFetch();
const select = getSelectElement();
expect(select).toHaveAttribute("data-loading", "false");
consoleErrorSpy.mockRestore();
});
});
describe("Options rendering", () => {
it("should render vector store options after successful fetch", async () => {
renderComponent();
await waitForDataFetch();
expect(screen.getByText("My Store (store-1)")).toBeInTheDocument();
expect(screen.getByText("Another Store (store-2)")).toBeInTheDocument();
expect(screen.getByText("store-3 (store-3)")).toBeInTheDocument();
});
it("should use vector_store_name when available for label", async () => {
renderComponent();
await waitForDataFetch();
const option1 = screen.getByText("My Store (store-1)");
expect(option1).toBeInTheDocument();
expect(option1).toHaveAttribute("data-option-title", "A test store");
});
it("should fallback to vector_store_id when vector_store_name is missing", async () => {
renderComponent();
await waitForDataFetch();
const option3 = screen.getByText("store-3 (store-3)");
expect(option3).toBeInTheDocument();
// When vector_store_name is missing, title uses vector_store_description if available, otherwise vector_store_id
expect(option3).toHaveAttribute("data-option-title", "Store without name");
});
it("should use vector_store_description as title when available", async () => {
renderComponent();
await waitForDataFetch();
const option1 = screen.getByText("My Store (store-1)");
expect(option1).toHaveAttribute("data-option-title", "A test store");
});
it("should fallback to vector_store_id as title when vector_store_description is missing", async () => {
const storesWithoutDescription: VectorStore[] = [
{
vector_store_id: "store-no-desc",
custom_llm_provider: "openai",
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
},
];
mockVectorStoreListCall.mockResolvedValueOnce({
data: storesWithoutDescription,
});
renderComponent();
await waitForDataFetch();
const option = screen.getByText("store-no-desc (store-no-desc)");
expect(option).toHaveAttribute("data-option-title", "store-no-desc");
});
it("should use vector_store_id as option value", async () => {
renderComponent();
await waitForDataFetch();
const option1 = screen.getByText("My Store (store-1)");
expect(option1).toHaveAttribute("data-option-value", "store-1");
});
it("should handle empty vector stores array", async () => {
mockVectorStoreListCall.mockResolvedValueOnce({
data: [],
});
renderComponent();
await waitForDataFetch();
const options = getOptionElements();
expect(options.length).toBe(0);
});
it("should handle response without data property", async () => {
mockVectorStoreListCall.mockResolvedValueOnce({});
renderComponent();
await waitForDataFetch();
const options = getOptionElements();
expect(options.length).toBe(0);
});
});
describe("Value prop", () => {
it("should set initial value when value prop is provided", async () => {
renderComponent({ value: ["store-1", "store-2"] });
await waitForDataFetch();
const select = getSelectElement();
const dataValue = select.getAttribute("data-value");
expect(dataValue).toBe(JSON.stringify(["store-1", "store-2"]));
});
it("should handle empty value array", async () => {
renderComponent({ value: [] });
await waitForDataFetch();
const select = getSelectElement();
const dataValue = select.getAttribute("data-value");
expect(dataValue).toBe(JSON.stringify([]));
});
it("should handle undefined value", async () => {
renderComponent({ value: undefined });
await waitForDataFetch();
const select = getSelectElement();
const dataValue = select.getAttribute("data-value");
expect(dataValue).toBeNull(); // undefined value results in no data-value attribute
});
});
describe("onChange callback", () => {
it("should call onChange when selection changes", async () => {
renderComponent();
await waitForDataFetch();
const select = getSelectElement();
// Simulate selecting store-1 by setting test data attribute
select.setAttribute("data-test-selection", '["store-1"]');
fireEvent.click(select);
expect(mockOnChange).toHaveBeenCalledWith(["store-1"]);
});
it("should call onChange with multiple selected values", async () => {
renderComponent();
await waitForDataFetch();
const select = getSelectElement();
// Simulate selecting multiple values
select.setAttribute("data-test-selection", '["store-1", "store-2"]');
fireEvent.click(select);
expect(mockOnChange).toHaveBeenCalledWith(["store-1", "store-2"]);
});
it("should call onChange when deselecting options", async () => {
renderComponent({ value: ["store-1", "store-2"] });
await waitForDataFetch();
const select = getSelectElement();
// Simulate deselecting store-1
select.setAttribute("data-test-selection", '["store-2"]');
fireEvent.click(select);
expect(mockOnChange).toHaveBeenCalledWith(["store-2"]);
});
});
describe("Error handling", () => {
it("should handle fetch errors gracefully", async () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const error = new Error("Network error");
mockVectorStoreListCall.mockRejectedValueOnce(error);
renderComponent();
await waitForDataFetch();
expect(consoleErrorSpy).toHaveBeenCalledWith("Error fetching vector stores:", error);
consoleErrorSpy.mockRestore();
});
it("should not crash when fetch throws non-Error", async () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
mockVectorStoreListCall.mockRejectedValueOnce("String error");
renderComponent();
await waitForDataFetch();
expect(consoleErrorSpy).toHaveBeenCalledWith("Error fetching vector stores:", "String error");
consoleErrorSpy.mockRestore();
});
it("should continue to work after error", async () => {
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
mockVectorStoreListCall.mockRejectedValueOnce(new Error("Network error"));
renderComponent();
await waitForDataFetch();
// Component should still render
expect(getSelectElement()).toBeInTheDocument();
consoleErrorSpy.mockRestore();
});
});
describe("Edge cases", () => {
it("should handle vector stores with all optional fields missing", async () => {
const minimalStores: VectorStore[] = [
{
vector_store_id: "minimal-store",
custom_llm_provider: "openai",
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
},
];
mockVectorStoreListCall.mockResolvedValueOnce({
data: minimalStores,
});
renderComponent();
await waitForDataFetch();
expect(screen.getByText("minimal-store (minimal-store)")).toBeInTheDocument();
const option = screen.getByText("minimal-store (minimal-store)");
expect(option).toHaveAttribute("data-option-title", "minimal-store");
});
it("should handle very long vector store names", async () => {
const longNameStores: VectorStore[] = [
{
vector_store_id: "store-long",
custom_llm_provider: "openai",
vector_store_name: "A".repeat(200),
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
},
];
mockVectorStoreListCall.mockResolvedValueOnce({
data: longNameStores,
});
renderComponent();
await waitForDataFetch();
const expectedLabel = `${"A".repeat(200)} (store-long)`;
expect(screen.getByText(expectedLabel)).toBeInTheDocument();
});
it("should handle special characters in vector store names", async () => {
const specialCharStores: VectorStore[] = [
{
vector_store_id: "store-special",
custom_llm_provider: "openai",
vector_store_name: 'Store & Co. <Test> "Quotes"',
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
},
];
mockVectorStoreListCall.mockResolvedValueOnce({
data: specialCharStores,
});
renderComponent();
await waitForDataFetch();
expect(screen.getByText(/Store & Co\. <Test> "Quotes"/)).toBeInTheDocument();
});
});
});

View file

@ -0,0 +1,415 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import VectorStoreTable from "./VectorStoreTable";
import { VectorStore } from "./types";
// Mock dependencies
const mockGetProviderLogoAndName = vi.fn();
const mockTableIconActionButton = vi.fn();
vi.mock("../provider_info_helpers", () => ({
getProviderLogoAndName: (...args: any[]) => mockGetProviderLogoAndName(...args),
}));
vi.mock("../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton", () => ({
default: (props: any) => {
mockTableIconActionButton(props);
return (
<button
data-testid={`action-button-${props.variant.toLowerCase()}`}
data-tooltip={props.tooltipText}
onClick={props.onClick}
disabled={props.disabled}
>
{props.variant}
</button>
);
},
}));
// Mock Tremor components to avoid complex styling issues
vi.mock("@tremor/react", () => ({
Table: ({ children, ...props }: any) => <table {...props}>{children}</table>,
TableHead: ({ children, ...props }: any) => <thead {...props}>{children}</thead>,
TableBody: ({ children, ...props }: any) => <tbody {...props}>{children}</tbody>,
TableRow: ({ children, ...props }: any) => <tr {...props}>{children}</tr>,
TableHeaderCell: ({ children, ...props }: any) => <th {...props}>{children}</th>,
TableCell: ({ children, ...props }: any) => <td {...props}>{children}</td>,
}));
// Mock antd Tooltip
vi.mock("antd", () => ({
Tooltip: ({ children, title }: any) => (
<div data-testid="tooltip" data-title={title}>
{children}
</div>
),
}));
// Mock Heroicons
vi.mock("@heroicons/react/outline", () => ({
ChevronDownIcon: (props: any) => <div data-testid="chevron-down" {...props} />,
ChevronUpIcon: (props: any) => <div data-testid="chevron-up" {...props} />,
SwitchVerticalIcon: (props: any) => <div data-testid="switch-vertical" {...props} />,
}));
// Test data
const mockVectorStores: VectorStore[] = [
{
vector_store_id: "short-id",
custom_llm_provider: "openai",
vector_store_name: "My OpenAI Store",
vector_store_description: "A store for OpenAI vectors",
created_at: "2024-01-15T10:30:00Z",
updated_at: "2024-01-15T11:00:00Z",
created_by: "user-1",
updated_by: "user-1",
},
{
vector_store_id: "very-long-vector-store-id-that-should-be-truncated",
custom_llm_provider: "azure",
vector_store_name: undefined, // Test missing name
vector_store_description: "A store for Azure vectors with a very long description that should show a tooltip",
created_at: "2024-01-10T09:15:00Z",
updated_at: "2024-01-12T14:20:00Z",
},
{
vector_store_id: "store-3",
custom_llm_provider: "pg_vector",
vector_store_name: "PostgreSQL Store",
vector_store_description: undefined, // Test missing description
created_at: "2024-01-05T08:00:00Z",
updated_at: "2024-01-08T16:45:00Z",
},
];
// Mock functions
const mockOnView = vi.fn();
const mockOnEdit = vi.fn();
const mockOnDelete = vi.fn();
const defaultProps = {
data: mockVectorStores,
onView: mockOnView,
onEdit: mockOnEdit,
onDelete: mockOnDelete,
};
// Helper function to render component
const renderComponent = (props = {}) => {
return render(<VectorStoreTable {...defaultProps} {...props} />);
};
describe("VectorStoreTable", () => {
beforeEach(() => {
vi.clearAllMocks();
// Setup default mock returns for getProviderLogoAndName
mockGetProviderLogoAndName.mockImplementation((provider: string) => {
const providerMap: Record<string, { displayName: string; logo: string }> = {
openai: { displayName: "OpenAI", logo: "/openai-logo.png" },
azure: { displayName: "Azure", logo: "/azure-logo.png" },
pg_vector: { displayName: "PostgreSQL Vector", logo: "/pg-logo.png" },
};
return providerMap[provider] || { displayName: provider, logo: "" };
});
});
describe("Rendering", () => {
it("should render the table with data", () => {
renderComponent();
expect(screen.getByRole("table")).toBeInTheDocument();
});
it("should render table headers", () => {
renderComponent();
expect(screen.getByText("Vector Store ID")).toBeInTheDocument();
expect(screen.getByText("Name")).toBeInTheDocument();
expect(screen.getByText("Description")).toBeInTheDocument();
expect(screen.getByText("Provider")).toBeInTheDocument();
expect(screen.getByText("Created At")).toBeInTheDocument();
expect(screen.getByText("Updated At")).toBeInTheDocument();
// Check that we have the expected number of header cells (6 data + 1 actions)
const headers = screen.getAllByRole("columnheader");
expect(headers).toHaveLength(7);
});
it("should render all vector store rows", () => {
renderComponent();
expect(screen.getAllByRole("row")).toHaveLength(mockVectorStores.length + 1); // +1 for header row
});
it("should render empty state when no data", () => {
renderComponent({ data: [] });
expect(screen.getByText("No vector stores found")).toBeInTheDocument();
});
});
describe("Vector Store ID Column", () => {
it("should render short vector store IDs fully", () => {
renderComponent();
expect(screen.getByText("short-id")).toBeInTheDocument();
});
it("should truncate long vector store IDs", () => {
renderComponent();
// Check that the truncated text is rendered (first 15 chars + ...)
const truncatedText = "very-long-vecto...";
expect(screen.getByText(truncatedText)).toBeInTheDocument();
});
it("should make vector store ID clickable", async () => {
const user = userEvent.setup();
renderComponent();
const idButton = screen.getByText("short-id");
await user.click(idButton);
expect(mockOnView).toHaveBeenCalledWith("short-id");
});
it("should have correct styling for vector store ID button", () => {
renderComponent();
const idButton = screen.getByText("short-id").closest("button");
expect(idButton).toHaveClass("font-mono", "text-blue-500", "bg-blue-50", "hover:bg-blue-100");
});
});
describe("Name Column", () => {
it("should render vector store name", () => {
renderComponent();
expect(screen.getByText("My OpenAI Store")).toBeInTheDocument();
});
it("should render fallback for missing name", () => {
renderComponent();
const fallbackElements = screen.getAllByText("-");
expect(fallbackElements.length).toBe(2); // One for missing name, one for missing description
});
it("should wrap name in tooltip", () => {
renderComponent();
const tooltips = screen.getAllByTestId("tooltip");
const nameTooltip = tooltips.find((t) => t.getAttribute("data-title") === "My OpenAI Store");
expect(nameTooltip).toBeInTheDocument();
});
});
describe("Description Column", () => {
it("should render vector store description", () => {
renderComponent();
expect(screen.getByText("A store for OpenAI vectors")).toBeInTheDocument();
});
it("should render fallback for missing description", () => {
renderComponent();
const fallbackElements = screen.getAllByText("-");
expect(fallbackElements.length).toBe(2); // One for missing name, one for missing description
});
it("should wrap description in tooltip", () => {
renderComponent();
const tooltips = screen.getAllByTestId("tooltip");
const descTooltip = tooltips.find(
(t) =>
t.getAttribute("data-title") ===
"A store for Azure vectors with a very long description that should show a tooltip",
);
expect(descTooltip).toBeInTheDocument();
});
});
describe("Provider Column", () => {
it("should render provider display name", () => {
renderComponent();
expect(screen.getByText("OpenAI")).toBeInTheDocument();
expect(screen.getByText("Azure")).toBeInTheDocument();
expect(screen.getByText("PostgreSQL Vector")).toBeInTheDocument();
});
it("should render provider logo when available", () => {
renderComponent();
const logos = screen.getAllByRole("img");
expect(logos).toHaveLength(3); // All providers have logos in our mock
expect(logos[0]).toHaveAttribute("src", "/openai-logo.png");
expect(logos[0]).toHaveAttribute("alt", "OpenAI");
});
it("should call getProviderLogoAndName for each provider", () => {
renderComponent();
expect(mockGetProviderLogoAndName).toHaveBeenCalledWith("openai");
expect(mockGetProviderLogoAndName).toHaveBeenCalledWith("azure");
expect(mockGetProviderLogoAndName).toHaveBeenCalledWith("pg_vector");
});
});
describe("Date Columns", () => {
it("should render created at dates", () => {
renderComponent();
const dateElements = screen.getAllByText(/1\/\d+\/2024/);
expect(dateElements.length).toBe(6); // 3 created_at + 3 updated_at dates
});
it("should render updated at dates", () => {
renderComponent();
const dateElements = screen.getAllByText(/1\/\d+\/2024/);
expect(dateElements.length).toBe(6); // 3 created_at + 3 updated_at dates
});
});
describe("Actions Column", () => {
it("should render edit and delete action buttons for each row", () => {
renderComponent();
expect(screen.getAllByTestId("action-button-edit")).toHaveLength(mockVectorStores.length);
expect(screen.getAllByTestId("action-button-delete")).toHaveLength(mockVectorStores.length);
});
it("should call onEdit when edit button is clicked", async () => {
const user = userEvent.setup();
renderComponent();
const editButtons = screen.getAllByTestId("action-button-edit");
await user.click(editButtons[0]);
expect(mockOnEdit).toHaveBeenCalledWith("short-id");
});
it("should call onDelete when delete button is clicked", async () => {
const user = userEvent.setup();
renderComponent();
const deleteButtons = screen.getAllByTestId("action-button-delete");
await user.click(deleteButtons[0]);
expect(mockOnDelete).toHaveBeenCalledWith("short-id");
});
it("should pass correct props to TableIconActionButton", () => {
renderComponent();
expect(mockTableIconActionButton).toHaveBeenCalledWith(
expect.objectContaining({
variant: "Edit",
tooltipText: "Edit vector store",
onClick: expect.any(Function),
}),
);
expect(mockTableIconActionButton).toHaveBeenCalledWith(
expect.objectContaining({
variant: "Delete",
tooltipText: "Delete vector store",
onClick: expect.any(Function),
}),
);
});
});
describe("Sorting", () => {
it("should initialize with created_at descending sort", () => {
renderComponent();
// The table should initialize with sorting state
expect(screen.getByTestId("chevron-down")).toBeInTheDocument();
});
it("should render sort icons for sortable columns", () => {
renderComponent();
// Should have sort icons for Created At and Updated At columns
const sortIcons = screen.getAllByTestId(/^chevron-(up|down)$|^switch-vertical$/);
expect(sortIcons.length).toBeGreaterThan(0);
});
it("should make header cells clickable for sorting", () => {
renderComponent();
const headerCells = screen.getAllByRole("columnheader");
const sortableHeaders = headerCells.filter((cell) => cell.textContent !== "");
expect(sortableHeaders.length).toBeGreaterThan(0);
});
it("should show ascending icon when sorted ascending", () => {
renderComponent();
// Initially shows descending, but we can test the logic by checking the icons are present
expect(screen.getByTestId("chevron-down")).toBeInTheDocument();
});
});
describe("Styling and Layout", () => {
it("should apply correct CSS classes to table container", () => {
renderComponent();
const tableContainer = screen.getByRole("table").parentElement?.parentElement;
expect(tableContainer).toHaveClass("rounded-lg", "custom-border", "relative");
});
it("should apply overflow styling to table wrapper", () => {
renderComponent();
const tableWrapper = screen.getByRole("table").parentElement;
expect(tableWrapper).toHaveClass("overflow-x-auto");
});
it("should apply sticky styling to actions column", () => {
renderComponent();
const headerCells = screen.getAllByRole("columnheader");
const actionsHeader = headerCells[headerCells.length - 1];
expect(actionsHeader).toHaveClass("sticky", "right-0", "bg-white");
});
it("should apply sticky styling to action cells", () => {
renderComponent();
const rows = screen.getAllByRole("row").slice(1); // Skip header row
rows.forEach((row) => {
const cells = row.querySelectorAll("td");
const lastCell = cells[cells.length - 1];
expect(lastCell).toHaveClass("sticky", "right-0", "bg-white");
});
});
});
describe("Table Row Styling", () => {
it("should apply correct height to table rows", () => {
renderComponent();
const rows = screen.getAllByRole("row").slice(1); // Skip header row
rows.forEach((row) => {
expect(row).toHaveClass("h-8");
});
});
it("should apply correct cell padding and styling", () => {
renderComponent();
const cells = screen.getAllByRole("cell");
cells.forEach((cell) => {
expect(cell).toHaveClass("py-0.5", "max-h-8", "overflow-hidden", "text-ellipsis", "whitespace-nowrap");
});
});
});
describe("Empty State", () => {
it("should render single row with centered message when no data", () => {
renderComponent({ data: [] });
const rows = screen.getAllByRole("row");
expect(rows).toHaveLength(2); // Header + empty state row
expect(screen.getByText("No vector stores found")).toBeInTheDocument();
});
it("should span all columns in empty state", () => {
renderComponent({ data: [] });
const emptyCell = screen.getByText("No vector stores found").closest("td");
expect(emptyCell).toHaveAttribute("colSpan", "7"); // 6 data columns + 1 actions column
});
});
describe("Data Edge Cases", () => {
it("should handle vector stores with minimal data", () => {
const minimalData: VectorStore[] = [
{
vector_store_id: "minimal",
custom_llm_provider: "test",
created_at: "2024-01-01T00:00:00Z",
updated_at: "2024-01-01T00:00:00Z",
},
];
renderComponent({ data: minimalData });
expect(screen.getByText("minimal")).toBeInTheDocument();
expect(screen.getAllByText("-")).toHaveLength(2); // Name and description fallbacks
});
it("should handle single vector store", () => {
const singleData = [mockVectorStores[0]];
renderComponent({ data: singleData });
expect(screen.getAllByRole("row")).toHaveLength(2); // Header + 1 data row
});
});
});

View file

@ -44,6 +44,78 @@ describe("cookieUtils", () => {
expect(getCookie("token")).toBeNull();
});
it("should return early when window is undefined (server-side rendering)", () => {
const originalWindow = global.window;
const originalDocument = global.document;
// Mock server-side environment
delete (global as any).window;
delete (global as any).document;
// This should not throw an error and should return early
expect(() => clearTokenCookies()).not.toThrow();
// Restore globals
global.window = originalWindow;
global.document = originalDocument;
});
it("should return early when document is undefined (server-side rendering)", () => {
const originalDocument = global.document;
// Mock server-side environment where document is undefined
delete (global as any).document;
// This should not throw an error and should return early
expect(() => clearTokenCookies()).not.toThrow();
// Restore globals
global.document = originalDocument;
});
it("should add current path directory to paths array when different from root and /ui", () => {
// Mock window.location.pathname using vi.stubGlobal
const originalLocation = window.location;
vi.stubGlobal('location', { ...originalLocation, pathname: '/custom/path/page.html' });
// Spy on document.cookie to verify the paths being used
const cookieSpy = vi.spyOn(document, 'cookie', 'set');
clearTokenCookies();
// Verify that cookies were cleared for /custom/path/ path
expect(cookieSpy).toHaveBeenCalledWith(
expect.stringContaining('path=/custom/path/')
);
vi.restoreAllMocks();
});
it("should not add current path directory when it's already in paths array", () => {
// Mock window.location.pathname using vi.stubGlobal
const originalLocation = window.location;
vi.stubGlobal('location', { ...originalLocation, pathname: '/' });
// Spy on document.cookie to count calls
const cookieSpy = vi.spyOn(document, 'cookie', 'set');
clearTokenCookies();
// Count how many times each path was used
const rootPathCalls = cookieSpy.mock.calls.filter(call =>
call[0].includes('path=/;') || call[0].includes('path=/ ')
);
const uiPathCalls = cookieSpy.mock.calls.filter(call =>
call[0].includes('path=/ui;') || call[0].includes('path=/ui ')
);
// Should have calls for root and /ui paths, but not duplicate root
expect(rootPathCalls.length).toBeGreaterThan(0);
expect(uiPathCalls.length).toBeGreaterThan(0);
vi.restoreAllMocks();
});
});
describe("getCookie", () => {

View file

@ -0,0 +1,78 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { fetchProxySettings } from "./proxyUtils";
import { getProxyUISettings } from "@/components/networking";
vi.mock("@/components/networking", () => ({
getProxyUISettings: vi.fn(),
}));
describe("fetchProxySettings", () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
it("should return null when accessToken is null", async () => {
const result = await fetchProxySettings(null);
expect(result).toBeNull();
expect(getProxyUISettings).not.toHaveBeenCalled();
});
it("should return null when accessToken is undefined", async () => {
const result = await fetchProxySettings(undefined as any);
expect(result).toBeNull();
expect(getProxyUISettings).not.toHaveBeenCalled();
});
it("should return proxy settings when getProxyUISettings succeeds", async () => {
const mockProxySettings = { someSetting: "value", anotherSetting: 123 };
const accessToken = "test-token";
vi.mocked(getProxyUISettings).mockResolvedValue(mockProxySettings);
const result = await fetchProxySettings(accessToken);
expect(result).toEqual(mockProxySettings);
expect(getProxyUISettings).toHaveBeenCalledOnce();
expect(getProxyUISettings).toHaveBeenCalledWith(accessToken);
});
it("should return null and log error when getProxyUISettings throws", async () => {
const accessToken = "test-token";
const mockError = new Error("Network error");
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
vi.mocked(getProxyUISettings).mockRejectedValue(mockError);
const result = await fetchProxySettings(accessToken);
expect(result).toBeNull();
expect(getProxyUISettings).toHaveBeenCalledOnce();
expect(getProxyUISettings).toHaveBeenCalledWith(accessToken);
expect(consoleSpy).toHaveBeenCalledWith("Error fetching proxy settings:", mockError);
consoleSpy.mockRestore();
});
it("should return null and log error when getProxyUISettings throws a string", async () => {
const accessToken = "test-token";
const mockError = "String error";
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
vi.mocked(getProxyUISettings).mockRejectedValue(mockError);
const result = await fetchProxySettings(accessToken);
expect(result).toBeNull();
expect(getProxyUISettings).toHaveBeenCalledOnce();
expect(getProxyUISettings).toHaveBeenCalledWith(accessToken);
expect(consoleSpy).toHaveBeenCalledWith("Error fetching proxy settings:", mockError);
consoleSpy.mockRestore();
});
});

View file

@ -5,6 +5,15 @@ describe("formatLabel", () => {
it("should format label", () => {
expect(formatLabel("test_label")).toBe("Test Label");
});
it("should return empty string when text is empty string", () => {
expect(formatLabel("")).toBe("");
});
it("should return the same value when text is falsy", () => {
expect(formatLabel(null as any)).toBe(null);
expect(formatLabel(undefined as any)).toBe(undefined);
});
});
describe("truncateString", () => {
@ -26,4 +35,16 @@ describe("formItemValidateJSON", () => {
it("should reject with an error message for invalid JSON", async () => {
await expect(formItemValidateJSON({}, "invalid JSON")).rejects.toBe("Please enter valid JSON");
});
it("should resolve when value is empty string", async () => {
await expect(formItemValidateJSON({}, "")).resolves.toBeUndefined();
});
it("should resolve when value is null", async () => {
await expect(formItemValidateJSON({}, null as any)).resolves.toBeUndefined();
});
it("should resolve when value is undefined", async () => {
await expect(formItemValidateJSON({}, undefined as any)).resolves.toBeUndefined();
});
});