import { expect, test } from "@playwright/test"; import { gotoDashboardRoute } from "./helpers/dashboardAuth"; const DEFAULT_BAILIAN_URL = "https://coding-intl.dashscope.aliyuncs.com/apps/anthropic/v1"; test.describe("Bailian Coding Plan Provider", () => { test.describe.configure({ mode: "serial" }); test("default URL visible and editable in Add API Key modal", async ({ page }) => { const capturedPayloads: { createProvider?: Record } = {}; await page.route("**/api/providers", async (route) => { const method = route.request().method(); if (method === "GET") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ connections: [] }), }); return; } if (method === "POST") { const payload = route.request().postDataJSON(); capturedPayloads.createProvider = payload; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ connection: { id: "conn-bailian-test", provider: "bailian-coding-plan", name: payload.name || "Test Connection", testStatus: "active", providerSpecificData: payload.providerSpecificData, }, }), }); return; } await route.fulfill({ status: 405 }); }); await page.route("**/api/providers/validate", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ valid: true }), }); }); await page.route("**/api/provider-nodes", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ nodes: [] }), }); }); await gotoDashboardRoute(page, "/dashboard/providers/bailian-coding-plan"); await page.waitForLoadState("domcontentloaded"); // Dismiss any pre-existing dialog/overlay that may appear on page load const preExistingDialog = page.getByRole("dialog").first(); if (await preExistingDialog.isVisible({ timeout: 2000 }).catch(() => false)) { await page.keyboard.press("Escape"); await preExistingDialog.waitFor({ state: "hidden", timeout: 3000 }).catch(() => {}); } const addKeyButton = page.getByRole("button", { name: /add.*api.*key|add.*key|add.*connection|connect|adicionar.*chave/i, }); // Wait for the button to appear instead of immediately checking visibility await addKeyButton.first().waitFor({ state: "visible", timeout: 15000 }); await expect(addKeyButton.first()).toBeEnabled({ timeout: 5000 }); await addKeyButton.first().click(); 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 nameInput = dialog.getByLabel(/name/i).or(dialog.locator("input").first()); await nameInput.fill("Test Bailian Connection"); const apiKeyInput = dialog .getByLabel(/api.*key/i) .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, }) .last(); await expect(saveButton).toBeEnabled({ timeout: 15000 }); await saveButton.click(); await expect(dialog) .toBeHidden({ timeout: 10000 }) .catch(() => undefined); expect(capturedPayloads.createProvider).toBeDefined(); const payload = capturedPayloads.createProvider; expect(payload?.providerSpecificData).toBeDefined(); expect((payload?.providerSpecificData as Record)?.baseUrl).toBe(customUrl); }); test("invalid URL blocks save with validation error", async ({ page }) => { let createAttempted = false; await page.route("**/api/providers", async (route) => { const method = route.request().method(); if (method === "GET") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ connections: [] }), }); return; } if (method === "POST") { createAttempted = true; await route.fulfill({ status: 400, contentType: "application/json", body: JSON.stringify({ message: "Invalid request", details: [ { field: "providerSpecificData.baseUrl", message: "providerSpecificData.baseUrl must be a valid URL", }, ], }), }); return; } await route.fulfill({ status: 405 }); }); await page.route("**/api/providers/validate", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ valid: true }), }); }); await page.route("**/api/provider-nodes", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ nodes: [] }), }); }); await gotoDashboardRoute(page, "/dashboard/providers/bailian-coding-plan"); await page.waitForLoadState("domcontentloaded"); // Dismiss any pre-existing dialog/overlay that may appear on page load const preExistingDialog = page.getByRole("dialog").first(); if (await preExistingDialog.isVisible({ timeout: 2000 }).catch(() => false)) { await page.keyboard.press("Escape"); await preExistingDialog.waitFor({ state: "hidden", timeout: 3000 }).catch(() => {}); } const addKeyButton = page.getByRole("button", { name: /add.*api.*key|add.*key|add.*connection|connect|adicionar.*chave/i, }); // Wait for the button to appear instead of immediately checking visibility await addKeyButton.first().waitFor({ state: "visible", timeout: 15000 }); await expect(addKeyButton.first()).toBeEnabled({ timeout: 5000 }); await addKeyButton.first().click(); 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 nameInput = dialog.getByLabel(/name/i).or(dialog.locator("input").first()); await nameInput.fill("Test Invalid URL 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 saveButton.click(); // Wait for React to process the validation and re-render await page.waitForTimeout(1000); // 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); }); });