import { expect, test, type Page, type Route } from "@playwright/test"; import { gotoDashboardRoute } from "./helpers/dashboardAuth"; const NAVIGATION_TIMEOUT_MS = 300_000; const UI_STABILITY_TIMEOUT_MS = 120_000; type ApiKeyRecord = { id: string; name: string; key: string; fullKey: string; allowedModels: string[] | null; allowedConnections: string[] | null; createdAt: string; }; async function fulfillJson(route: Route, body: unknown, status = 200) { await route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body), }); } async function installClipboardMock(page: Page) { await page.addInitScript(() => { let clipboardValue = ""; Object.defineProperty(window, "__clipboardValue", { configurable: true, get: () => clipboardValue, set: (value) => { clipboardValue = typeof value === "string" ? value : String(value ?? ""); }, }); Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText: async (value: string) => { (window as Window & { __clipboardValue?: string }).__clipboardValue = value; }, readText: async () => clipboardValue, }, }); }); } async function readClipboard(page: Page) { return page.evaluate(() => (window as Window & { __clipboardValue?: string }).__clipboardValue); } async function waitForPageToSettle(page: Page) { try { await page.waitForLoadState("networkidle", { timeout: 15_000 }); } catch { // Some dashboard pages keep background requests alive; visibility assertions below // are the authoritative readiness check for these E2E flows. } } async function waitForNextDevCompileToFinish(page: Page) { const nextDevToolsButton = page.getByRole("button", { name: /open next\.js dev tools/i }); if ((await nextDevToolsButton.count()) === 0) return; await expect(nextDevToolsButton).not.toContainText(/compiling/i, { timeout: 120_000 }); } test.describe("API keys flow", () => { test.setTimeout(600_000); test("creates, copies, reveals, revokes, and returns to the empty state", async ({ page }) => { const state: { keys: ApiKeyRecord[]; nextId: number; revealCalls: number; deleteCalls: number; } = { keys: [], nextId: 1, revealCalls: 0, deleteCalls: 0, }; await installClipboardMock(page); await page.route("**/v1/models", async (route) => { await fulfillJson(route, { data: [] }); }); await page.route("**/api/settings", async (route) => { await fulfillJson(route, {}); }); await page.route("**/api/providers", async (route) => { await fulfillJson(route, { connections: [ { id: "conn-openai", name: "OpenAI Main", provider: "openai", isActive: true }, ], }); }); await page.route(/\/api\/usage\/call-logs(?:\?.*)?$/, async (route) => { await fulfillJson(route, []); }); await page.route("**/api/sessions", async (route) => { await fulfillJson(route, { byApiKey: {} }); }); await page.route(/\/api\/keys\/[^/]+\/reveal$/, async (route) => { state.revealCalls += 1; const keyId = route.request().url().split("/").slice(-2)[0]; const record = state.keys.find((key) => key.id === keyId); await fulfillJson(route, { key: record?.fullKey ?? "" }); }); await page.route(/\/api\/keys\/[^/]+$/, async (route) => { if (route.request().method() === "DELETE") { state.deleteCalls += 1; const keyId = route.request().url().split("/").pop() || ""; state.keys = state.keys.filter((key) => key.id !== keyId); await fulfillJson(route, { success: true }); return; } if (route.request().method() === "PATCH") { const keyId = route.request().url().split("/").pop() || ""; const payload = (await route.request().postDataJSON()) as { name?: string }; const record = state.keys.find((key) => key.id === keyId); if (!record) { await fulfillJson(route, { error: "Key not found" }, 404); return; } if (payload.name) { record.name = payload.name; } await fulfillJson(route, { message: "API key settings updated successfully", ...payload }); return; } await fulfillJson(route, { error: "Method not allowed in api key detail stub" }, 405); }); await page.route("**/api/keys", async (route) => { const method = route.request().method(); if (method === "GET") { await fulfillJson(route, { keys: state.keys.map(({ fullKey, ...record }) => record), allowKeyReveal: true, }); return; } if (method === "POST") { const payload = (route.request().postDataJSON() as { name?: string }) || {}; const id = `key-${state.nextId++}`; const suffix = String(1000 + state.nextId); const fullKey = `sk-live-${suffix}-demo-secret`; const maskedKey = `sk-live-****${suffix}`; state.keys.push({ id, name: payload.name || "New Key", key: maskedKey, fullKey, allowedModels: null, allowedConnections: null, createdAt: new Date("2026-04-05T20:00:00.000Z").toISOString(), }); await fulfillJson(route, { key: fullKey, id }); return; } await fulfillJson(route, { error: "Method not allowed in api keys stub" }, 405); }); await gotoDashboardRoute(page, "/dashboard/api-manager", { timeoutMs: NAVIGATION_TIMEOUT_MS, }); await waitForPageToSettle(page); await waitForNextDevCompileToFinish(page); const createFirstKeyButton = page.getByRole("button", { name: /create (your )?first key/i, }); await expect(createFirstKeyButton).toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS }); await waitForPageToSettle(page); await waitForNextDevCompileToFinish(page); await createFirstKeyButton.click(); const createDialog = page.getByRole("dialog", { name: /create api key/i }); await expect(createDialog).toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS }); await createDialog.locator("input").first().fill("Team Key"); const createKeyButton = createDialog.getByRole("button", { name: /create api key/i }); await expect(createKeyButton).toBeEnabled({ timeout: UI_STABILITY_TIMEOUT_MS }); await createKeyButton.click({ force: true }); await expect.poll(() => state.keys.length).toBe(1); const createdDialog = page.getByRole("dialog", { name: /api key created/i }); const createdKeyInput = createdDialog.locator("input[readonly]").first(); await expect(createdKeyInput).toHaveValue(/sk-live-/); await createdDialog.getByRole("button", { name: /copy/i }).click(); await expect.poll(() => readClipboard(page)).toBeTruthy(); const createdClipboardValue = await readClipboard(page); await expect(createdKeyInput).toHaveValue(createdClipboardValue || ""); await createdDialog.getByRole("button", { name: /done/i }).click(); await expect(page.getByText("Team Key")).toBeVisible(); await expect(page.getByText("sk-live-****1002")).toBeVisible(); const keyRow = page .locator("div") .filter({ has: page.getByText("Team Key", { exact: true }) }) .filter({ has: page.getByText("sk-live-****1002", { exact: true }) }) .first(); await keyRow.getByRole("button", { name: /copy/i }).click(); await expect.poll(() => state.revealCalls).toBe(1); await expect.poll(() => readClipboard(page)).toBe("sk-live-1002-demo-secret"); page.once("dialog", async (dialog) => { await dialog.accept(); }); await keyRow.locator("button[title]").last().click({ force: true }); await expect.poll(() => state.deleteCalls).toBe(1); await expect(page.getByText("Team Key")).toHaveCount(0); await expect(createFirstKeyButton).toBeVisible(); }); test("renames a key through the permissions modal", async ({ page }) => { const state: { keys: ApiKeyRecord[]; nextId: number; } = { keys: [], nextId: 1, }; await page.route("**/v1/models", async (route) => { await fulfillJson(route, { data: [] }); }); await page.route("**/api/settings", async (route) => { await fulfillJson(route, {}); }); await page.route("**/api/providers", async (route) => { await fulfillJson(route, { connections: [ { id: "conn-openai", name: "OpenAI Main", provider: "openai", isActive: true }, ], }); }); await page.route(/\/api\/usage\/call-logs(?:\?.*)?$/, async (route) => { await fulfillJson(route, []); }); await page.route("**/api/sessions", async (route) => { await fulfillJson(route, { byApiKey: {} }); }); await page.route(/\/api\/keys\/[^/]+$/, async (route) => { if (route.request().method() === "PATCH") { const keyId = route.request().url().split("/").pop() || ""; const payload = (await route.request().postDataJSON()) as { name?: string }; const record = state.keys.find((key) => key.id === keyId); if (!record) { await fulfillJson(route, { error: "Key not found" }, 404); return; } if (payload.name) { record.name = payload.name; } await fulfillJson(route, { message: "API key settings updated successfully", ...payload }); return; } await fulfillJson(route, { error: "Method not allowed" }, 405); }); await page.route("**/api/keys", async (route) => { const method = route.request().method(); if (method === "GET") { await fulfillJson(route, { keys: state.keys.map(({ fullKey, ...record }) => record), allowKeyReveal: true, }); return; } if (method === "POST") { const payload = (route.request().postDataJSON() as { name?: string }) || {}; const id = `key-${state.nextId++}`; const suffix = String(1000 + state.nextId); const fullKey = `sk-live-${suffix}-demo-secret`; const maskedKey = `sk-live-****${suffix}`; state.keys.push({ id, name: payload.name || "New Key", key: maskedKey, fullKey, allowedModels: null, allowedConnections: null, createdAt: new Date("2026-04-05T20:00:00.000Z").toISOString(), }); await fulfillJson(route, { key: fullKey, id }); return; } await fulfillJson(route, { error: "Method not allowed" }, 405); }); await gotoDashboardRoute(page, "/dashboard/api-manager", { timeoutMs: NAVIGATION_TIMEOUT_MS, }); await waitForPageToSettle(page); await waitForNextDevCompileToFinish(page); // --- Create a key --- const createFirstKeyButton = page.getByRole("button", { name: /create (your )?first key/i, }); await expect(createFirstKeyButton).toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS }); await waitForPageToSettle(page); await waitForNextDevCompileToFinish(page); await createFirstKeyButton.click(); const createDialog = page.getByRole("dialog", { name: /create api key/i }); await expect(createDialog).toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS }); await createDialog.locator("input").first().fill("Original Name"); const createKeyButton = createDialog.getByRole("button", { name: /create api key/i }); await expect(createKeyButton).toBeEnabled({ timeout: UI_STABILITY_TIMEOUT_MS }); await createKeyButton.click({ force: true }); await expect.poll(() => state.keys.length).toBe(1); // Dismiss the "key created" dialog const createdDialog = page.getByRole("dialog", { name: /api key created/i }); await createdDialog.getByRole("button", { name: /done/i }).click(); // Wait for the key list to settle after re-fetch await waitForPageToSettle(page); await waitForNextDevCompileToFinish(page); // Verify the key appears in the list await expect(page.getByText("Original Name")).toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS, }); // --- Open the permissions modal --- // The edit-permissions button is opacity-0 until hover; use title attribute locator const keyRow = page .locator("div") .filter({ has: page.getByText("Original Name", { exact: true }) }) .first(); await keyRow.locator('button[title="Edit permissions"]').click({ force: true }); // --- Rename the key --- const permissionsDialog = page.getByRole("dialog", { name: /permissions: original name/i }); await expect(permissionsDialog).toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS }); // The key name input is inside the permissions modal const nameInput = permissionsDialog.locator("input").first(); await expect(nameInput).toHaveValue("Original Name"); await nameInput.clear(); await nameInput.fill("Renamed Key"); // Save await permissionsDialog .getByRole("button", { name: /save permissions/i }) .click({ force: true }); // Verify the mock state was updated await expect.poll(() => state.keys[0]?.name).toBe("Renamed Key"); // Verify the dialog closes and the list reflects the new name await expect(permissionsDialog).not.toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS }); await expect(page.getByText("Renamed Key")).toBeVisible(); }); test("validation error appears inside the create key modal, not behind the backdrop", async ({ page, }) => { const state = { keys: [] as ApiKeyRecord[], nextId: 1, }; // Stub all API routes await page.route("**/v1/models", async (route) => { await fulfillJson(route, { data: [] }); }); await page.route("**/api/connections", async (route) => { await fulfillJson(route, { connections: [ { id: "conn-openai", name: "OpenAI Main", provider: "openai", isActive: true }, ], }); }); await page.route(/\/api\/usage\/call-logs(?:\?.*)?$/, async (route) => { await fulfillJson(route, []); }); await page.route("**/api/sessions", async (route) => { await fulfillJson(route, { byApiKey: {} }); }); await page.route("**/api/keys", async (route) => { const method = route.request().method(); if (method === "GET") { await fulfillJson(route, { keys: state.keys.map(({ fullKey, ...record }) => record), allowKeyReveal: true, }); return; } if (method === "POST") { const payload = (route.request().postDataJSON() as { name?: string }) || {}; const id = `key-${state.nextId++}`; const suffix = String(1000 + state.nextId); const fullKey = `sk-live-${suffix}-demo-secret`; const maskedKey = `sk-live-****${suffix}`; state.keys.push({ id, name: payload.name || "New Key", key: maskedKey, fullKey, allowedModels: null, allowedConnections: null, createdAt: new Date().toISOString(), }); await fulfillJson(route, { key: fullKey, id }); return; } await fulfillJson(route, { error: "Method not allowed" }, 405); }); await page.route(/\/api\/keys\/[^/]+$/, async (route) => { await fulfillJson(route, { error: "Not found" }, 404); }); await page.route(/\/api\/keys\/[^/]+\/reveal$/, async (route) => { await fulfillJson(route, { key: "" }); }); await gotoDashboardRoute(page, "/dashboard/api-manager", { timeoutMs: NAVIGATION_TIMEOUT_MS, }); await waitForPageToSettle(page); await waitForNextDevCompileToFinish(page); // Open the create key modal const createFirstKeyButton = page.getByRole("button", { name: /create (your )?first key/i, }); await expect(createFirstKeyButton).toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS }); await createFirstKeyButton.click(); const createDialog = page.getByRole("dialog", { name: /create api key/i }); await expect(createDialog).toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS }); // Fill an invalid name (special characters) and submit const nameInput = createDialog.locator("input").first(); await nameInput.fill("bad@name!"); const createKeyButton = createDialog.getByRole("button", { name: /create api key/i }); await createKeyButton.click({ force: true }); // The validation error must appear as an inline
INSIDE the modal const inlineError = createDialog.locator("p[role='alert']"); await expect(inlineError).toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS }); // The error must be visible — i.e. not obscured by the backdrop. // A covered element has box visibility but fails the check below because // pointer events and paint are blocked by the overlay div. await expect(inlineError).toBeInViewport(); // No page-level error banner should appear behind the modal const pageErrorBanner = page.locator('.flex.flex-col.gap-8 > [class~="bg-red-500/10"]'); await expect(pageErrorBanner).not.toBeVisible(); // Typing should clear the error await nameInput.fill("valid-name"); await expect(inlineError).not.toBeVisible(); // A valid name should succeed without errors await createKeyButton.click({ force: true }); await expect.poll(() => state.keys.length).toBe(1); }); test("validation error appears inside the permissions modal when renaming, not behind the backdrop", async ({ page, }) => { const state: { keys: ApiKeyRecord[]; nextId: number; } = { keys: [], nextId: 1, }; // Stub all API routes await page.route("**/v1/models", async (route) => { await fulfillJson(route, { data: [] }); }); await page.route("**/api/settings", async (route) => { await fulfillJson(route, {}); }); await page.route("**/api/providers", async (route) => { await fulfillJson(route, { connections: [ { id: "conn-openai", name: "OpenAI Main", provider: "openai", isActive: true }, ], }); }); await page.route(/\/api\/usage\/call-logs(?:\?.*)?$/, async (route) => { await fulfillJson(route, []); }); await page.route("**/api/sessions", async (route) => { await fulfillJson(route, { byApiKey: {} }); }); await page.route(/\/api\/keys\/[^/]+$/, async (route) => { if (route.request().method() === "PATCH") { const keyId = route.request().url().split("/").pop() || ""; const payload = (await route.request().postDataJSON()) as { name?: string }; const record = state.keys.find((key) => key.id === keyId); if (!record) { await fulfillJson(route, { error: "Key not found" }, 404); return; } if (payload.name) { record.name = payload.name; } await fulfillJson(route, { message: "API key settings updated successfully", ...payload }); return; } await fulfillJson(route, { error: "Method not allowed" }, 405); }); await page.route("**/api/keys", async (route) => { const method = route.request().method(); if (method === "GET") { await fulfillJson(route, { keys: state.keys.map(({ fullKey, ...record }) => record), allowKeyReveal: true, }); return; } if (method === "POST") { const payload = (route.request().postDataJSON() as { name?: string }) || {}; const id = `key-${state.nextId++}`; const suffix = String(1000 + state.nextId); const fullKey = `sk-live-${suffix}-demo-secret`; const maskedKey = `sk-live-****${suffix}`; state.keys.push({ id, name: payload.name || "New Key", key: maskedKey, fullKey, allowedModels: null, allowedConnections: null, createdAt: new Date("2026-04-05T20:00:00.000Z").toISOString(), }); await fulfillJson(route, { key: fullKey, id }); return; } await fulfillJson(route, { error: "Method not allowed" }, 405); }); await gotoDashboardRoute(page, "/dashboard/api-manager", { timeoutMs: NAVIGATION_TIMEOUT_MS, }); await waitForPageToSettle(page); await waitForNextDevCompileToFinish(page); // Create a key first const createFirstKeyButton = page.getByRole("button", { name: /create (your )?first key/i, }); await expect(createFirstKeyButton).toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS }); await createFirstKeyButton.click(); const createDialog = page.getByRole("dialog", { name: /create api key/i }); await expect(createDialog).toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS }); await createDialog.locator("input").first().fill("Original Name"); await createDialog.getByRole("button", { name: /create api key/i }).click({ force: true }); // Dismiss the created dialog const createdDialog = page.getByRole("dialog", { name: /api key created/i }); await createdDialog.getByRole("button", { name: /done/i }).click(); await waitForPageToSettle(page); await waitForNextDevCompileToFinish(page); await expect(page.getByText("Original Name")).toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS, }); // Open the permissions modal const keyRow = page .locator("div") .filter({ has: page.getByText("Original Name", { exact: true }) }) .first(); await keyRow.locator('button[title="Edit permissions"]').click({ force: true }); const permissionsDialog = page.getByRole("dialog", { name: /permissions: original name/i, }); await expect(permissionsDialog).toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS }); // --- Try to save with an invalid name (empty) --- const nameInput = permissionsDialog.locator("input").first(); await nameInput.clear(); await nameInput.fill(" "); const saveButton = permissionsDialog.getByRole("button", { name: /save permissions/i }); await saveButton.click({ force: true }); // Inline error should appear inside the permissions modal const inlineError = permissionsDialog.locator('[id$="-error"]'); await expect(inlineError).toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS }); // No page-level error banner should appear behind the modal const pageErrorBanner = page.locator('.flex.flex-col.gap-8 > [class~="bg-red-500/10"]'); await expect(pageErrorBanner).not.toBeVisible(); // Typing a valid name should clear the error await nameInput.fill("Renamed Key"); await expect(inlineError).not.toBeVisible(); // Save should succeed now await saveButton.click({ force: true }); await expect.poll(() => state.keys[0]?.name).toBe("Renamed Key"); await expect(permissionsDialog).not.toBeVisible({ timeout: UI_STABILITY_TIMEOUT_MS }); await expect(page.getByText("Renamed Key")).toBeVisible(); }); });