Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions tests/e2e/ui/helpers/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,21 @@ export async function createMcpServer(page: PwPage, url: string): Promise<string
await expect(discovery).toBeVisible({ timeout: 5_000 });
await discovery.getByRole("button", { name: /Custom Server/i }).click();

const formModal = page.locator(".ant-modal:visible").filter({ hasText: "MCP Server Name" });
const formModal = page.getByRole("dialog").filter({ hasText: "MCP Server Name" });
await expect(formModal).toBeVisible({ timeout: 5_000 });

// validateMCPServerName rejects spaces and hyphens; the worker index avoids a same-millisecond collision.
const name = `e2e_mcp_${process.env.TEST_WORKER_INDEX ?? "0"}_${Date.now()}`;
await formModal.locator('input[id="server_name"]').fill(name);
await formModal.getByLabel("MCP Server Name").fill(name);

const transportField = formModal.locator(".ant-form-item", { hasText: "Transport Type" });
await transportField.locator(".ant-select").click();
await page.locator(".ant-select-dropdown:visible").getByText("Streamable HTTP").click();
// Select popups are portaled to the body, so the option lookup is page-scoped, not modal-scoped.
await formModal.getByRole("combobox", { name: "Transport Type" }).click();
await page.getByRole("option", { name: "Streamable HTTP" }).click();

await formModal.locator('input[id="url"]').fill(url);
await formModal.getByLabel("MCP Server URL").fill(url);

// The auth_type Form.Item has no label prop, so anchor on the enclosing Collapse panel.
const authSection = formModal.locator(".ant-collapse-item", { hasText: /^Authentication/ });
await authSection.locator(".ant-form-item").first().locator(".ant-select").click();
await page.locator(".ant-select-dropdown:visible").getByText("None", { exact: true }).click();
await formModal.getByRole("combobox", { name: "Authentication", exact: true }).click();
await page.getByRole("option", { name: "None", exact: true }).click();

await formModal.getByRole("button", { name: /^Add MCP Server$/ }).click();
await expect(page.getByText("MCP Server created successfully").first()).toBeVisible({ timeout: 15_000 });
Expand Down
27 changes: 11 additions & 16 deletions tests/e2e/ui/tests/mcp/mcpServers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,31 +30,26 @@ test.describe("MCP Servers", () => {
await expect(discovery).toBeVisible({ timeout: 5_000 });
await discovery.getByRole("button", { name: /Custom Server/i }).click();

const formModal = page.locator(".ant-modal:visible").filter({ hasText: "MCP Server Name" });
const formModal = page.getByRole("dialog").filter({ hasText: "MCP Server Name" });
await expect(formModal).toBeVisible({ timeout: 5_000 });

// Name — no spaces or hyphens per validateMCPServerName
const uniqueName = `e2e_mcp_${Date.now()}`;
createdServerName = uniqueName;
await formModal.locator('input[id="server_name"]').fill(uniqueName);
await formModal.getByLabel("MCP Server Name").fill(uniqueName);

// Transport: Streamable HTTP — the only value the proxy actually accepts is "http"
const transportField = formModal.locator(".ant-form-item", { hasText: "Transport Type" });
await transportField.locator(".ant-select").click();
await page.locator(".ant-select-dropdown:visible").getByText("Streamable HTTP").click();
// Transport: Streamable HTTP — the only value the proxy actually accepts is "http".
// Select popups are portaled to the body, so the option lookup is page-scoped.
await formModal.getByRole("combobox", { name: "Transport Type" }).click();
await page.getByRole("option", { name: "Streamable HTTP" }).click();

// URL — use a fake URL; the form just persists it, it doesn't have to be reachable
await formModal.locator('input[id="url"]').fill("https://e2e-fake-mcp.test.local/mcp");
await formModal.getByLabel("MCP Server URL").fill("https://e2e-fake-mcp.test.local/mcp");

// Authentication: None
// The auth_type Form.Item has no label prop (CreateMCPServer.tsx), so
// it can't be anchored by label text. Scope via the enclosing Collapse
// panel ("Authentication") instead — that anchor is stable even if the
// placeholder copy changes.
const authSection = formModal.locator(".ant-collapse-item", { hasText: /^Authentication/ });
const authField = authSection.locator(".ant-form-item").first();
await authField.locator(".ant-select").click();
await page.locator(".ant-select-dropdown:visible").getByText("None", { exact: true }).click();
// Authentication: None. "Authentication" is exact so it can't also match the
// "Authentication Value" field that some auth types reveal below it.
await formModal.getByRole("combobox", { name: "Authentication", exact: true }).click();
await page.getByRole("option", { name: "None", exact: true }).click();

// Submit
await formModal.getByRole("button", { name: /^Add MCP Server$/ }).click();
Expand Down
61 changes: 30 additions & 31 deletions tests/e2e/ui/tests/modelsPage/addModel.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,22 @@ async function findDeploymentByName(page: PlaywrightPage, modelName: string): Pr
return body.data.find((row) => row.model_name === modelName);
}

/** Anchors a substring match to the whole string, escaping regex metacharacters. */
const exactly = (text: string): RegExp => new RegExp(`^${text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`);

/**
* Helper to select a provider from the Add Model form dropdown.
* Helper to select a provider from the Add Model form dropdown. The field is a
* searchable combobox: it only opens on click, typing filters the list, and the
* option has to be picked explicitly because nothing is highlighted by default.
* Options are matched on their visible text, not their accessible name, which
* also carries the provider logo's alt text ("Anthropic logo Anthropic").
*/
async function selectProvider(page: any, providerName: string) {
const providerDropdown = page.getByRole("combobox", { name: /Provider/i });
async function selectProvider(page: PlaywrightPage, providerName: string) {
const providerDropdown = page.getByRole("combobox", { name: "Provider", exact: true });
await providerDropdown.click();
await providerDropdown.fill(providerName);
await page.waitForTimeout(1000);
await providerDropdown.press("Enter");
await page.waitForTimeout(2000);
await page.getByRole("option").filter({ hasText: exactly(providerName) }).click();
await expect(providerDropdown).toHaveValue(providerName);
}

test.describe("Add Model", () => {
Expand Down Expand Up @@ -64,11 +71,10 @@ test.describe("Add Model", () => {
await selectProvider(page, "Anthropic");

// 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();
await page.getByRole("combobox", { name: "Select models" }).click();

// Verify provider-specific models are listed
await expect(page.getByTitle("claude-haiku-4-5", { exact: true })).toBeVisible();
await expect(page.getByRole("option", { name: "claude-haiku-4-5", exact: true })).toBeVisible();
});

test("Edit team model TPM and RPM limits", async ({ page }) => {
Expand Down Expand Up @@ -156,14 +162,14 @@ test.describe("Add Model", () => {
await page.getByRole("tab", { name: "Add Model" }).click();

// Labels come from /public/providers/fields, not the frontend Providers enum, and the two differ.
await selectProvider(page, "OpenAI-Compatible Endpoints");
await selectProvider(page, "OpenAI-Compatible Endpoints (Together AI, etc.)");

const publicName = `e2e-ui-added-${Date.now()}`;
uiAddedModelName = publicName;

// The model picker's "custom" entry reveals the free-text name field.
await page.locator(".ant-select-selection-overflow").first().click();
await page.locator(".ant-select-dropdown:visible").getByText("Custom Model Name (Enter below)").click();
await page.getByRole("combobox", { name: "Select models" }).click();
await page.getByRole("option", { name: "Custom Model Name (Enter below)" }).click();
await page.keyboard.press("Escape");
await page.getByPlaceholder("Enter custom model name").fill(publicName);

Expand All @@ -177,8 +183,8 @@ test.describe("Add Model", () => {
await expect(page.getByTestId("connection-success-msg")).toBeVisible({ timeout: 30_000 });

// The modal swallows the Add click. Scope to the footer: the dismiss X is also named "Close".
const resultsModal = page.locator(".ant-modal:visible").filter({ hasText: "Connection Test Results" });
await resultsModal.locator(".ant-modal-footer").getByRole("button", { name: "Close" }).click();
const resultsModal = page.getByRole("dialog", { name: "Connection Test Results" });
await resultsModal.locator('[data-slot="dialog-footer"]').getByRole("button", { name: "Close" }).click();
await expect(resultsModal).toBeHidden({ timeout: 5_000 });

const created = await captureRequestBody(page, { method: "POST", urlIncludes: "/model/new" }, async () => {
Expand Down Expand Up @@ -213,9 +219,8 @@ test.describe("Add Model", () => {
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.getByRole("combobox", { name: "Select models" }).click();
await page.getByRole("option", { name: "claude-haiku-4-5", exact: true }).click();
await page.keyboard.press("Escape");

// Enter bad API key
Expand All @@ -239,9 +244,8 @@ test.describe("Add Model", () => {
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.getByRole("combobox", { name: "Select models" }).click();
await page.getByRole("option", { name: "claude-haiku-4-5", exact: true }).click();
await page.keyboard.press("Escape");

// Enter any API key
Expand Down Expand Up @@ -315,18 +319,15 @@ test.describe("Add Model", () => {

await selectProvider(page, "Cohere");

const modelDropdown = page.locator(".ant-select-selection-overflow").first();
await modelDropdown.click();
const wildcardOption = page.getByTitle(/All .* Models \(Wildcard\)/);
await wildcardOption.click();
await page.getByRole("combobox", { name: "Select models" }).click();
await page.getByRole("option", { name: /All .* Models \(Wildcard\)/ }).click();
await page.keyboard.press("Escape");

const apiKeyInput = page.locator('input[type="password"]').first();
await apiKeyInput.fill("sk-any-key-for-team-byok-test");

// Flip the Team-BYOK switch on (Form.Item label "Team-BYOK Model")
const teamByokRow = page.locator(".ant-form-item", { hasText: "Team-BYOK Model" });
await teamByokRow.getByRole("switch").click();
// Flip the Team-BYOK switch on; the Switch carries its own aria-label.
await page.getByRole("switch", { name: "Team-BYOK Model" }).click();

// TeamDropdown options show the alias above the team id, so match on the id line by text.
const teamDropdown = page.getByTestId("team-dropdown").getByRole("combobox");
Expand Down Expand Up @@ -376,10 +377,8 @@ test.describe("Add Model", () => {
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.getByRole("combobox", { name: "Select models" }).click();
await page.getByRole("option", { name: /All .* Models \(Wildcard\)/ }).click();
await page.keyboard.press("Escape");

// Enter any API key
Expand Down
2 changes: 1 addition & 1 deletion tests/e2e/ui/tests/modelsPage/credentials.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ test.describe("Edit LLM credential", () => {
await row.getByTestId(`credential-actions-${credentialName}`).click();
await page.getByTestId("credential-action-edit").click();

const modal = page.locator(".ant-modal-content").filter({ hasText: "Edit Credential" });
const modal = page.getByRole("dialog", { name: "Edit Credential" });
await expect(modal).toBeVisible({ timeout: 10_000 });

const apiKeyField = modal.locator("#api_key");
Expand Down
27 changes: 10 additions & 17 deletions tests/e2e/ui/tests/proxy-admin/keys.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ test.describe("Proxy Admin - Keys", () => {
await page.keyboard.type(E2E_TEAM_CRUD_ALIAS);
await page.locator('[data-slot="combobox-content"]:visible').getByText(E2E_TEAM_CRUD_ALIAS).first().click();

// Select models
await page.locator(".ant-select-selection-overflow").click();
await page.locator(".ant-select-dropdown:visible").getByText("All Team Models").click();
// Select models — the popup is portaled to the body, so scope options to the page.
await page.getByRole("combobox", { name: "Select models" }).click();
await page.getByRole("option", { name: "All Team Models", exact: true }).click();
await page.keyboard.press("Escape");

// Submit
Expand Down Expand Up @@ -86,7 +86,7 @@ test.describe("Proxy Admin - Keys", () => {
// Scope to the modal — the Regenerate button has an icon whose aria-label
// ("sync") is concatenated into the button's accessible name, and the
// "Regenerate Key" button is still in the DOM behind the modal.
const modal = page.locator(".ant-modal:visible");
const modal = page.getByRole("dialog", { name: "Regenerate Virtual Key" });
await modal.getByRole("button", { name: /Regenerate/ }).click();

// Success view shows a Copy button in the footer (text varies between modal versions)
Expand Down Expand Up @@ -198,8 +198,8 @@ test.describe("Proxy Admin - Keys", () => {
// Select models — open the multi-select and pick the all-models meta-option.
// With no team selected the modal offers "All Proxy Models"; the team-scoped
// "All Team Models" option only appears once a team is picked.
await page.locator(".ant-select-selection-overflow").click();
await page.locator(".ant-select-dropdown:visible").getByText("All Proxy Models").click();
await page.getByRole("combobox", { name: "Select models" }).click();
await page.getByRole("option", { name: "All Proxy Models", exact: true }).click();
await page.keyboard.press("Escape");

await page.getByRole("button", { name: "Create Key", exact: true }).click();
Expand All @@ -221,17 +221,10 @@ test.describe("Proxy Admin - Keys", () => {
const keyName = `e2e-admin-specific-${Date.now()}`;
await page.getByLabel(/Key Name/).fill(keyName);

// Open the model multi-select and pick a single specific model. Use
// getByRole("option", ...) to avoid the strict-mode collision between
// the option container and its inner text node.
// Open the model multi-select and pick a single specific model.
const modelName = "fake-openai-gpt-4";
await page.locator(".ant-select-selection-overflow").click();
const option = page.locator(".ant-select-dropdown:visible").getByRole("option", { name: modelName, exact: true });
await option.waitFor({ state: "attached" });
// Dispatch the click via the DOM — antd's dropdown can render the option
// off-viewport during the open animation, which trips Playwright's
// visibility/stability checks. The click handler fires regardless.
await option.evaluate((el: HTMLElement) => el.click());
await page.getByRole("combobox", { name: "Select models" }).click();
await page.getByRole("option", { name: modelName, exact: true }).click();
await page.keyboard.press("Escape");

await page.getByRole("button", { name: "Create Key", exact: true }).click();
Expand All @@ -242,7 +235,7 @@ test.describe("Proxy Admin - Keys", () => {
// verify it can call /chat/completions for the model it was scoped to.
// The mock LLM server (fixtures/mock_llm_server/server.py) replies with
// a fixed "This is a mock response." body.
const apiKey = (await page.locator(".ant-modal:visible pre").innerText()).trim();
const apiKey = (await page.getByRole("dialog", { name: "Save your Key" }).locator("pre").innerText()).trim();
expect(apiKey).toMatch(/^sk-/);

const response = await page.request.post("/chat/completions", {
Expand Down
13 changes: 7 additions & 6 deletions tests/e2e/ui/tests/proxy-admin/teams.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,12 @@ test.describe("Proxy Admin - Teams", () => {
.click();

// Wait for the Create Team modal
const dialog = page.locator(".ant-modal:visible");
const dialog = page.getByRole("dialog", { name: "Create Team" });
await expect(dialog).toBeVisible({ timeout: 5_000 });

// Fill Team Name — the input has id="team_alias"
await dialog.locator("#team_alias").fill(uniqueAlias);
// Fill Team Name — FormField derives the control id from React.useId(), so
// the input is only addressable by its label or its test id.
await dialog.getByTestId("team-name-input").fill(uniqueAlias);

// Select models — the models multi-select is inside the modal. Its popup is
// portaled to the body, so scope the option lookup to the page, not the dialog.
Expand Down Expand Up @@ -75,7 +76,7 @@ test.describe("Proxy Admin - Teams", () => {
await page.getByRole("button", { name: /Add Member/i }).click();

// Wait for Add Team Member modal
const modal = page.locator(".ant-modal:visible");
const modal = page.getByRole("dialog", { name: "Add Team Member" });
await expect(modal).toBeVisible({ timeout: 5_000 });

// The email field is a Select — type to search, then select from dropdown
Expand Down Expand Up @@ -112,7 +113,7 @@ test.describe("Proxy Admin - Teams", () => {

await page.getByTestId("edit-member").first().click();

const modal = page.locator(".ant-modal:visible");
const modal = page.getByRole("dialog", { name: "Edit Member" });
await expect(modal).toBeVisible({ timeout: 5_000 });
await modal.getByRole("button", { name: /Save Changes/i }).click();

Expand Down Expand Up @@ -155,7 +156,7 @@ test.describe("Proxy Admin - Teams", () => {

await page.getByTestId("edit-member").first().click();

const modal = page.locator(".ant-modal:visible");
const modal = page.getByRole("dialog", { name: "Edit Member" });
await expect(modal).toBeVisible({ timeout: 5_000 });
await modal.getByRole("button", { name: /Save Changes/i }).click();

Expand Down
Loading
Loading