test(app): cover provider removal with e2e

Move provider removal coverage out of mocked component tests and into the real Settings flow.
This commit is contained in:
Mohamed Boudra
2026-07-16 15:10:53 +02:00
parent 8b8f35fc37
commit 1d20ba5250
2 changed files with 132 additions and 101 deletions
+96
View File
@@ -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<void>;
close(): Promise<void>;
patchDaemonConfig(config: { removeProviders?: string[] }): Promise<unknown>;
getProvidersSnapshot(): Promise<{
entries: Array<{ provider: string; source?: "builtin" | "custom" }>;
}>;
}
async function removeCustomProvider(client: ProviderRemovalDaemonClient): Promise<void> {
await client.patchDaemonConfig({ removeProviders: [CUSTOM_PROVIDER.id] });
}
async function expectProviderSource(
client: ProviderRemovalDaemonClient,
source: "custom" | undefined,
): Promise<void> {
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<Dialog> {
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<ProviderRemovalDaemonClient>({
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);
}
});
});
@@ -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<HTMLElement>('[data-testid="provider-actions-gemini"]');
const remove = row.querySelector<HTMLElement>('[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();
});
});