From 3848b625afad19b300933a3d4c4fb5df5a7301d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 10 May 2026 01:11:35 +0200 Subject: [PATCH] test(e2e): AI provider dropdown switches the rendered config form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Locks in the B2 contract: /settings/ai exposes the active-provider Pulldown, switching the dropdown swaps the form below it (Codex → OpenAI → Anthropic → Local), and the fallback chain card surfaces all configured rows with their stable data-chain-row markers. The spec mocks every backend endpoint the section reads so the run needs no real Codex / OpenAI keys in CI. data-testid markers on each config form keep the assertions independent of the locale. Co-Authored-By: Claude Opus 4.7 (1M context) --- e2e/ai-provider-dropdown.spec.ts | 116 +++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 e2e/ai-provider-dropdown.spec.ts diff --git a/e2e/ai-provider-dropdown.spec.ts b/e2e/ai-provider-dropdown.spec.ts new file mode 100644 index 000000000..d64a38122 --- /dev/null +++ b/e2e/ai-provider-dropdown.spec.ts @@ -0,0 +1,116 @@ +import { expect, test } from "@playwright/test"; + +import { STORAGE_STATE_PATH } from "./setup/global-setup"; + +/** + * v1.4.16 phase B2 — AI provider settings UX with single dropdown + * driving the form below. Three things to lock in here: + * + * 1. /settings/ai exposes the active-provider Pulldown + * (`data-testid="ai-active-provider-select"`). + * 2. Switching the dropdown changes the rendered config form below + * it: Codex form vs. OpenAI form vs. Anthropic form vs. Local + * form. Each form carries a stable `data-testid` so we don't + * lean on i18n labels. + * 3. The fallback chain card surfaces with stable + * `data-chain-row=""` markers per row. + */ +test.describe("Settings → AI provider dropdown UX (B2)", () => { + test.use({ storageState: STORAGE_STATE_PATH }); + + test("dropdown switch changes the rendered config form", async ({ page }) => { + // Stable mocks for every endpoint the section reads. The chain + // mirrors the default order (codex first) so the rows render + // deterministically and the dropdown defaults to codex. + await page.route("**/api/insights/settings", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + data: { + codexStatus: "disconnected", + codexConnectedAt: null, + hasAdminKey: false, + codexOauthConfigured: true, + privacyMode: "aggregated", + lastInsightAt: null, + }, + error: null, + }), + }), + ); + + await page.route("**/api/user/ai-provider", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + data: { + provider: null, + model: null, + baseUrl: null, + hasAnthropicKey: false, + anthropicKeyPreview: null, + hasLocalKey: false, + hasOpenaiKey: false, + openaiKeyPreview: null, + }, + error: null, + }), + }), + ); + + await page.route("**/api/insights/provider-chain", (route) => + route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify({ + data: { + activeProvider: "codex", + cachedActiveProvider: null, + configuredChain: [ + { providerType: "codex", available: true }, + { providerType: "openai", available: true }, + { providerType: "anthropic", available: true }, + { providerType: "local", available: true }, + ], + }, + error: null, + }), + }), + ); + + await page.goto("/settings/ai", { waitUntil: "domcontentloaded" }); + + // 1. Dropdown surfaces on the page. + const select = page.getByTestId("ai-active-provider-select"); + await expect(select).toBeVisible({ timeout: 10_000 }); + + // 2a. Default → Codex form rendered. + await expect(page.getByTestId("ai-provider-config-codex")).toBeVisible(); + await expect(page.getByTestId("ai-provider-config-openai")).toHaveCount(0); + + // 2b. Switch to OpenAI → OpenAI form rendered, Codex form gone. + await select.selectOption("openai"); + await expect(page.getByTestId("ai-provider-config-openai")).toBeVisible(); + await expect(page.getByTestId("ai-provider-config-codex")).toHaveCount(0); + await expect(page.getByTestId("ai-openai-api-key")).toBeVisible(); + await expect(page.getByTestId("ai-openai-model")).toBeVisible(); + + // 2c. Switch to Anthropic → Anthropic form rendered. + await select.selectOption("anthropic"); + await expect(page.getByTestId("ai-provider-config-anthropic")).toBeVisible(); + + // 2d. Switch to Local → Local form rendered. + await select.selectOption("local"); + await expect(page.getByTestId("ai-provider-config-local")).toBeVisible(); + + // 3. Fallback chain card surfaces with the four chain rows. + await expect(page.getByTestId("ai-fallback-chain")).toBeVisible(); + for (const slug of ["codex", "openai", "anthropic", "local"]) { + await expect( + page.locator(`[data-chain-row="${slug}"]`), + ).toBeVisible(); + } + }); +});