Merge pull request #25590 from BerriAI/litellm_add_model_e2e_tests

[Test] UI - Models: Add E2E tests for Add Model flow
This commit is contained in:
yuneng-jiang 2026-04-13 19:03:39 -07:00 • committed by GitHub
commit e64d98f725
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 132 additions and 15 deletions

View file

@ -1,24 +1,34 @@
import { test, expect } from "@playwright/test";
import { ADMIN_STORAGE_PATH, E2E_TEAM_CRUD_ID } from "../../constants";
import { Role, users } from "../../fixtures/users";
import { navigateToPage } from "../../helpers/navigation";
import { Page } from "../../fixtures/pages";
/**
* Helper to select a provider from the Add Model form dropdown.
*/
async function selectProvider(page: any, providerName: string) {
const providerDropdown = page.getByRole("combobox", { name: /Provider/i });
await providerDropdown.fill(providerName);
await page.waitForTimeout(1000);
await providerDropdown.press("Enter");
await page.waitForTimeout(2000);
}
test.describe("Add Model", () => {
test.use({ storageState: ADMIN_STORAGE_PATH });
test("Able to see all models for a specific provider in the model dropdown", async ({ page }) => {
await page.goto("/ui");
await page.getByText("Models + Endpoints").click();
await navigateToPage(page, Page.Models);
await page.getByRole("tab", { name: "Add Model" }).click();
const providerInputDropdown = page.getByRole("combobox", { name: /Provider/i });
await providerInputDropdown.fill("Anthropic");
await page.waitForTimeout(1000);
await providerInputDropdown.press("Enter");
await page.waitForTimeout(2000);
await selectProvider(page, "Anthropic");
const providerModelsDropdown = page.locator(".ant-select-selection-overflow").first();
await providerModelsDropdown.click();
// The model field should be a multi-select dropdown; click to open it
const modelDropdown = page.locator(".ant-select-selection-overflow").first();
await modelDropdown.click();
// Verify provider-specific models are listed
await expect(page.getByTitle("claude-haiku-4-5", { exact: true })).toBeVisible();
});
@ -72,4 +82,109 @@ test.describe("Add Model", () => {
await expect(page.getByText("999", { exact: true })).toBeVisible({ timeout: 10_000 });
await expect(page.getByText("888", { exact: true })).toBeVisible({ timeout: 10_000 });
});
test("Test connection with bad credentials shows failure", async ({ page }) => {
await navigateToPage(page, Page.Models);
await page.getByRole("tab", { name: "Add Model" }).click();
await selectProvider(page, "Anthropic");
// Select model: claude-haiku-4-5
const modelDropdown = page.locator(".ant-select-selection-overflow").first();
await modelDropdown.click();
await page.getByTitle("claude-haiku-4-5", { exact: true }).click();
await page.keyboard.press("Escape");
// Enter bad API key
const apiKeyInput = page.locator('input[type="password"]').first();
await apiKeyInput.fill("sk-bad-key-12345");
// Click Test Connect button by its text
await page.getByRole("button", { name: "Test Connect" }).click();
// Wait for modal to appear and connection test to complete
await expect(page.getByText("Connection Test Results")).toBeVisible({ timeout: 10_000 });
// Verify failure message appears (the test makes a real API call, so it will fail with bad creds)
await expect(page.getByText(/Connection to .* failed/)).toBeVisible({ timeout: 30_000 });
});
test("Add specific model and verify it appears in All Models", async ({ page }) => {
await navigateToPage(page, Page.Models);
await page.getByRole("tab", { name: "Add Model" }).click();
await selectProvider(page, "Anthropic");
// Select model: claude-haiku-4-5
const modelDropdown = page.locator(".ant-select-selection-overflow").first();
await modelDropdown.click();
await page.getByTitle("claude-haiku-4-5", { exact: true }).click();
await page.keyboard.press("Escape");
// Enter any API key
const apiKeyInput = page.locator('input[type="password"]').first();
await apiKeyInput.fill("sk-any-key-for-add-test");
// Click Add Model button by its text
await page.getByRole("button", { name: "Add Model" }).last().click();
// Wait for success notification
await expect(page.getByText("created successfully")).toBeVisible({ timeout: 15_000 });
// Navigate to All Models tab
await page.getByRole("tab", { name: "All Models" }).click();
await page.waitForLoadState("networkidle");
await page.waitForTimeout(2000);
// Search for the model we just added
await page.locator('input[placeholder="Search model names..."]').fill("claude-haiku-4-5");
await page.waitForTimeout(1000);
// Verify the model appears in the results count (not "Showing 0 results")
await expect(page.getByText(/Showing \d+ - \d+ of \d+ results/)).toBeVisible({ timeout: 15_000 });
// Verify the model name appears in the table body
const tableBody = page.locator("table tbody");
await expect(tableBody.getByText("claude-haiku-4-5").first()).toBeVisible({ timeout: 15_000 });
});
test("Add wildcard route and verify it appears in All Models", async ({ page }) => {
await navigateToPage(page, Page.Models);
await page.getByRole("tab", { name: "Add Model" }).click();
await selectProvider(page, "Cohere");
// Select All Cohere Models (Wildcard)
const modelDropdown = page.locator(".ant-select-selection-overflow").first();
await modelDropdown.click();
const wildcardOption = page.getByTitle(/All .* Models \(Wildcard\)/);
await wildcardOption.click();
await page.keyboard.press("Escape");
// Enter any API key
const apiKeyInput = page.locator('input[type="password"]').first();
await apiKeyInput.fill("sk-any-key-for-wildcard-test");
// Click Add Model button by its text
await page.getByRole("button", { name: "Add Model" }).last().click();
// Wait for success notification
await expect(page.getByText("created successfully")).toBeVisible({ timeout: 15_000 });
// Navigate to All Models tab
await page.getByRole("tab", { name: "All Models" }).click();
await page.waitForLoadState("networkidle");
await page.waitForTimeout(2000);
// Search for the wildcard model
await page.locator('input[placeholder="Search model names..."]').fill("cohere");
await page.waitForTimeout(1000);
// Verify the model appears in the results count (not "Showing 0 results")
await expect(page.getByText(/Showing \d+ - \d+ of \d+ results/)).toBeVisible({ timeout: 15_000 });
// Verify the wildcard model appears in the table body (wildcard models show as "cohere/*")
const tableBody = page.locator("table tbody");
await expect(tableBody.getByText("cohere/").first()).toBeVisible({ timeout: 15_000 });
});
});

View file

@ -365,6 +365,7 @@ const AllModelsTab = ({
<input
type="text"
placeholder="Search model names..."
data-testid="model-search-input"
className="w-full px-3 py-2 pl-8 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
value={modelNameSearch}
onChange={(e) => setModelNameSearch(e.target.value)}
@ -472,7 +473,7 @@ const AllModelsTab = ({
{isLoading ? (
<Skeleton.Input active style={{ width: 184, height: 20 }} />
) : (
<span className="text-sm text-gray-700">
<span data-testid="models-results-count" className="text-sm text-gray-700">
{paginationMeta.total_count > 0
? `Showing ${((currentPage - 1) * pageSize) + 1} - ${Math.min(currentPage * pageSize, paginationMeta.total_count)} of ${paginationMeta.total_count} results`
: "Showing 0 results"}

View file

@ -367,10 +367,10 @@ const AddModelForm: React.FC<AddModelFormProps> = ({
<Typography.Link href="https://github.com/BerriAI/litellm/issues">Need Help?</Typography.Link>
</Tooltip>
<div className="space-x-2">
<Button onClick={handleTestConnection} loading={isTestingConnection}>
<Button data-testid="test-connect-btn" onClick={handleTestConnection} loading={isTestingConnection}>
Test Connect
</Button>
<Button htmlType="submit">Add Model</Button>
<Button data-testid="add-model-btn" htmlType="submit">Add Model</Button>
</div>
</div>
</>

View file

@ -126,6 +126,7 @@ const LiteLLMModelNameField: React.FC<LiteLLMModelNameFieldProps> = ({
</>
) : providerModels.length > 0 ? (
<AntSelect
data-testid="model-name-select"
mode="multiple"
allowClear
showSearch

View file

@ -181,7 +181,7 @@ ${formattedBody}
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm193.5 301.7l-210.6 292a31.8 31.8 0 01-51.7 0L318.5 484.9c-3.8-5.3 0-12.7 6.5-12.7h46.9c10.2 0 19.9 4.9 25.9 13.3l71.2 98.8 157.2-218c6-8.3 15.6-13.3 25.9-13.3H699c6.5 0 10.3 7.4 6.5 12.7z"></path>
</svg>
</div>
<Text type="success" style={{ fontSize: "18px", fontWeight: 500, marginLeft: "10px" }}>
<Text data-testid="connection-success-msg" type="success" style={{ fontSize: "18px", fontWeight: 500, marginLeft: "10px" }}>
Connection to {modelName} successful!
</Text>
</div>
@ -190,7 +190,7 @@ ${formattedBody}
<div>
<div style={{ display: "flex", alignItems: "center", marginBottom: "20px" }}>
<WarningOutlined style={{ color: "#ff4d4f", fontSize: "24px", marginRight: "12px" }} />
<Text type="danger" style={{ fontSize: "18px", fontWeight: 500 }}>
<Text data-testid="connection-failure-msg" type="danger" style={{ fontSize: "18px", fontWeight: 500 }}>
Connection to {modelName} failed
</Text>
</div>