From 0ffc08b7e58e0bdcbb8f2b944211b920fda58d76 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Wed, 29 Jul 2026 08:41:41 -0300 Subject: [PATCH] test(e2e): repair the four shards the first green Build finally exercised MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test-e2e has `needs: [build]`, and the release PR's Build died on every round until now — so the 9-shard matrix produced ZERO signal for this whole cycle while ~200 PRs merged. The first successful Build surfaced four independent breakages, each traced to the commit that caused it: - providers-management (#7361): the single-connection delete moved from window.confirm() to a ConfirmModal, so page.once("dialog") never fired and the DELETE was never sent (deleteCalls stayed 0). Click the modal instead. - providers-bailian-coding-plan (#7882): the free-text Base URL field was deliberately replaced by a region step whose choice resolves the endpoint (global-sg -> coding-intl.dashscope, china-beijing -> coding.dashscope). Both cases rewritten against the region step; the invalid-URL case is unreachable from this modal now, so it covers the CN choice instead. - group-b-activity-feed: the stack-trace guard ran against page.content(), which embeds the serialized i18n payload — zenmux's "endpoint at /api/v1/chat/completions" is prose, not a leak. Assert on rendered innerText and require the :line:col every real stack frame carries. - navigation (#8292): APP_ROUTE_PATTERN accepted only /login and /dashboard, but the new prefetch spec is the sole caller passing /home, so waitForURL never resolved and the retry loop burned the full 180s timeout. E2E is green on main (9/9 on 07-22 and 07-23), so all four are cycle regressions, not pre-existing debt. Tests only — no production code touched. --- tests/e2e/group-b-activity-feed.spec.ts | 11 ++- tests/e2e/helpers/dashboardAuth.ts | 6 +- .../e2e/providers-bailian-coding-plan.spec.ts | 95 ++++++++----------- tests/e2e/providers-management.spec.ts | 10 +- 4 files changed, 61 insertions(+), 61 deletions(-) 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);