diff --git a/packages/app/e2e/helpers/settings.ts b/packages/app/e2e/helpers/settings.ts index e7648e0ea..2f91b5aab 100644 --- a/packages/app/e2e/helpers/settings.ts +++ b/packages/app/e2e/helpers/settings.ts @@ -1,8 +1,18 @@ import { expect, type Page } from "@playwright/test"; -import { TEST_HOST_LABEL } from "./daemon-registry"; +import { buildCreateAgentPreferences, buildSeededHost, TEST_HOST_LABEL } from "./daemon-registry"; import { escapeRegex } from "./regex"; import { getServerId } from "./server-id"; +const DISABLE_DEFAULT_SEED_ONCE_KEY = "@paseo:e2e-disable-default-seed-once"; +const SEED_NONCE_KEY = "@paseo:e2e-seed-nonce"; +const REGISTRY_KEY = "@paseo:daemon-registry"; + +interface SavedSettingsHostInput { + serverId: string; + label: string; + endpoint: string; +} + const SECTION_LABELS = { general: "General", appearance: "Appearance", @@ -84,6 +94,60 @@ export async function openCompactSettings(page: Page): Promise { await expect(page.getByTestId("settings-sidebar")).toBeVisible(); } +export async function seedSavedSettingsHosts( + page: Page, + hosts: SavedSettingsHostInput[], +): Promise { + await page.goto("/"); + const nowIso = new Date().toISOString(); + const registry = hosts.map((host) => + buildSeededHost({ + serverId: host.serverId, + label: host.label, + endpoint: host.endpoint, + nowIso, + }), + ); + const firstHost = registry[0]; + if (!firstHost) { + throw new Error("Expected at least one settings host fixture."); + } + const preferences = buildCreateAgentPreferences(firstHost.serverId); + + await page.evaluate( + ({ keys, storedRegistry, storedPreferences }) => { + const nonce = localStorage.getItem(keys.seedNonce); + if (!nonce) { + throw new Error("Expected e2e seed nonce before overriding settings host registry."); + } + + localStorage.setItem(keys.registry, JSON.stringify(storedRegistry)); + localStorage.setItem("@paseo:create-agent-preferences", JSON.stringify(storedPreferences)); + localStorage.setItem(keys.disableDefaultSeedOnce, nonce); + }, + { + keys: { + disableDefaultSeedOnce: DISABLE_DEFAULT_SEED_ONCE_KEY, + registry: REGISTRY_KEY, + seedNonce: SEED_NONCE_KEY, + }, + storedRegistry: registry, + storedPreferences: preferences, + }, + ); +} + +export async function selectSettingsHost(page: Page, serverId: string): Promise { + await page.getByTestId("settings-host-picker").click(); + await page.getByTestId(`settings-host-picker-item-${serverId}`).click(); +} + +export async function expectSettingsHostPickerLabel(page: Page, label: string): Promise { + await expect( + page.getByTestId("settings-host-picker").getByText(label, { exact: true }), + ).toBeVisible(); +} + export async function expectCompactSettingsList(page: Page): Promise { await expect(page).toHaveURL(/\/settings$/); await expect(page.getByTestId("settings-sidebar")).toBeVisible(); diff --git a/packages/app/e2e/settings-navigation.spec.ts b/packages/app/e2e/settings-navigation.spec.ts index 7aaab7bae..e8b7ffac8 100644 --- a/packages/app/e2e/settings-navigation.spec.ts +++ b/packages/app/e2e/settings-navigation.spec.ts @@ -1,5 +1,6 @@ import { test, expect } from "./fixtures"; import { gotoAppShell, openSettings } from "./helpers/app"; +import { getE2EDaemonPort } from "./helpers/daemon-port"; import { openSettingsSection, expectSettingsHeader, @@ -26,7 +27,12 @@ import { expectAboutContent, expectGeneralContent, expectAppearanceContent, + seedSavedSettingsHosts, + selectSettingsHost, + expectSettingsHostPickerLabel, + openSettingsHostSection, } from "./helpers/settings"; +import { getServerId } from "./helpers/server-id"; test.describe("Settings sidebar navigation", () => { test("clicking a sidebar section updates the URL and renders the section", async ({ page }) => { @@ -157,4 +163,28 @@ test.describe("Settings — compact master-detail", () => { await expect(page).toHaveURL(/\/settings$/); await expectSettingsSidebarVisible(page); }); + + test("switching the host picker on the settings list scopes host rows without navigating", async ({ + page, + }) => { + const primaryServerId = getServerId(); + const secondaryServerId = "srv_e2e_settings_secondary"; + const secondaryHostLabel = "Stable horse"; + const endpoint = `127.0.0.1:${getE2EDaemonPort()}`; + + await seedSavedSettingsHosts(page, [ + { serverId: primaryServerId, label: "First horse", endpoint }, + { serverId: secondaryServerId, label: secondaryHostLabel, endpoint }, + ]); + await gotoAppShell(page); + await openCompactSettings(page); + + await selectSettingsHost(page, secondaryServerId); + + await expect(page).toHaveURL(/\/settings$/); + await expectSettingsSidebarVisible(page); + await expectSettingsHostPickerLabel(page, secondaryHostLabel); + + await openSettingsHostSection(page, secondaryServerId, "connections"); + }); }); diff --git a/packages/app/src/screens/settings-screen.tsx b/packages/app/src/screens/settings-screen.tsx index 82d82ae17..0dd19f9bf 100644 --- a/packages/app/src/screens/settings-screen.tsx +++ b/packages/app/src/screens/settings-screen.tsx @@ -1097,13 +1097,30 @@ export default function SettingsScreen({ view }: SettingsScreenProps) { const sortedHosts = useSortedHosts(hosts, localServerId); const hostServerIds = useMemo(() => hosts.map((host) => host.serverId), [hosts]); const anyOnlineServerId = useAnyOnlineHostServerId(hostServerIds); + const [selectedSettingsHostServerId, setSelectedSettingsHostServerId] = useState( + view.kind === "host" ? view.serverId : null, + ); + const knownSelectedSettingsHostServerId = useMemo(() => { + if (!selectedSettingsHostServerId) { + return null; + } + return hosts.some((host) => host.serverId === selectedSettingsHostServerId) + ? selectedSettingsHostServerId + : null; + }, [hosts, selectedSettingsHostServerId]); + + useEffect(() => { + if (view.kind === "host") { + setSelectedSettingsHostServerId(view.serverId); + } + }, [view]); // The host the four sections scope to: the host on the active view, otherwise - // the local daemon, otherwise the first available host. + // the picker choice, otherwise the local daemon, otherwise the first host. const activeHostServerId = useMemo(() => { if (view.kind === "host") return view.serverId; - return localServerId ?? sortedHosts[0]?.serverId ?? null; - }, [view, localServerId, sortedHosts]); + return knownSelectedSettingsHostServerId ?? localServerId ?? sortedHosts[0]?.serverId ?? null; + }, [view, knownSelectedSettingsHostServerId, localServerId, sortedHosts]); const handleSendBehaviorChange = useCallback( (behavior: SendBehavior) => { @@ -1205,10 +1222,15 @@ export default function SettingsScreen({ view }: SettingsScreenProps) { [isCompactLayout, router], ); - // Picker: swap the host but keep the section the user is already looking at. + // Picker: choose the host for host-section rows. If the user is already on a + // host detail route, keep that detail section and swap only the host segment. const handleSelectHost = useCallback( (serverId: string) => { - const section: HostSectionSlug = view.kind === "host" ? view.section : "connections"; + setSelectedSettingsHostServerId(serverId); + if (view.kind !== "host") { + return; + } + const section: HostSectionSlug = view.section; const target = buildSettingsHostSectionRoute(serverId, section); if (isCompactLayout) { router.push(target);