import type { ProxyModel } from "@/app/(dashboard)/hooks/models/useModels"; import type { Organization } from "@/components/networking"; import { screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderWithProviders } from "../../../tests/test-utils"; import { ModelSelect } from "./ModelSelect"; vi.mock("@/app/(dashboard)/hooks/models/useModels", () => ({ useAllProxyModels: vi.fn(), })); vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({ useTeam: vi.fn(), })); vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({ useOrganization: vi.fn(), })); vi.mock("@/app/(dashboard)/hooks/users/useCurrentUser", () => ({ useCurrentUser: vi.fn(), })); vi.mock("antd", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, Select: ({ value, onChange, options, "data-testid": dataTestId, allowClear, maxTagCount, maxTagPlaceholder, mode, ...props }: any) => { // Simulate maxTagCount responsive behavior - if value length > 5, call maxTagPlaceholder const shouldShowPlaceholder = maxTagCount === "responsive" && Array.isArray(value) && value.length > 5; const visibleValues = shouldShowPlaceholder ? value.slice(0, 5) : value; const omittedValues = shouldShowPlaceholder ? value.slice(5).map((v: string) => ({ value: v, label: v })) : []; return (
{shouldShowPlaceholder && maxTagPlaceholder && (
{maxTagPlaceholder(omittedValues)}
)}
); }, Skeleton: { Input: ({ active, block }: any) =>
, }, Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}, }; }); import { useAllProxyModels } from "@/app/(dashboard)/hooks/models/useModels"; import { useOrganization } from "@/app/(dashboard)/hooks/organizations/useOrganizations"; import { useTeam } from "@/app/(dashboard)/hooks/teams/useTeams"; import { useCurrentUser } from "@/app/(dashboard)/hooks/users/useCurrentUser"; const mockUseAllProxyModels = vi.mocked(useAllProxyModels); const mockUseTeam = vi.mocked(useTeam); const mockUseOrganization = vi.mocked(useOrganization); const mockUseCurrentUser = vi.mocked(useCurrentUser); const createMockOrganization = (models: string[]): Organization => ({ organization_id: "org-1", organization_alias: "Test Org", budget_id: "budget-1", metadata: {}, models, spend: 0, model_spend: {}, created_at: "2024-01-01", created_by: "user-1", updated_at: "2024-01-01", updated_by: "user-1", litellm_budget_table: null, teams: null, users: null, members: null, }); describe("ModelSelect", () => { const mockProxyModels: ProxyModel[] = [ { id: "gpt-4", object: "model", created: 1234567890, owned_by: "openai" }, { id: "claude-3", object: "model", created: 1234567890, owned_by: "anthropic" }, { id: "openai/*", object: "model", created: 1234567890, owned_by: "openai" }, { id: "anthropic/*", object: "model", created: 1234567890, owned_by: "anthropic" }, ]; const mockOnChange = vi.fn(); beforeEach(() => { vi.clearAllMocks(); mockUseAllProxyModels.mockReturnValue({ data: { data: mockProxyModels }, isLoading: false, } as any); mockUseTeam.mockReturnValue({ data: undefined, isLoading: false, } as any); mockUseOrganization.mockReturnValue({ data: undefined, isLoading: false, } as any); mockUseCurrentUser.mockReturnValue({ data: { models: [] }, isLoading: false, } as any); }); it("should render with all option groups", async () => { renderWithProviders( , ); await waitFor(() => { expect(screen.getByTestId("model-select")).toBeInTheDocument(); expect(screen.getByText("gpt-4")).toBeInTheDocument(); expect(screen.getByText("claude-3")).toBeInTheDocument(); expect(screen.getByText("All Openai models")).toBeInTheDocument(); expect(screen.getByText("All Anthropic models")).toBeInTheDocument(); }); }); it("should show skeleton loader when any data is loading", () => { const loadingScenarios = [ { hook: mockUseAllProxyModels, context: "user" as const }, { hook: mockUseTeam, context: "team" as const, props: { teamID: "team-1" } }, { hook: mockUseOrganization, context: "organization" as const, props: { organizationID: "org-1" } }, { hook: mockUseCurrentUser, context: "user" as const }, ]; loadingScenarios.forEach(({ hook, context, props = {} }) => { hook.mockReturnValue({ data: undefined, isLoading: true, } as any); const { unmount } = renderWithProviders( , ); expect(screen.getByTestId("skeleton-input")).toBeInTheDocument(); unmount(); }); }); it("should handle model selection and onChange", async () => { const user = userEvent.setup(); renderWithProviders( , ); await waitFor(() => { expect(screen.getByTestId("model-select")).toBeInTheDocument(); }); const select = screen.getByRole("listbox"); await user.selectOptions(select, "gpt-4"); expect(mockOnChange).toHaveBeenCalledWith(["gpt-4"]); await user.selectOptions(select, ["gpt-4", "claude-3"]); expect(mockOnChange).toHaveBeenCalled(); }); it("should handle special options correctly", async () => { const user = userEvent.setup(); mockUseOrganization.mockReturnValue({ data: createMockOrganization(["all-proxy-models"]), isLoading: false, } as any); renderWithProviders( , ); await waitFor(() => { expect(screen.getByText("All Proxy Models")).toBeInTheDocument(); expect(screen.getByText("No Default Models")).toBeInTheDocument(); }); const select = screen.getByRole("listbox"); await user.selectOptions(select, ["all-proxy-models", "no-default-models"]); expect(mockOnChange).toHaveBeenCalledWith(["no-default-models"]); }); it("should disable models when special option is selected", async () => { renderWithProviders( , ); await waitFor(() => { expect(screen.getByRole("option", { name: "gpt-4" })).toBeDisabled(); expect(screen.getByRole("option", { name: "All Openai models" })).toBeDisabled(); }); }); it("should filter models based on context", async () => { const testCases = [ { name: "user context with includeUserModels", context: "user" as const, options: { includeUserModels: true }, setup: () => { mockUseCurrentUser.mockReturnValue({ data: { models: ["gpt-4"] }, isLoading: false, } as any); }, expectedVisible: ["gpt-4"], expectedHidden: ["claude-3"], }, { name: "user context without includeUserModels", context: "user" as const, options: {}, setup: () => { mockUseCurrentUser.mockReturnValue({ data: { models: ["gpt-4"] }, isLoading: false, } as any); }, expectedVisible: [], expectedHidden: ["gpt-4", "claude-3"], }, { name: "team context without organization", context: "team" as const, options: {}, props: { teamID: "team-1" }, setup: () => { mockUseTeam.mockReturnValue({ data: { team_id: "team-1", team_alias: "Test Team", models: [] }, isLoading: false, } as any); mockUseOrganization.mockReturnValue({ data: undefined, isLoading: false, } as any); }, expectedVisible: ["gpt-4", "claude-3"], expectedHidden: [], }, { name: "team context with organization having all-proxy-models", context: "team" as const, options: {}, props: { teamID: "team-1", organizationID: "org-1" }, setup: () => { mockUseTeam.mockReturnValue({ data: { team_id: "team-1", team_alias: "Test Team", models: [] }, isLoading: false, } as any); mockUseOrganization.mockReturnValue({ data: createMockOrganization(["all-proxy-models"]), isLoading: false, } as any); }, expectedVisible: ["gpt-4", "claude-3"], expectedHidden: [], }, { name: "team context with organization filtering models", context: "team" as const, options: {}, props: { teamID: "team-1", organizationID: "org-1" }, setup: () => { mockUseTeam.mockReturnValue({ data: { team_id: "team-1", team_alias: "Test Team", models: [] }, isLoading: false, } as any); mockUseOrganization.mockReturnValue({ data: createMockOrganization(["gpt-4"]), isLoading: false, } as any); }, expectedVisible: ["gpt-4"], expectedHidden: ["claude-3"], }, { name: "organization context", context: "organization" as const, options: {}, props: { organizationID: "org-1" }, setup: () => { mockUseOrganization.mockReturnValue({ data: createMockOrganization(["gpt-4"]), isLoading: false, } as any); }, expectedVisible: ["gpt-4", "claude-3"], expectedHidden: [], }, { name: "global context", context: "global" as const, options: {}, setup: () => { }, expectedVisible: ["gpt-4", "claude-3"], expectedHidden: [], }, ]; for (const testCase of testCases) { testCase.setup(); const { unmount } = renderWithProviders( , ); await waitFor(() => { testCase.expectedVisible.forEach((model) => { expect(screen.getByText(model)).toBeInTheDocument(); }); testCase.expectedHidden.forEach((model) => { expect(screen.queryByText(model)).not.toBeInTheDocument(); }); }); unmount(); vi.clearAllMocks(); mockUseAllProxyModels.mockReturnValue({ data: { data: mockProxyModels }, isLoading: false, } as any); } }); it("should show All Proxy Models option based on conditions", async () => { const testCases = [ { name: "when showAllProxyModelsOverride is true", context: "user" as const, options: { showAllProxyModelsOverride: true, includeSpecialOptions: true }, setup: () => { }, shouldShow: true, }, { name: "when organization has all-proxy-models", context: "organization" as const, options: { includeSpecialOptions: true }, props: { organizationID: "org-1" }, setup: () => { mockUseOrganization.mockReturnValue({ data: createMockOrganization(["all-proxy-models"]), isLoading: false, } as any); }, shouldShow: true, }, { name: "when organization has empty models array", context: "organization" as const, options: { includeSpecialOptions: true }, props: { organizationID: "org-1" }, setup: () => { mockUseOrganization.mockReturnValue({ data: createMockOrganization([]), isLoading: false, } as any); }, shouldShow: true, }, { name: "when context is global", context: "global" as const, options: { includeSpecialOptions: true }, setup: () => { }, shouldShow: true, }, { name: "when organization has specific models", context: "organization" as const, options: { includeSpecialOptions: true }, props: { organizationID: "org-1" }, setup: () => { mockUseOrganization.mockReturnValue({ data: createMockOrganization(["gpt-4"]), isLoading: false, } as any); }, shouldShow: false, }, ]; for (const testCase of testCases) { testCase.setup(); const { unmount } = renderWithProviders( , ); await waitFor(() => { if (testCase.shouldShow) { expect(screen.getByText("All Proxy Models")).toBeInTheDocument(); } else { expect(screen.queryByText("All Proxy Models")).not.toBeInTheDocument(); expect(screen.getByText("No Default Models")).toBeInTheDocument(); } }); unmount(); vi.clearAllMocks(); mockUseAllProxyModels.mockReturnValue({ data: { data: mockProxyModels }, isLoading: false, } as any); } }); it("should deduplicate models with same id", async () => { const duplicateModels: ProxyModel[] = [ { id: "gpt-4", object: "model", created: 1234567890, owned_by: "openai" }, { id: "gpt-4", object: "model", created: 1234567890, owned_by: "openai" }, ]; mockUseAllProxyModels.mockReturnValue({ data: { data: duplicateModels }, isLoading: false, } as any); renderWithProviders( , ); await waitFor(() => { const gpt4Options = screen.getAllByText("gpt-4"); expect(gpt4Options.length).toBeGreaterThan(0); }); }); it("should use custom dataTestId when provided", async () => { renderWithProviders( , ); await waitFor(() => { expect(screen.getByTestId("custom-test-id")).toBeInTheDocument(); }); }); it("should return all proxy models for team context when organization has empty models array", async () => { mockUseTeam.mockReturnValue({ data: { team_id: "team-1", team_alias: "Test Team", models: [] }, isLoading: false, } as any); mockUseOrganization.mockReturnValue({ data: createMockOrganization([]), isLoading: false, } as any); renderWithProviders(); await waitFor(() => { expect(screen.getByText("gpt-4")).toBeInTheDocument(); expect(screen.getByText("claude-3")).toBeInTheDocument(); }); }); it("should disable No Default Models when all-proxy-models is selected", async () => { mockUseOrganization.mockReturnValue({ data: createMockOrganization(["all-proxy-models"]), isLoading: false, } as any); renderWithProviders( , ); await waitFor(() => { const noDefaultOption = screen.getByRole("option", { name: "No Default Models" }); expect(noDefaultOption).toBeDisabled(); }); }); it("should render maxTagPlaceholder when many items are selected", async () => { // Create many models to trigger maxTagCount responsive behavior const manyModels: ProxyModel[] = Array.from({ length: 20 }, (_, i) => ({ id: `model-${i}`, object: "model", created: 1234567890, owned_by: "test", })); mockUseAllProxyModels.mockReturnValue({ data: { data: manyModels }, isLoading: false, } as any); const selectedValues = manyModels.slice(0, 10).map((m) => m.id); renderWithProviders( , ); await waitFor(() => { expect(screen.getByTestId("model-select")).toBeInTheDocument(); // Verify maxTagPlaceholder is rendered with omitted values expect(screen.getByTestId("max-tag-placeholder")).toBeInTheDocument(); expect(screen.getByText(/\+5 more/)).toBeInTheDocument(); }); }); });