mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
[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:
parent
8427534f13
commit
4f364a8138
5 changed files with 143 additions and 13 deletions
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ const LiteLLMModelNameField: React.FC<LiteLLMModelNameFieldProps> = ({
|
|||
</>
|
||||
) : providerModels.length > 0 ? (
|
||||
<AntSelect
|
||||
data-testid="model-name-select"
|
||||
mode="multiple"
|
||||
allowClear
|
||||
showSearch
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue