mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
Migrate Default Team settings to use reusable Model Select
This commit is contained in:
parent
8eba641190
commit
cf734cb586
4 changed files with 974 additions and 378 deletions
|
|
@ -37,12 +37,19 @@ vi.mock("antd", async (importOriginal) => {
|
|||
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 (
|
||||
<div data-testid={dataTestId || "model-select"}>
|
||||
<select
|
||||
multiple={mode === "multiple"}
|
||||
role="listbox"
|
||||
value={value}
|
||||
value={visibleValues}
|
||||
onChange={(e) => {
|
||||
const selectedValues = Array.from(e.target.selectedOptions, (option) => option.value);
|
||||
onChange(mode === "multiple" ? selectedValues : selectedValues[0]);
|
||||
|
|
@ -62,6 +69,9 @@ vi.mock("antd", async (importOriginal) => {
|
|||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
{shouldShowPlaceholder && maxTagPlaceholder && (
|
||||
<div data-testid="max-tag-placeholder">{maxTagPlaceholder(omittedValues)}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
@ -82,6 +92,24 @@ 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" },
|
||||
|
|
@ -112,125 +140,44 @@ describe("ModelSelect", () => {
|
|||
} as any);
|
||||
});
|
||||
|
||||
it("should render", async () => {
|
||||
it("should render with all option groups", async () => {
|
||||
renderWithProviders(
|
||||
<ModelSelect onChange={mockOnChange} context="user" options={{ showAllProxyModelsOverride: true }} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("model-select")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show skeleton loader when loading", () => {
|
||||
mockUseAllProxyModels.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<ModelSelect onChange={mockOnChange} context="user" />);
|
||||
|
||||
expect(screen.getByTestId("skeleton-input")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("model-select")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show skeleton loader when team is loading", () => {
|
||||
mockUseTeam.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<ModelSelect onChange={mockOnChange} context="team" teamID="team-1" />);
|
||||
|
||||
expect(screen.getByTestId("skeleton-input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show skeleton loader when organization is loading", () => {
|
||||
mockUseOrganization.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<ModelSelect onChange={mockOnChange} context="organization" organizationID="org-1" />);
|
||||
|
||||
expect(screen.getByTestId("skeleton-input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show skeleton loader when current user is loading", () => {
|
||||
mockUseCurrentUser.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<ModelSelect onChange={mockOnChange} context="user" />);
|
||||
|
||||
expect(screen.getByTestId("skeleton-input")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render special options group", async () => {
|
||||
const mockOrganization: Organization = {
|
||||
organization_id: "org-1",
|
||||
organization_alias: "Test Org",
|
||||
budget_id: "budget-1",
|
||||
metadata: {},
|
||||
models: ["all-proxy-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,
|
||||
};
|
||||
|
||||
mockUseOrganization.mockReturnValue({
|
||||
data: mockOrganization,
|
||||
isLoading: false,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(
|
||||
<ModelSelect
|
||||
onChange={mockOnChange}
|
||||
context="organization"
|
||||
organizationID="org-1"
|
||||
options={{ includeSpecialOptions: true }}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const select = screen.getByTestId("model-select");
|
||||
expect(select).toBeInTheDocument();
|
||||
expect(screen.getByText("All Proxy Models")).toBeInTheDocument();
|
||||
expect(screen.getByText("No Default Models")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render wildcard options group", async () => {
|
||||
renderWithProviders(
|
||||
<ModelSelect onChange={mockOnChange} context="user" options={{ showAllProxyModelsOverride: true }} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
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 render regular models group", async () => {
|
||||
renderWithProviders(
|
||||
<ModelSelect onChange={mockOnChange} context="user" options={{ showAllProxyModelsOverride: true }} />,
|
||||
);
|
||||
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 },
|
||||
];
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("gpt-4")).toBeInTheDocument();
|
||||
expect(screen.getByText("claude-3")).toBeInTheDocument();
|
||||
loadingScenarios.forEach(({ hook, context, props = {} }) => {
|
||||
hook.mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
} as any);
|
||||
|
||||
const { unmount } = renderWithProviders(
|
||||
<ModelSelect onChange={mockOnChange} context={context} {...props} />,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("skeleton-input")).toBeInTheDocument();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it("should call onChange when selecting a regular model", async () => {
|
||||
it("should handle model selection and onChange", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<ModelSelect onChange={mockOnChange} context="user" options={{ showAllProxyModelsOverride: true }} />,
|
||||
|
|
@ -242,32 +189,16 @@ describe("ModelSelect", () => {
|
|||
|
||||
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 call onChange with only last special option when multiple special options are selected", async () => {
|
||||
it("should handle special options correctly", async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockOrganization: Organization = {
|
||||
organization_id: "org-1",
|
||||
organization_alias: "Test Org",
|
||||
budget_id: "budget-1",
|
||||
metadata: {},
|
||||
models: ["all-proxy-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,
|
||||
};
|
||||
|
||||
mockUseOrganization.mockReturnValue({
|
||||
data: mockOrganization,
|
||||
data: createMockOrganization(["all-proxy-models"]),
|
||||
isLoading: false,
|
||||
} as any);
|
||||
|
||||
|
|
@ -281,16 +212,16 @@ describe("ModelSelect", () => {
|
|||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("model-select")).toBeInTheDocument();
|
||||
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 regular models when special option is selected", async () => {
|
||||
it("should disable models when special option is selected", async () => {
|
||||
renderWithProviders(
|
||||
<ModelSelect
|
||||
onChange={mockOnChange}
|
||||
|
|
@ -301,187 +232,230 @@ describe("ModelSelect", () => {
|
|||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const gpt4Option = screen.getByRole("option", { name: "gpt-4" });
|
||||
expect(gpt4Option).toBeDisabled();
|
||||
expect(screen.getByRole("option", { name: "gpt-4" })).toBeDisabled();
|
||||
expect(screen.getByRole("option", { name: "All Openai models" })).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable wildcard models when special option is selected", async () => {
|
||||
renderWithProviders(
|
||||
<ModelSelect
|
||||
onChange={mockOnChange}
|
||||
value={["all-proxy-models"]}
|
||||
context="user"
|
||||
options={{ showAllProxyModelsOverride: true }}
|
||||
/>,
|
||||
);
|
||||
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: [],
|
||||
},
|
||||
];
|
||||
|
||||
await waitFor(() => {
|
||||
const openaiWildcardOption = screen.getByRole("option", { name: "All Openai models" });
|
||||
expect(openaiWildcardOption).toBeDisabled();
|
||||
});
|
||||
for (const testCase of testCases) {
|
||||
testCase.setup();
|
||||
const { unmount } = renderWithProviders(
|
||||
<ModelSelect
|
||||
onChange={mockOnChange}
|
||||
context={testCase.context}
|
||||
options={testCase.options}
|
||||
{...(testCase.props || {})}
|
||||
/>,
|
||||
);
|
||||
|
||||
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 disable other special options when one special option is selected", async () => {
|
||||
const mockOrganization: Organization = {
|
||||
organization_id: "org-1",
|
||||
organization_alias: "Test Org",
|
||||
budget_id: "budget-1",
|
||||
metadata: {},
|
||||
models: ["all-proxy-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,
|
||||
};
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
mockUseOrganization.mockReturnValue({
|
||||
data: mockOrganization,
|
||||
isLoading: false,
|
||||
} as any);
|
||||
for (const testCase of testCases) {
|
||||
testCase.setup();
|
||||
const { unmount } = renderWithProviders(
|
||||
<ModelSelect
|
||||
onChange={mockOnChange}
|
||||
context={testCase.context}
|
||||
options={testCase.options}
|
||||
{...(testCase.props || {})}
|
||||
/>,
|
||||
);
|
||||
|
||||
renderWithProviders(
|
||||
<ModelSelect
|
||||
onChange={mockOnChange}
|
||||
value={["all-proxy-models"]}
|
||||
context="organization"
|
||||
organizationID="org-1"
|
||||
options={{ showAllProxyModelsOverride: true, includeSpecialOptions: true }}
|
||||
/>,
|
||||
);
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
const noDefaultOption = screen.getByRole("option", { name: "No Default Models" });
|
||||
expect(noDefaultOption).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter models when showAllProxyModelsOverride is true", async () => {
|
||||
renderWithProviders(
|
||||
<ModelSelect onChange={mockOnChange} context="user" options={{ showAllProxyModelsOverride: true }} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("gpt-4")).toBeInTheDocument();
|
||||
expect(screen.getByText("claude-3")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter models when organization has all-proxy-models in models array", async () => {
|
||||
const mockOrganization: Organization = {
|
||||
organization_id: "org-1",
|
||||
organization_alias: "Test Org",
|
||||
budget_id: "budget-1",
|
||||
metadata: {},
|
||||
models: ["all-proxy-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,
|
||||
};
|
||||
|
||||
mockUseOrganization.mockReturnValue({
|
||||
data: mockOrganization,
|
||||
isLoading: false,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<ModelSelect onChange={mockOnChange} context="organization" organizationID="org-1" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("gpt-4")).toBeInTheDocument();
|
||||
expect(screen.getByText("claude-3")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show all models when organization context is used", async () => {
|
||||
const mockOrganization: Organization = {
|
||||
organization_id: "org-1",
|
||||
organization_alias: "Test Org",
|
||||
budget_id: "budget-1",
|
||||
metadata: {},
|
||||
models: ["gpt-4"],
|
||||
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,
|
||||
};
|
||||
|
||||
mockUseOrganization.mockReturnValue({
|
||||
data: mockOrganization,
|
||||
isLoading: false,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<ModelSelect onChange={mockOnChange} context="organization" organizationID="org-1" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("gpt-4")).toBeInTheDocument();
|
||||
expect(screen.getByText("claude-3")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should use custom dataTestId when provided", async () => {
|
||||
renderWithProviders(
|
||||
<ModelSelect
|
||||
onChange={mockOnChange}
|
||||
dataTestId="custom-test-id"
|
||||
context="user"
|
||||
options={{ showAllProxyModelsOverride: true }}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("custom-test-id")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle multiple model selections", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(
|
||||
<ModelSelect onChange={mockOnChange} context="user" options={{ showAllProxyModelsOverride: true }} />,
|
||||
);
|
||||
|
||||
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, "claude-3");
|
||||
expect(mockOnChange).toHaveBeenCalled();
|
||||
const allCalls = mockOnChange.mock.calls.map((call) => call[0]);
|
||||
expect(allCalls.some((call) => Array.isArray(call) && call.includes("gpt-4"))).toBe(true);
|
||||
expect(allCalls.some((call) => Array.isArray(call) && call.includes("claude-3"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should capitalize provider name in wildcard options", async () => {
|
||||
renderWithProviders(
|
||||
<ModelSelect onChange={mockOnChange} context="user" options={{ showAllProxyModelsOverride: true }} />,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("All Openai models")).toBeInTheDocument();
|
||||
expect(screen.getByText("All Anthropic models")).toBeInTheDocument();
|
||||
});
|
||||
unmount();
|
||||
vi.clearAllMocks();
|
||||
mockUseAllProxyModels.mockReturnValue({
|
||||
data: { data: mockProxyModels },
|
||||
isLoading: false,
|
||||
} as any);
|
||||
}
|
||||
});
|
||||
|
||||
it("should deduplicate models with same id", async () => {
|
||||
|
|
@ -505,52 +479,29 @@ describe("ModelSelect", () => {
|
|||
});
|
||||
});
|
||||
|
||||
it("should filter models based on user context with includeUserModels option", async () => {
|
||||
mockUseCurrentUser.mockReturnValue({
|
||||
data: { models: ["gpt-4"] },
|
||||
isLoading: false,
|
||||
} as any);
|
||||
|
||||
renderWithProviders(<ModelSelect onChange={mockOnChange} context="user" options={{ includeUserModels: true }} />);
|
||||
it("should use custom dataTestId when provided", async () => {
|
||||
renderWithProviders(
|
||||
<ModelSelect
|
||||
onChange={mockOnChange}
|
||||
dataTestId="custom-test-id"
|
||||
context="user"
|
||||
options={{ showAllProxyModelsOverride: true }}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("gpt-4")).toBeInTheDocument();
|
||||
expect(screen.queryByText("claude-3")).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("custom-test-id")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter models based on team context", async () => {
|
||||
const mockTeam = {
|
||||
team_id: "team-1",
|
||||
team_alias: "Test Team",
|
||||
models: ["gpt-4"],
|
||||
};
|
||||
|
||||
const mockOrganization: Organization = {
|
||||
organization_id: "org-1",
|
||||
organization_alias: "Test Org",
|
||||
budget_id: "budget-1",
|
||||
metadata: {},
|
||||
models: ["gpt-4"],
|
||||
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,
|
||||
};
|
||||
|
||||
it("should return all proxy models for team context when organization has empty models array", async () => {
|
||||
mockUseTeam.mockReturnValue({
|
||||
data: mockTeam,
|
||||
data: { team_id: "team-1", team_alias: "Test Team", models: [] },
|
||||
isLoading: false,
|
||||
} as any);
|
||||
|
||||
mockUseOrganization.mockReturnValue({
|
||||
data: mockOrganization,
|
||||
data: createMockOrganization([]),
|
||||
isLoading: false,
|
||||
} as any);
|
||||
|
||||
|
|
@ -558,7 +509,62 @@ describe("ModelSelect", () => {
|
|||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("gpt-4")).toBeInTheDocument();
|
||||
expect(screen.queryByText("claude-3")).not.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(
|
||||
<ModelSelect
|
||||
onChange={mockOnChange}
|
||||
value={["all-proxy-models"]}
|
||||
context="organization"
|
||||
organizationID="org-1"
|
||||
options={{ includeSpecialOptions: true }}
|
||||
/>,
|
||||
);
|
||||
|
||||
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(
|
||||
<ModelSelect
|
||||
onChange={mockOnChange}
|
||||
value={selectedValues}
|
||||
context="user"
|
||||
options={{ showAllProxyModelsOverride: true }}
|
||||
/>,
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,10 +30,11 @@ export interface ModelSelectProps {
|
|||
showAllProxyModelsOverride?: boolean;
|
||||
includeSpecialOptions?: boolean;
|
||||
};
|
||||
context: "team" | "organization" | "user";
|
||||
context: "team" | "organization" | "user" | "global";
|
||||
dataTestId?: string;
|
||||
value?: string[];
|
||||
onChange: (values: string[]) => void;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
type FilterContextArgs = {
|
||||
|
|
@ -65,6 +66,10 @@ const contextFilters: Record<ModelSelectProps["context"], (args: FilterContextAr
|
|||
organization: ({ allProxyModels }) => {
|
||||
return allProxyModels;
|
||||
},
|
||||
|
||||
global: ({ allProxyModels }) => {
|
||||
return allProxyModels;
|
||||
},
|
||||
};
|
||||
|
||||
const filterModels = (
|
||||
|
|
@ -84,7 +89,7 @@ const filterModels = (
|
|||
};
|
||||
|
||||
export const ModelSelect = (props: ModelSelectProps) => {
|
||||
const { teamID, organizationID, options, context, dataTestId, value = [], onChange } = props;
|
||||
const { teamID, organizationID, options, context, dataTestId, value = [], onChange, style } = props;
|
||||
const { includeUserModels, showAllTeamModelsOption, showAllProxyModelsOverride, includeSpecialOptions } =
|
||||
options || {};
|
||||
const { data: allProxyModels, isLoading: isLoadingAllProxyModels } = useAllProxyModels();
|
||||
|
|
@ -98,7 +103,7 @@ export const ModelSelect = (props: ModelSelectProps) => {
|
|||
const organizationHasAllProxyModels = organization?.models.includes(MODEL_SELECT_ALL_PROXY_MODELS_SPECIAL_VALUE.value) || organization?.models.length === 0;
|
||||
const shouldShowAllProxyModels =
|
||||
showAllProxyModelsOverride ||
|
||||
(organizationHasAllProxyModels && includeSpecialOptions);
|
||||
(organizationHasAllProxyModels && includeSpecialOptions) || context === "global";
|
||||
|
||||
if (isLoading) {
|
||||
return <Skeleton.Input active block />;
|
||||
|
|
@ -134,6 +139,7 @@ export const ModelSelect = (props: ModelSelectProps) => {
|
|||
data-testid={dataTestId}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
style={style}
|
||||
options={[
|
||||
includeSpecialOptions
|
||||
? {
|
||||
|
|
|
|||
|
|
@ -1,63 +1,653 @@
|
|||
import { screen } from "@testing-library/react";
|
||||
import React from "react";
|
||||
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 TeamSSOSettings from "./TeamSSOSettings";
|
||||
import * as networking from "./networking";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
|
||||
// Mock the networking functions
|
||||
vi.mock("./networking");
|
||||
|
||||
// Mock the budget duration dropdown
|
||||
vi.mock("@tremor/react", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@tremor/react")>();
|
||||
const React = await import("react");
|
||||
return {
|
||||
...actual,
|
||||
Card: ({ children }: { children: React.ReactNode }) => React.createElement("div", { "data-testid": "card" }, children),
|
||||
Title: ({ children }: { children: React.ReactNode }) => React.createElement("h2", {}, children),
|
||||
Text: ({ children }: { children: React.ReactNode }) => React.createElement("span", {}, children),
|
||||
Divider: () => React.createElement("hr", {}),
|
||||
TextInput: ({ value, onChange, placeholder, className }: any) =>
|
||||
React.createElement("input", {
|
||||
type: "text",
|
||||
value: value || "",
|
||||
onChange,
|
||||
placeholder,
|
||||
className,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./common_components/budget_duration_dropdown", () => ({
|
||||
default: ({ value, onChange }: { value: string | null; onChange: (value: string) => void }) => (
|
||||
<select data-testid="budget-duration-dropdown" value={value || ""} onChange={(e) => onChange(e.target.value)}>
|
||||
<select
|
||||
data-testid="budget-duration-dropdown"
|
||||
value={value || ""}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
aria-label="Budget duration"
|
||||
>
|
||||
<option value="">Select duration</option>
|
||||
<option value="daily">Daily</option>
|
||||
<option value="monthly">Monthly</option>
|
||||
</select>
|
||||
),
|
||||
getBudgetDurationLabel: vi.fn((value: string) => value),
|
||||
getBudgetDurationLabel: vi.fn((value: string) => `Budget: ${value}`),
|
||||
}));
|
||||
|
||||
// Mock the model display name helper
|
||||
vi.mock("./key_team_helpers/fetch_available_models_team_key", () => ({
|
||||
getModelDisplayName: vi.fn((model: string) => model),
|
||||
}));
|
||||
|
||||
vi.mock("./ModelSelect/ModelSelect", () => ({
|
||||
ModelSelect: ({ value, onChange }: { value: string[]; onChange: (value: string[]) => void }) => (
|
||||
<select
|
||||
data-testid="model-select"
|
||||
multiple
|
||||
value={value || []}
|
||||
onChange={(e) => {
|
||||
const selectedValues = Array.from(e.target.selectedOptions, (option) => option.value);
|
||||
onChange(selectedValues);
|
||||
}}
|
||||
aria-label="Models"
|
||||
>
|
||||
<option value="gpt-4">gpt-4</option>
|
||||
<option value="claude-3">claude-3</option>
|
||||
</select>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("antd", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("antd")>();
|
||||
const React = await import("react");
|
||||
const SelectComponent = ({
|
||||
value,
|
||||
onChange,
|
||||
mode,
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
value: any;
|
||||
onChange: (value: any) => void;
|
||||
mode?: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) => {
|
||||
const isMultiple = mode === "multiple";
|
||||
const selectValue = isMultiple ? (Array.isArray(value) ? value : []) : value || "";
|
||||
return React.createElement(
|
||||
"select",
|
||||
{
|
||||
multiple: isMultiple,
|
||||
value: selectValue,
|
||||
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const selectedValues = Array.from(e.target.selectedOptions, (option) => option.value);
|
||||
onChange(isMultiple ? selectedValues : selectedValues[0] || undefined);
|
||||
},
|
||||
className,
|
||||
"aria-label": "Select",
|
||||
role: "listbox",
|
||||
},
|
||||
children,
|
||||
);
|
||||
};
|
||||
SelectComponent.Option = ({ value: optionValue, children: optionChildren }: { value: string; children: React.ReactNode }) =>
|
||||
React.createElement("option", { value: optionValue }, optionChildren);
|
||||
return {
|
||||
...actual,
|
||||
Spin: ({ size }: { size?: string }) => React.createElement("div", { "data-testid": "spinner", "data-size": size }),
|
||||
Switch: ({ checked, onChange }: { checked: boolean; onChange: (checked: boolean) => void }) =>
|
||||
React.createElement("input", {
|
||||
type: "checkbox",
|
||||
role: "switch",
|
||||
checked: checked,
|
||||
onChange: (e) => onChange(e.target.checked),
|
||||
"aria-label": "Toggle switch",
|
||||
}),
|
||||
Select: SelectComponent,
|
||||
Typography: {
|
||||
Paragraph: ({ children }: { children: React.ReactNode }) => React.createElement("p", {}, children),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const mockGetDefaultTeamSettings = vi.mocked(networking.getDefaultTeamSettings);
|
||||
const mockUpdateDefaultTeamSettings = vi.mocked(networking.updateDefaultTeamSettings);
|
||||
const mockModelAvailableCall = vi.mocked(networking.modelAvailableCall);
|
||||
const mockNotificationsManager = vi.mocked(NotificationsManager);
|
||||
|
||||
describe("TeamSSOSettings", () => {
|
||||
const defaultProps = {
|
||||
accessToken: "test-token",
|
||||
userID: "test-user",
|
||||
userRole: "admin",
|
||||
};
|
||||
|
||||
const mockSettings = {
|
||||
values: {
|
||||
budget_duration: "monthly",
|
||||
max_budget: 1000,
|
||||
enabled: true,
|
||||
allowed_models: ["gpt-4", "claude-3"],
|
||||
models: ["gpt-4"],
|
||||
status: "active",
|
||||
},
|
||||
field_schema: {
|
||||
description: "Default team settings schema",
|
||||
properties: {
|
||||
budget_duration: {
|
||||
type: "string",
|
||||
description: "Budget duration setting",
|
||||
},
|
||||
max_budget: {
|
||||
type: "number",
|
||||
description: "Maximum budget amount",
|
||||
},
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
description: "Enable feature",
|
||||
},
|
||||
allowed_models: {
|
||||
type: "array",
|
||||
items: {
|
||||
enum: ["gpt-4", "claude-3", "gpt-3.5-turbo"],
|
||||
},
|
||||
description: "Allowed models",
|
||||
},
|
||||
models: {
|
||||
type: "array",
|
||||
description: "Selected models",
|
||||
},
|
||||
status: {
|
||||
type: "string",
|
||||
enum: ["active", "inactive", "pending"],
|
||||
description: "Status",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockModelAvailableCall.mockResolvedValue({
|
||||
data: [{ id: "gpt-4" }, { id: "claude-3" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("renders the component", async () => {
|
||||
// Mock successful API responses
|
||||
vi.mocked(networking.getDefaultTeamSettings).mockResolvedValue({
|
||||
it("should render", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Default Team Settings")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show loading spinner while fetching settings", () => {
|
||||
mockGetDefaultTeamSettings.mockImplementation(() => new Promise(() => { }));
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
expect(screen.getByTestId("spinner")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display message when no settings are available", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(null as any);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByText("No team settings available or you do not have permission to view them."),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should not fetch settings when access token is null", async () => {
|
||||
renderWithProviders(<TeamSSOSettings accessToken={null} userID="test-user" userRole="admin" />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGetDefaultTeamSettings).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should display settings fields with correct values", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Budget Duration")).toBeInTheDocument();
|
||||
expect(screen.getByText("Max Budget")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText("Budget: monthly")).toBeInTheDocument();
|
||||
expect(screen.getByText("1000")).toBeInTheDocument();
|
||||
const enabledTexts = screen.getAllByText("Enabled");
|
||||
expect(enabledTexts.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should display 'Not set' for null values", async () => {
|
||||
const settingsWithNulls = {
|
||||
...mockSettings,
|
||||
values: {
|
||||
budget_duration: "monthly",
|
||||
max_budget: 1000,
|
||||
...mockSettings.values,
|
||||
max_budget: null,
|
||||
},
|
||||
};
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(settingsWithNulls);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Not set")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should toggle edit mode when edit button is clicked", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const editButton = screen.getByRole("button", { name: "Edit Settings" });
|
||||
await userEvent.click(editButton);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Save Changes" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Edit Settings" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should cancel edit mode and reset values", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const editButton = screen.getByRole("button", { name: "Edit Settings" });
|
||||
await userEvent.click(editButton);
|
||||
|
||||
const cancelButton = screen.getByRole("button", { name: "Cancel" });
|
||||
await userEvent.click(cancelButton);
|
||||
|
||||
expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should save settings when save button is clicked", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
mockUpdateDefaultTeamSettings.mockResolvedValue({
|
||||
settings: mockSettings.values,
|
||||
});
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const editButton = screen.getByRole("button", { name: "Edit Settings" });
|
||||
await userEvent.click(editButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Save Changes" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Changes" });
|
||||
await userEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateDefaultTeamSettings).toHaveBeenCalledWith("test-token", mockSettings.values);
|
||||
});
|
||||
|
||||
expect(mockNotificationsManager.success).toHaveBeenCalledWith("Default team settings updated successfully");
|
||||
});
|
||||
|
||||
it("should show error notification when save fails", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
mockUpdateDefaultTeamSettings.mockRejectedValue(new Error("Save failed"));
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const editButton = screen.getByRole("button", { name: "Edit Settings" });
|
||||
await userEvent.click(editButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Save Changes" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Changes" });
|
||||
await userEvent.click(saveButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotificationsManager.fromBackend).toHaveBeenCalledWith("Failed to update team settings");
|
||||
});
|
||||
});
|
||||
|
||||
it("should render boolean field as switch in edit mode", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const editButton = screen.getByRole("button", { name: "Edit Settings" });
|
||||
await userEvent.click(editButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const switchElement = screen.getByRole("switch");
|
||||
expect(switchElement).toBeInTheDocument();
|
||||
expect(switchElement).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
it("should update boolean value when switch is toggled", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const editButton = screen.getByRole("button", { name: "Edit Settings" });
|
||||
await userEvent.click(editButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("switch")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const switchElement = screen.getByRole("switch");
|
||||
await userEvent.click(switchElement);
|
||||
|
||||
expect(switchElement).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("should render budget duration dropdown in edit mode", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const editButton = screen.getByRole("button", { name: "Edit Settings" });
|
||||
await userEvent.click(editButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("Budget duration")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should update budget duration when dropdown value changes", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const editButton = screen.getByRole("button", { name: "Edit Settings" });
|
||||
await userEvent.click(editButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("Budget duration")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const dropdown = screen.getByLabelText("Budget duration");
|
||||
await userEvent.selectOptions(dropdown, "daily");
|
||||
|
||||
expect(dropdown).toHaveValue("daily");
|
||||
});
|
||||
|
||||
it("should render text input for string fields in edit mode", async () => {
|
||||
const settingsWithString = {
|
||||
...mockSettings,
|
||||
field_schema: {
|
||||
description: "Default team settings",
|
||||
...mockSettings.field_schema,
|
||||
properties: {
|
||||
budget_duration: {
|
||||
...mockSettings.field_schema.properties,
|
||||
team_name: {
|
||||
type: "string",
|
||||
description: "Budget duration",
|
||||
},
|
||||
max_budget: {
|
||||
type: "number",
|
||||
description: "Maximum budget",
|
||||
description: "Team name",
|
||||
},
|
||||
},
|
||||
},
|
||||
values: {
|
||||
...mockSettings.values,
|
||||
team_name: "Test Team",
|
||||
},
|
||||
};
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(settingsWithString);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
vi.mocked(networking.modelAvailableCall).mockResolvedValue({
|
||||
data: [{ id: "gpt-4" }, { id: "claude-3" }],
|
||||
const editButton = screen.getByRole("button", { name: "Edit Settings" });
|
||||
await userEvent.click(editButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const textInput = screen.getByDisplayValue("Test Team");
|
||||
expect(textInput).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render enum select for string enum fields in edit mode", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
renderWithProviders(<TeamSSOSettings accessToken="test-token" userID="test-user" userRole="admin" />);
|
||||
const editButton = screen.getByRole("button", { name: "Edit Settings" });
|
||||
await userEvent.click(editButton);
|
||||
|
||||
const container = await screen.findByText("Default Team Settings");
|
||||
expect(container).toBeInTheDocument();
|
||||
await waitFor(() => {
|
||||
const statusSelect = screen.getAllByRole("listbox")[0];
|
||||
expect(statusSelect).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render multi-select for array enum fields in edit mode", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const editButton = screen.getByRole("button", { name: "Edit Settings" });
|
||||
await userEvent.click(editButton);
|
||||
|
||||
await waitFor(() => {
|
||||
const multiSelects = screen.getAllByRole("listbox");
|
||||
expect(multiSelects.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should render ModelSelect for models field in edit mode", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const editButton = screen.getByRole("button", { name: "Edit Settings" });
|
||||
await userEvent.click(editButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("model-select")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should display models as badges in view mode", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const gpt4Elements = screen.getAllByText("gpt-4");
|
||||
expect(gpt4Elements.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should display 'None' for empty arrays in view mode", async () => {
|
||||
const settingsWithEmptyArray = {
|
||||
...mockSettings,
|
||||
values: {
|
||||
...mockSettings.values,
|
||||
models: [],
|
||||
},
|
||||
};
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(settingsWithEmptyArray);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
const noneTexts = screen.getAllByText("None");
|
||||
expect(noneTexts.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("should display schema description when available", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Default team settings schema")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should show error notification when fetching settings fails", async () => {
|
||||
mockGetDefaultTeamSettings.mockRejectedValue(new Error("Fetch failed"));
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotificationsManager.fromBackend).toHaveBeenCalledWith("Failed to fetch team settings");
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle model fetch error gracefully", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
mockModelAvailableCall.mockRejectedValue(new Error("Model fetch failed"));
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Default Team Settings")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should disable cancel button while saving", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
mockUpdateDefaultTeamSettings.mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(() => resolve({ settings: mockSettings.values }), 100)),
|
||||
);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Edit Settings" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const editButton = screen.getByRole("button", { name: "Edit Settings" });
|
||||
await userEvent.click(editButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("button", { name: "Save Changes" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const saveButton = screen.getByRole("button", { name: "Save Changes" });
|
||||
await userEvent.click(saveButton);
|
||||
|
||||
const cancelButton = screen.getByRole("button", { name: "Cancel" });
|
||||
expect(cancelButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should display field descriptions", async () => {
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(mockSettings);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Budget duration setting")).toBeInTheDocument();
|
||||
expect(screen.getByText("Maximum budget amount")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should format field names by replacing underscores and capitalizing", async () => {
|
||||
const settingsWithUnderscores = {
|
||||
...mockSettings,
|
||||
field_schema: {
|
||||
...mockSettings.field_schema,
|
||||
properties: {
|
||||
...mockSettings.field_schema.properties,
|
||||
max_budget_per_user: {
|
||||
type: "number",
|
||||
description: "Max budget per user",
|
||||
},
|
||||
},
|
||||
},
|
||||
values: {
|
||||
...mockSettings.values,
|
||||
max_budget_per_user: 500,
|
||||
},
|
||||
};
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(settingsWithUnderscores);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Max Budget Per User")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should display 'No schema information available' when schema is missing", async () => {
|
||||
const settingsWithoutSchema = {
|
||||
values: {},
|
||||
field_schema: null,
|
||||
};
|
||||
mockGetDefaultTeamSettings.mockResolvedValue(settingsWithoutSchema);
|
||||
|
||||
renderWithProviders(<TeamSSOSettings {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("No schema information available")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { getDefaultTeamSettings, updateDefaultTeamSettings, modelAvailableCall }
|
|||
import BudgetDurationDropdown, { getBudgetDurationLabel } from "./common_components/budget_duration_dropdown";
|
||||
import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key";
|
||||
import NotificationsManager from "./molecules/notifications_manager";
|
||||
import { ModelSelect } from "./ModelSelect/ModelSelect";
|
||||
|
||||
interface TeamSSOSettingsProps {
|
||||
accessToken: string | null;
|
||||
|
|
@ -116,22 +117,15 @@ const TeamSSOSettings: React.FC<TeamSSOSettingsProps> = ({ accessToken, userID,
|
|||
);
|
||||
} else if (key === "models") {
|
||||
return (
|
||||
<Select
|
||||
mode="multiple"
|
||||
style={{ width: "100%" }}
|
||||
<ModelSelect
|
||||
value={editedValues[key] || []}
|
||||
onChange={(value) => handleTextInputChange(key, value)}
|
||||
className="mt-2"
|
||||
>
|
||||
<Option key="no-default-models" value="no-default-models">
|
||||
No Default Models
|
||||
</Option>
|
||||
{availableModels.map((model: string) => (
|
||||
<Option key={model} value={model}>
|
||||
{getModelDisplayName(model)}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
context="global"
|
||||
style={{ width: "100%" }}
|
||||
options={{
|
||||
includeSpecialOptions: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (type === "string" && property.enum) {
|
||||
return (
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue