[Test] UI - Models: Add E2E tests for Add Model flow

Add E2E tests covering:
- Test connection with bad credentials shows failure modal
- Adding a specific model and verifying it appears in All Models table
- Adding a wildcard route and verifying it appears in All Models table
- Verifying model dropdown shows provider-specific models (existing test updated)

Added data-testid attributes to UI components to support stable test selectors.

Tests verified passing 3/3 consecutive runs with zero flakiness.
This commit is contained in:
Yuneng Jiang 2026-04-11 20:06:56 -07:00
parent 8427534f13
commit 4f364a8138
No known key found for this signature in database
5 changed files with 143 additions and 13 deletions

View file

@ -1,24 +1,29 @@
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";
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");
const providerDropdown = page.getByRole("combobox", { name: /Provider/i });
await providerDropdown.fill("Anthropic");
await page.waitForTimeout(1000);
await providerInputDropdown.press("Enter");
await providerDropdown.press("Enter");
await page.waitForTimeout(2000);
const providerModelsDropdown = page.locator(".ant-select-selection-overflow").first();
await providerModelsDropdown.click();
// The model field should be a multi-select dropdown (not a text input)
const modelSelect = page.getByTestId("model-name-select");
await expect(modelSelect).toBeVisible({ timeout: 10_000 });
// Click to open the dropdown and verify provider-specific models are listed
const modelDropdown = page.locator(".ant-select-selection-overflow").first();
await modelDropdown.click();
await expect(page.getByTitle("claude-haiku-4-5", { exact: true })).toBeVisible();
});
@ -72,4 +77,127 @@ 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();
// Select provider: Anthropic
const providerDropdown = page.getByRole("combobox", { name: /Provider/i });
await providerDropdown.fill("Anthropic");
await page.waitForTimeout(1000);
await providerDropdown.press("Enter");
await page.waitForTimeout(2000);
// 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
await page.getByTestId("test-connect-btn").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.getByTestId("connection-failure-msg")).toBeVisible({ timeout: 30_000 });
await expect(page.getByTestId("connection-failure-msg")).toContainText("failed");
});
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();
// Select provider: Anthropic
const providerDropdown = page.getByRole("combobox", { name: /Provider/i });
await providerDropdown.fill("Anthropic");
await page.waitForTimeout(1000);
await providerDropdown.press("Enter");
await page.waitForTimeout(2000);
// 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
await page.getByTestId("add-model-btn").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.getByTestId("model-search-input").fill("claude-haiku-4-5");
await page.waitForTimeout(1000);
// Verify the model appears in the results count (not "Showing 0 results")
const resultsCount = page.getByTestId("models-results-count");
await expect(resultsCount).not.toHaveText("Showing 0 results", { 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();
// Select provider: Cohere
const providerDropdown = page.getByRole("combobox", { name: /Provider/i });
await providerDropdown.fill("Cohere");
await page.waitForTimeout(1000);
await providerDropdown.press("Enter");
await page.waitForTimeout(2000);
// 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
await page.getByTestId("add-model-btn").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.getByTestId("model-search-input").fill("cohere");
await page.waitForTimeout(1000);
// Verify the model appears in the results count (not "Showing 0 results")
const resultsCount = page.getByTestId("models-results-count");
await expect(resultsCount).not.toHaveText("Showing 0 results", { 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>