241 lines
8.2 KiB
TypeScript
241 lines
8.2 KiB
TypeScript
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<string, unknown> } = {};
|
|
|
|
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<string, unknown>)?.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);
|
|
});
|
|
});
|