mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 21:02:12 +03:00
test(e2e): repair the four shards the first green Build finally exercised
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.
This commit is contained in:
@@ -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+/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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<string, unknown> } = {};
|
||||
|
||||
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<string, unknown>)?.baseUrl).toBe(customUrl);
|
||||
expect((payload?.providerSpecificData as Record<string, unknown>)?.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<string, unknown> } = {};
|
||||
|
||||
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<string, unknown>)?.region).toBe("china-beijing");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user