diff --git a/tests/e2e/group-b-activity-feed.spec.ts b/tests/e2e/group-b-activity-feed.spec.ts index 8af335b1eb..4543d3cf1d 100644 --- a/tests/e2e/group-b-activity-feed.spec.ts +++ b/tests/e2e/group-b-activity-feed.spec.ts @@ -89,8 +89,13 @@ test.describe("Group B — Activity Feed", () => { await gotoDashboardRoute(page, "/dashboard/activity"); - const pageContent = await page.content(); - // Stack traces should never appear in the UI (Hard Rule #12) - expect(pageContent).not.toMatch(/\s+at\s+\//); + // Assert on what the user actually SEES, not on page.content(): the full HTML + // embeds the serialized i18n payload, where ordinary provider prose trips a + // naive stack-trace match ("...endpoint at /api/v1/chat/completions" — zenmux). + // innerText carries only rendered text, which is exactly what Hard Rule #12 is + // about. The pattern also requires the :line:col suffix every real stack frame + // has, so English sentences containing " at /" can never masquerade as a leak. + const visibleText = await page.locator("body").innerText(); + expect(visibleText).not.toMatch(/\s+at\s+\/\S*:\d+:\d+/); }); }); diff --git a/tests/e2e/helpers/dashboardAuth.ts b/tests/e2e/helpers/dashboardAuth.ts index d13ff83380..553470bdf2 100644 --- a/tests/e2e/helpers/dashboardAuth.ts +++ b/tests/e2e/helpers/dashboardAuth.ts @@ -6,7 +6,11 @@ type GotoDashboardRouteOptions = { }; const DEFAULT_TIMEOUT_MS = 300_000; -const APP_ROUTE_PATTERN = /\/(login|dashboard)(\/[^?#]*)?([?#].*)?$/; +// `home` belongs here too: it is an app route under the (dashboard) group but does +// NOT sit under the /dashboard prefix. Without it, waitForURL() never resolves for +// gotoDashboardRoute(page, "/home") — the retry loop just burns the whole test +// timeout with no assertion error, which is how #8292's prefetch spec hung. +const APP_ROUTE_PATTERN = /\/(login|dashboard|home)(\/[^?#]*)?([?#].*)?$/; const E2E_PASSWORD = process.env.OMNIROUTE_E2E_PASSWORD || process.env.INITIAL_PASSWORD || "omniroute-e2e-password"; diff --git a/tests/e2e/providers-bailian-coding-plan.spec.ts b/tests/e2e/providers-bailian-coding-plan.spec.ts index 1dd3704806..4194d6bf34 100644 --- a/tests/e2e/providers-bailian-coding-plan.spec.ts +++ b/tests/e2e/providers-bailian-coding-plan.spec.ts @@ -1,12 +1,17 @@ import { expect, test } from "@playwright/test"; import { gotoDashboardRoute } from "./helpers/dashboardAuth"; -const DEFAULT_BAILIAN_URL = "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1"; +// #7882 replaced this provider's free-text Base URL field with a region step: +// the endpoint is now derived from the choice ("global-sg" -> +// coding-intl.dashscope.aliyuncs.com, "china-beijing" -> coding.dashscope.aliyuncs.com, +// see src/shared/constants/alibabaProviderRegions.ts), so the modal persists +// providerSpecificData.region instead of a baseUrl. A per-connection base-URL +// override still exists, but it moved to Advanced in the edit-connection modal. test.describe("Bailian Coding Plan Provider", () => { test.describe.configure({ mode: "serial" }); - test("default URL visible and editable in Add API Key modal", async ({ page }) => { + test("region step persists the international (Singapore) choice", async ({ page }) => { const capturedPayloads: { createProvider?: Record } = {}; await page.route("**/api/providers", async (route) => { @@ -80,14 +85,10 @@ test.describe("Bailian Coding Plan Provider", () => { const dialog = page.getByRole("dialog").first(); await expect(dialog).toBeVisible({ timeout: 10000 }); - const baseUrlInput = dialog - .getByLabel(/base.*url/i) - .or(dialog.locator("input").filter({ has: page.locator("..").getByText(/base.*url/i) })); - - await expect(baseUrlInput).toBeVisible({ timeout: 15000 }); - - const inputValue = await baseUrlInput.inputValue(); - expect(inputValue).toBe(DEFAULT_BAILIAN_URL); + const regionStep = dialog.getByTestId("alibaba-region-step"); + await expect(regionStep).toBeVisible({ timeout: 15000 }); + await expect(regionStep.locator("[data-region]")).toHaveCount(2); + await regionStep.locator('[data-region="global-sg"]').click(); const nameInput = dialog.getByLabel(/name/i).or(dialog.locator("input").first()); await nameInput.fill("Test Bailian Connection"); @@ -97,9 +98,6 @@ test.describe("Bailian Coding Plan Provider", () => { .or(dialog.locator('input[type="password"]').first()); await apiKeyInput.fill("test-api-key-12345"); - const customUrl = "https://custom.example.com/anthropic/v1"; - await baseUrlInput.fill(customUrl); - const saveButton = dialog .getByRole("button", { name: /save|add|create|connect/i, @@ -115,11 +113,16 @@ test.describe("Bailian Coding Plan Provider", () => { expect(capturedPayloads.createProvider).toBeDefined(); const payload = capturedPayloads.createProvider; expect(payload?.providerSpecificData).toBeDefined(); - expect((payload?.providerSpecificData as Record)?.baseUrl).toBe(customUrl); + expect((payload?.providerSpecificData as Record)?.region).toBe("global-sg"); }); - test("invalid URL blocks save with validation error", async ({ page }) => { - let createAttempted = false; + // The old "invalid URL blocks save" case tested client-side validation of the + // free-text Base URL field, which #7882 removed for this provider — an invalid + // URL is no longer reachable from this modal. Replaced with the other half of + // the region contract: the China-mainland choice must persist as typed, since + // that is what selects the coding.dashscope.aliyuncs.com endpoint. + test("region step persists the China-mainland (Beijing) choice", async ({ page }) => { + const capturedPayloads: { createProvider?: Record } = {}; await page.route("**/api/providers", async (route) => { const method = route.request().method(); @@ -133,18 +136,19 @@ test.describe("Bailian Coding Plan Provider", () => { } if (method === "POST") { - createAttempted = true; + const payload = route.request().postDataJSON(); + capturedPayloads.createProvider = payload; await route.fulfill({ - status: 400, + status: 200, contentType: "application/json", body: JSON.stringify({ - message: "Invalid request", - details: [ - { - field: "providerSpecificData.baseUrl", - message: "providerSpecificData.baseUrl must be a valid URL", - }, - ], + connection: { + id: "conn-bailian-cn", + provider: "bailian-coding-plan", + name: payload.name || "Test Connection", + testStatus: "active", + providerSpecificData: payload.providerSpecificData, + }, }), }); return; @@ -191,50 +195,33 @@ test.describe("Bailian Coding Plan Provider", () => { const dialog = page.getByRole("dialog").first(); await expect(dialog).toBeVisible({ timeout: 10000 }); - const baseUrlInput = dialog - .getByLabel(/base.*url/i) - .or(dialog.locator("input").filter({ has: page.locator("..").getByText(/base.*url/i) })); - await expect(baseUrlInput).toBeVisible({ timeout: 15000 }); + const regionStep = dialog.getByTestId("alibaba-region-step"); + await expect(regionStep).toBeVisible({ timeout: 15000 }); + await regionStep.locator('[data-region="china-beijing"]').click(); const nameInput = dialog.getByLabel(/name/i).or(dialog.locator("input").first()); - await nameInput.fill("Test Invalid URL Connection"); + await nameInput.fill("Test Bailian CN Connection"); const apiKeyInput = dialog .getByLabel(/api.*key/i) .or(dialog.locator('input[type="password"]').first()); await apiKeyInput.fill("test-api-key-12345"); - await baseUrlInput.fill("not-a-url"); - const saveButton = dialog .getByRole("button", { name: /save|add|create|connect/i, }) .last(); + await expect(saveButton).toBeEnabled({ timeout: 15000 }); await saveButton.click(); - // Wait for React to process the validation and re-render - await page.waitForTimeout(1000); + await expect(dialog) + .toBeHidden({ timeout: 10000 }) + .catch(() => undefined); - // Check for the validation error scoped to the dialog to avoid strict-mode - // violations from broad selectors matching unrelated page elements. - const errorTextLocator = dialog - .locator("text=/invalid.*url|url.*invalid|must be a valid url|must use http/i") - .first(); - const errorClassLocator = dialog.locator(".text-red-500").first(); - - let errorVisible = - (await errorTextLocator.isVisible().catch(() => false)) || - (await errorClassLocator.isVisible().catch(() => false)); - - if (!errorVisible) { - // Fallback: if the dialog stays open after clicking save, it means the - // client-side validation prevented submission (which is the desired behavior). - await page.waitForTimeout(2000); - errorVisible = await dialog.isVisible().catch(() => false); - } - - expect(errorVisible).toBe(true); - expect(createAttempted).toBe(false); + expect(capturedPayloads.createProvider).toBeDefined(); + const payload = capturedPayloads.createProvider; + expect(payload?.providerSpecificData).toBeDefined(); + expect((payload?.providerSpecificData as Record)?.region).toBe("china-beijing"); }); }); diff --git a/tests/e2e/providers-management.spec.ts b/tests/e2e/providers-management.spec.ts index 3f43e0f67d..5662a6be06 100644 --- a/tests/e2e/providers-management.spec.ts +++ b/tests/e2e/providers-management.spec.ts @@ -312,10 +312,14 @@ test.describe("Providers management", () => { ).__providersTestState.forceInvalidValidation = false; }); - page.once("dialog", async (dialog) => { - await dialog.accept(); - }); + // #7361 replaced the native window.confirm() with a ConfirmModal, so the old + // page.once("dialog") handler never fires and the delete request was never sent. await page.getByTitle(/^delete$/i).click(); + const confirmDialog = page + .getByRole("dialog") + .filter({ hasText: /delete this connection/i }); + await expect(confirmDialog).toBeVisible({ timeout: 10000 }); + await confirmDialog.getByRole("button", { name: /^delete$/i }).click(); await expect.poll(async () => (await readProviderMockState(page)).deleteCalls).toBe(1); await expect(page.getByText("Primary OpenAI Edited")).toHaveCount(0);