mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
test(ui): add tests for model management UI improvements
- Add tests for dismissable Missing provider banner with localStorage persistence - Add tests for compact Request Provider button when banner is dismissed - Add tests for delete modal functionality and DB Model badge - Add tests for clickable Model ID that calls setSelectedModelId
This commit is contained in:
parent
219cd414f3
commit
c005f1aec2
2 changed files with 218 additions and 2 deletions
|
|
@ -1,9 +1,27 @@
|
|||
/* @vitest-environment jsdom */
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { render } from "@testing-library/react";
|
||||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import ModelsAndEndpointsView from "./ModelsAndEndpointsView";
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = (() => {
|
||||
let store: Record<string, string> = {};
|
||||
return {
|
||||
getItem: (key: string) => store[key] || null,
|
||||
setItem: (key: string, value: string) => {
|
||||
store[key] = value;
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
delete store[key];
|
||||
},
|
||||
clear: () => {
|
||||
store = {};
|
||||
},
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(window, "localStorage", { value: localStorageMock });
|
||||
|
||||
// Minimal stubs to avoid Next.js router and network usage during render
|
||||
vi.mock("@/components/networking", () => ({
|
||||
credentialListCall: vi.fn().mockResolvedValue({ credentials: [] }),
|
||||
|
|
@ -104,4 +122,83 @@ describe("ModelsAndEndpointsView", () => {
|
|||
);
|
||||
expect(await findByText("Model Management", {}, { timeout: 10000 })).toBeInTheDocument();
|
||||
}, 15000);
|
||||
|
||||
it("should show Missing provider banner by default", async () => {
|
||||
localStorageMock.clear();
|
||||
const queryClient = createQueryClient();
|
||||
const { findByText } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ModelsAndEndpointsView
|
||||
token="123"
|
||||
modelData={{ data: [] }}
|
||||
keys={[]}
|
||||
setModelData={() => {}}
|
||||
premiumUser={false}
|
||||
teams={[]}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
expect(await findByText("Missing a provider?", {}, { timeout: 10000 })).toBeInTheDocument();
|
||||
}, 15000);
|
||||
|
||||
it("should hide Missing provider banner when dismiss button is clicked and persist to localStorage", async () => {
|
||||
localStorageMock.clear();
|
||||
const queryClient = createQueryClient();
|
||||
const { findByText, queryByText, container } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ModelsAndEndpointsView
|
||||
token="123"
|
||||
modelData={{ data: [] }}
|
||||
keys={[]}
|
||||
setModelData={() => {}}
|
||||
premiumUser={false}
|
||||
teams={[]}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Wait for banner to appear
|
||||
expect(await findByText("Missing a provider?", {}, { timeout: 10000 })).toBeInTheDocument();
|
||||
|
||||
// Find and click dismiss button (X button)
|
||||
const dismissButton = container.querySelector('button[aria-label="Dismiss banner"]');
|
||||
if (dismissButton) {
|
||||
fireEvent.click(dismissButton);
|
||||
|
||||
// Banner should be hidden
|
||||
expect(queryByText("Missing a provider?")).not.toBeInTheDocument();
|
||||
|
||||
// LocalStorage should be updated
|
||||
expect(localStorageMock.getItem("hideMissingProviderBanner")).toBe("true");
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
it("should show compact Request Provider button when banner is dismissed", async () => {
|
||||
// Set localStorage to hide banner
|
||||
localStorageMock.setItem("hideMissingProviderBanner", "true");
|
||||
const queryClient = createQueryClient();
|
||||
const { findByText, queryByText } = render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ModelsAndEndpointsView
|
||||
token="123"
|
||||
modelData={{ data: [] }}
|
||||
keys={[]}
|
||||
setModelData={() => {}}
|
||||
premiumUser={false}
|
||||
teams={[]}
|
||||
/>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
// Wait for component to render
|
||||
await findByText("Model Management", {}, { timeout: 10000 });
|
||||
|
||||
// Banner should not be visible
|
||||
expect(queryByText("Missing a provider?")).not.toBeInTheDocument();
|
||||
|
||||
// Compact Request Provider button should be visible in header
|
||||
const requestProviderLinks = document.querySelectorAll('a[href="https://models.litellm.ai/?request=true"]');
|
||||
// There should be a compact button when banner is hidden
|
||||
expect(requestProviderLinks.length).toBeGreaterThan(0);
|
||||
}, 15000);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,30 @@
|
|||
import * as useAuthorizedModule from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import AllModelsTab from "./AllModelsTab";
|
||||
|
||||
// Mock modelDeleteCall
|
||||
const mockModelDeleteCall = vi.fn().mockResolvedValue({});
|
||||
vi.mock("@/components/networking", () => ({
|
||||
modelDeleteCall: (...args: any[]) => mockModelDeleteCall(...args),
|
||||
}));
|
||||
|
||||
// Mock NotificationsManager
|
||||
vi.mock("@/components/molecules/notifications_manager", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
fromBackend: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock react-query
|
||||
const mockInvalidateQueries = vi.fn();
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQueryClient: () => ({
|
||||
invalidateQueries: mockInvalidateQueries,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock the useModelsInfo hook
|
||||
const mockUseModelsInfo = vi.fn(() => ({
|
||||
data: { data: [], total_count: 0, current_page: 1, total_pages: 1, size: 50 },
|
||||
|
|
@ -493,4 +515,101 @@ describe("AllModelsTab", () => {
|
|||
const previousButton = screen.getByRole("button", { name: /previous/i });
|
||||
expect(previousButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should pass setDeleteModalModelId to columns for delete functionality", async () => {
|
||||
// This test verifies that the delete modal setter is passed to columns
|
||||
// The actual modal rendering is handled by DeleteResourceModal component
|
||||
mockUseTeams.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
|
||||
mockUseModelCostMap.mockReturnValue(
|
||||
createModelCostMapMock({
|
||||
"gpt-4-delete-test": { litellm_provider: "openai" },
|
||||
}),
|
||||
);
|
||||
|
||||
const modelData = createPaginatedModelData([
|
||||
{
|
||||
model_name: "gpt-4-delete-test",
|
||||
litellm_model_name: "gpt-4-delete-test",
|
||||
provider: "openai",
|
||||
model_info: {
|
||||
id: "model-to-delete",
|
||||
db_model: true,
|
||||
direct_access: true,
|
||||
access_via_team_ids: [],
|
||||
access_groups: [],
|
||||
created_by: "user-123",
|
||||
created_at: "2024-01-01",
|
||||
updated_at: "2024-01-01",
|
||||
},
|
||||
},
|
||||
], 1, 1, 1, 50);
|
||||
|
||||
mockUseModelsInfo.mockReturnValue({ data: modelData, isLoading: false, error: null, refetch: vi.fn() });
|
||||
|
||||
render(<AllModelsTab {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("gpt-4-delete-test")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Verify the DB Model badge is shown (indicating it can be deleted)
|
||||
expect(screen.getByText("DB Model")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render clickable model ID that calls setSelectedModelId", async () => {
|
||||
mockUseTeams.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
error: null,
|
||||
refetch: vi.fn(),
|
||||
});
|
||||
|
||||
mockUseModelCostMap.mockReturnValue(
|
||||
createModelCostMapMock({
|
||||
"gpt-4-clickable": { litellm_provider: "openai" },
|
||||
}),
|
||||
);
|
||||
|
||||
const modelData = createPaginatedModelData([
|
||||
{
|
||||
model_name: "gpt-4-clickable",
|
||||
litellm_model_name: "gpt-4-clickable",
|
||||
provider: "openai",
|
||||
model_info: {
|
||||
id: "clickable-model-id",
|
||||
db_model: true,
|
||||
direct_access: true,
|
||||
access_via_team_ids: [],
|
||||
access_groups: [],
|
||||
created_by: "user-123",
|
||||
created_at: "2024-01-01",
|
||||
updated_at: "2024-01-01",
|
||||
},
|
||||
},
|
||||
], 1, 1, 1, 50);
|
||||
|
||||
mockUseModelsInfo.mockReturnValue({ data: modelData, isLoading: false, error: null, refetch: vi.fn() });
|
||||
|
||||
render(<AllModelsTab {...defaultProps} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("gpt-4-clickable")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Click on the Model ID cell which should call setSelectedModelId
|
||||
const modelIdCell = screen.getByText("clickable-model-id");
|
||||
expect(modelIdCell).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(modelIdCell);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetSelectedModelId).toHaveBeenCalledWith("clickable-model-id");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue