diff --git a/packages/app/e2e/host-removal.spec.ts b/packages/app/e2e/host-removal.spec.ts new file mode 100644 index 000000000..d5efcf8d1 --- /dev/null +++ b/packages/app/e2e/host-removal.spec.ts @@ -0,0 +1,79 @@ +import { test, expect } from './fixtures'; + +test('host removal removes the host from UI and persists after reload', async ({ page }) => { + const daemonPort = process.env.E2E_DAEMON_PORT; + if (!daemonPort) { + throw new Error('E2E_DAEMON_PORT is not set (expected from globalSetup).'); + } + + const extraPort = Number(daemonPort) + 1; + const extraEndpoint = `127.0.0.1:${extraPort}`; + const nowIso = new Date().toISOString(); + + const extraDaemon = { + id: 'e2e-extra-daemon', + label: 'extra', + endpoints: [extraEndpoint], + relay: null, + metadata: null, + createdAt: nowIso, + updatedAt: nowIso, + }; + + // Add a second host once (fixtures seed the primary host on every navigation). + await page.addInitScript( + ({ daemon }) => { + const seedOnceKey = '@paseo:e2e-extra-daemon-seeded-once'; + if (localStorage.getItem(seedOnceKey)) { + return; + } + localStorage.setItem(seedOnceKey, '1'); + + const raw = localStorage.getItem('@paseo:daemon-registry'); + let parsed: any[] = []; + if (raw) { + try { + parsed = JSON.parse(raw); + } catch { + parsed = []; + } + } + const next = Array.isArray(parsed) ? [...parsed, daemon] : [daemon]; + localStorage.setItem('@paseo:daemon-registry', JSON.stringify(next)); + }, + { daemon: extraDaemon } + ); + + await page.goto('/settings'); + + await expect(page.getByText('extra', { exact: true }).first()).toBeVisible(); + await expect(page.getByText(extraEndpoint, { exact: true }).first()).toBeVisible(); + + const removeButtons = page.getByText('Remove', { exact: true }); + await expect(removeButtons).toHaveCount(2); + + page.once('dialog', (dialog) => dialog.accept()); + await removeButtons.nth(1).click(); + + await expect(page.getByText(extraEndpoint, { exact: true })).toHaveCount(0); + await page.waitForFunction( + (daemonId) => { + const raw = localStorage.getItem('@paseo:daemon-registry'); + if (!raw) return false; + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) && !parsed.some((entry: any) => entry && entry.id === daemonId); + } catch { + return false; + } + }, + extraDaemon.id, + { timeout: 10000 } + ); + + // Prevent the fixture from overwriting storage on reload; verify persistence. + await page.evaluate(() => localStorage.setItem('@paseo:e2e-disable-default-seed-once', '1')); + await page.reload(); + await expect(page.getByText(extraEndpoint, { exact: true })).toHaveCount(0); +}); + diff --git a/packages/app/src/app/settings.tsx b/packages/app/src/app/settings.tsx index 029f2e565..413201655 100644 --- a/packages/app/src/app/settings.tsx +++ b/packages/app/src/app/settings.tsx @@ -545,6 +545,21 @@ export default function SettingsScreen() { const handleRemoveDaemon = useCallback( (profile: DaemonProfile) => { + if (Platform.OS === "web") { + const hasBrowserConfirm = + typeof globalThis !== "undefined" && + typeof (globalThis as any).confirm === "function"; + + const confirmed = hasBrowserConfirm ? (globalThis as any).confirm(`Remove ${profile.label}?`) : true; + if (!confirmed) return; + + void removeDaemon(profile.id).catch((error) => { + console.error("[Settings] Failed to remove daemon", error); + Alert.alert("Error", "Unable to remove host"); + }); + return; + } + Alert.alert( "Remove Host", `Remove ${profile.label}?`, diff --git a/packages/app/src/utils/tauri-window.ts b/packages/app/src/utils/tauri-window.ts index b8cd5084d..0257dddc2 100644 --- a/packages/app/src/utils/tauri-window.ts +++ b/packages/app/src/utils/tauri-window.ts @@ -19,9 +19,14 @@ async function getTauriWindow() { } try { - // @metro-ignore tells Metro to skip bundling this import - // It will be resolved at runtime in Tauri's WebView - const { getCurrentWindow } = await import(/* @metro-ignore */ "@tauri-apps/api/window"); + // Avoid emitting `import()` syntax into the Hermes bundle (it fails to parse), + // while still loading the Tauri module at runtime in the WebView. + const dynamicImport = new Function( + "moduleName", + "return import(moduleName)" + ) as (moduleName: string) => Promise; + + const { getCurrentWindow } = await dynamicImport("@tauri-apps/api/window"); tauriWindow = getCurrentWindow(); return tauriWindow; } catch {