From 1d20ba525094bad5293d2f36f3ace6798f7bda5e Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 9 Jul 2026 10:32:02 +0200 Subject: [PATCH] test(app): cover provider removal with e2e Move provider removal coverage out of mocked component tests and into the real Settings flow. --- packages/app/e2e/provider-removal.spec.ts | 96 ++++++++++++ .../settings/providers-section.test.tsx | 137 +++++------------- 2 files changed, 132 insertions(+), 101 deletions(-) create mode 100644 packages/app/e2e/provider-removal.spec.ts diff --git a/packages/app/e2e/provider-removal.spec.ts b/packages/app/e2e/provider-removal.spec.ts new file mode 100644 index 0000000000..dcddc3ad2c --- /dev/null +++ b/packages/app/e2e/provider-removal.spec.ts @@ -0,0 +1,96 @@ +import type { Dialog } from "@playwright/test"; +import { expect, test, type Page } from "./fixtures"; +import { gotoAppShell, openSettings } from "./helpers/app"; +import { connectDaemonClient } from "./helpers/daemon-client-loader"; +import { getServerId } from "./helpers/server-id"; +import { + expectProviderInstalledInSettings, + installAcpCatalogProvider, + openAddProviderArea, + openSettingsHost, + openSettingsHostSection, +} from "./helpers/settings"; + +const CUSTOM_PROVIDER = { + id: "junie", + name: "Junie", +} as const; + +interface ProviderRemovalDaemonClient { + connect(): Promise; + close(): Promise; + patchDaemonConfig(config: { removeProviders?: string[] }): Promise; + getProvidersSnapshot(): Promise<{ + entries: Array<{ provider: string; source?: "builtin" | "custom" }>; + }>; +} + +async function removeCustomProvider(client: ProviderRemovalDaemonClient): Promise { + await client.patchDaemonConfig({ removeProviders: [CUSTOM_PROVIDER.id] }); +} + +async function expectProviderSource( + client: ProviderRemovalDaemonClient, + source: "custom" | undefined, +): Promise { + await expect + .poll(async () => { + const snapshot = await client.getProvidersSnapshot(); + return snapshot.entries.find((entry) => entry.provider === CUSTOM_PROVIDER.id)?.source; + }) + .toBe(source); +} + +async function clickRemoveProviderAndAcceptWarning(page: Page): Promise { + let warning: Dialog | undefined; + page.once("dialog", (dialog) => { + warning = dialog; + expect(dialog.message()).toContain(`Remove ${CUSTOM_PROVIDER.name}?`); + expect(dialog.message()).toContain("This deletes the provider entry from config.json."); + void dialog.accept(); + }); + await page.getByTestId(`provider-remove-${CUSTOM_PROVIDER.id}`).click(); + if (!warning) { + throw new Error("Expected a provider removal confirmation dialog, but none was shown."); + } + return warning; +} + +test.describe("provider removal", () => { + test("removes a custom provider from Settings", async ({ page }) => { + test.setTimeout(120_000); + const client = await connectDaemonClient({ + clientIdPrefix: "provider-removal-e2e", + }); + + try { + await removeCustomProvider(client); + + await gotoAppShell(page); + await openSettings(page); + await openSettingsHost(page, getServerId()); + await openSettingsHostSection(page, getServerId(), "providers"); + + await expect(page.getByTestId("provider-actions-claude")).toHaveCount(0); + await openAddProviderArea(page); + await installAcpCatalogProvider(page, CUSTOM_PROVIDER.name); + await expectProviderInstalledInSettings(page, CUSTOM_PROVIDER.name); + await expectProviderSource(client, "custom"); + + await page.getByTestId(`provider-actions-${CUSTOM_PROVIDER.id}`).click(); + await expect(page.getByTestId(`provider-remove-${CUSTOM_PROVIDER.id}`)).toBeVisible(); + await clickRemoveProviderAndAcceptWarning(page); + + await expect( + page.getByRole("button", { + name: `${CUSTOM_PROVIDER.name} provider details`, + exact: true, + }), + ).toHaveCount(0); + await expectProviderSource(client, undefined); + } finally { + await removeCustomProvider(client).catch(() => undefined); + await client.close().catch(() => undefined); + } + }); +}); diff --git a/packages/app/src/screens/settings/providers-section.test.tsx b/packages/app/src/screens/settings/providers-section.test.tsx index 2e680408c0..6af35b675e 100644 --- a/packages/app/src/screens/settings/providers-section.test.tsx +++ b/packages/app/src/screens/settings/providers-section.test.tsx @@ -7,51 +7,41 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { ProviderSnapshotEntry } from "@getpaseo/protocol/agent-types"; import type { MutableDaemonConfig } from "@getpaseo/protocol/messages"; -const { - theme, - snapshotState, - configState, - hostFeatureState, - patchConfigMock, - openProviderSettingsMock, - confirmDialogMock, -} = vi.hoisted(() => ({ - theme: { - spacing: { 1: 4, "1.5": 6, 2: 8, 3: 12, 4: 16, 6: 24 }, - iconSize: { sm: 14, md: 20 }, - fontSize: { xs: 11, sm: 13, base: 15 }, - fontWeight: { normal: "400" }, - borderRadius: { lg: 8 }, - opacity: { 50: 0.5 }, - colors: { - surface1: "#111", - surface2: "#222", - surface3: "#333", - foreground: "#fff", - foregroundMuted: "#aaa", - border: "#555", - accent: "#0a84ff", - statusSuccess: "#00ff00", - statusWarning: "#ff9500", - statusDanger: "#ff0000", - palette: { red: { 300: "#ff6b6b" }, white: "#fff" }, +const { theme, snapshotState, configState, patchConfigMock, openProviderSettingsMock } = vi.hoisted( + () => ({ + theme: { + spacing: { 1: 4, "1.5": 6, 2: 8, 3: 12, 4: 16, 6: 24 }, + iconSize: { sm: 14, md: 20 }, + fontSize: { xs: 11, sm: 13, base: 15 }, + fontWeight: { normal: "400" }, + borderRadius: { lg: 8 }, + opacity: { 50: 0.5 }, + colors: { + surface1: "#111", + surface2: "#222", + surface3: "#333", + foreground: "#fff", + foregroundMuted: "#aaa", + border: "#555", + accent: "#0a84ff", + statusSuccess: "#00ff00", + statusWarning: "#ff9500", + statusDanger: "#ff0000", + palette: { red: { 300: "#ff6b6b" }, white: "#fff" }, + }, }, - }, - snapshotState: { - entries: undefined as ProviderSnapshotEntry[] | undefined, - isLoading: false, - isRefreshing: false, - }, - configState: { - config: null as MutableDaemonConfig | null, - }, - hostFeatureState: { - providerRemoval: false, - }, - patchConfigMock: vi.fn(async () => undefined), - openProviderSettingsMock: vi.fn(), - confirmDialogMock: vi.fn(async () => true), -})); + snapshotState: { + entries: undefined as ProviderSnapshotEntry[] | undefined, + isLoading: false, + isRefreshing: false, + }, + configState: { + config: null as MutableDaemonConfig | null, + }, + patchConfigMock: vi.fn(async () => undefined), + openProviderSettingsMock: vi.fn(), + }), +); vi.mock("react-native", () => ({ View: ({ children, testID }: { children?: React.ReactNode; testID?: string }) => @@ -277,14 +267,11 @@ vi.mock("@/runtime/host-runtime", () => ({ })); vi.mock("@/runtime/host-features", () => ({ - useHostFeature: (serverId: string, feature: string) => - serverId === "server-1" && feature === "providerRemoval" - ? hostFeatureState.providerRemoval - : false, + useHostFeature: () => false, })); vi.mock("@/utils/confirm-dialog", () => ({ - confirmDialog: confirmDialogMock, + confirmDialog: vi.fn(async () => true), })); import { ProvidersSection } from "./providers-section"; @@ -314,18 +301,6 @@ const disabledCodexEntry: ProviderSnapshotEntry = { modes: [], }; -const customGeminiEntry: ProviderSnapshotEntry = { - provider: "gemini", - status: "ready", - enabled: true, - source: "custom", - label: "Gemini", - description: "Gemini CLI", - defaultModeId: null, - modes: [], - models: [{ provider: "gemini", id: "gemini-3-pro", label: "Gemini 3 Pro" }], -}; - function makeConfig(providers: MutableDaemonConfig["providers"] = {}): MutableDaemonConfig { return { mcp: { injectIntoAgents: false }, @@ -366,12 +341,9 @@ describe("ProvidersSection", () => { snapshotState.isLoading = false; snapshotState.isRefreshing = false; configState.config = null; - hostFeatureState.providerRemoval = false; patchConfigMock.mockReset(); patchConfigMock.mockResolvedValue(undefined); openProviderSettingsMock.mockReset(); - confirmDialogMock.mockReset(); - confirmDialogMock.mockResolvedValue(true); }); afterEach(() => { @@ -485,41 +457,4 @@ describe("ProvidersSection", () => { providers: { claude: { enabled: false } }, }); }); - - it("removes a custom provider through the actions menu after confirmation", async () => { - snapshotState.entries = [customGeminiEntry]; - configState.config = makeConfig({ gemini: {} }); - hostFeatureState.providerRemoval = true; - - render(); - - const row = findRow("Gemini provider details"); - const menu = row.querySelector('[data-testid="provider-actions-gemini"]'); - const remove = row.querySelector('[data-testid="provider-remove-gemini"]'); - expect(menu?.getAttribute("aria-label")).toBe("Gemini actions"); - expect(remove?.textContent).toBe("Remove provider"); - - await act(async () => { - remove?.dispatchEvent(new window.MouseEvent("click", { bubbles: true })); - }); - - expect(confirmDialogMock).toHaveBeenCalledWith({ - title: "Remove Gemini?", - message: "This deletes the provider entry from config.json. It cannot be undone.", - confirmLabel: "Remove", - destructive: true, - }); - expect(patchConfigMock).toHaveBeenCalledWith({ removeProviders: ["gemini"] }); - }); - - it("does not show provider removal for built-in providers", () => { - snapshotState.entries = [claudeEntry]; - configState.config = makeConfig(); - hostFeatureState.providerRemoval = true; - - render(); - - const row = findRow("Claude provider details"); - expect(row.querySelector('[data-testid="provider-actions-claude"]')).toBeNull(); - }); });