diff --git a/packages/app/e2e/relay-fallback-connect.spec.ts b/packages/app/e2e/relay-fallback-connect.spec.ts index 7ccd504de..0ca38dc25 100644 --- a/packages/app/e2e/relay-fallback-connect.spec.ts +++ b/packages/app/e2e/relay-fallback-connect.spec.ts @@ -35,7 +35,8 @@ test('connects via relay when direct endpoints fail', async ({ page }) => { }, host); await page.reload(); - // Should eventually connect through the relay candidate URL. - await expect(page.getByText('Connected via relay', { exact: true }).first()).toBeVisible({ timeout: 20000 }); - await expect(page.getByText('Online', { exact: true }).first()).toBeVisible({ timeout: 20000 }); + // Should eventually connect through the relay connection. + const card = page.getByTestId(`daemon-card-${serverId}`); + await expect(card.getByText('Relay', { exact: true })).toBeVisible({ timeout: 20000 }); + await expect(card.getByText('Online', { exact: true })).toBeVisible({ timeout: 20000 }); }); diff --git a/packages/app/e2e/relay-stability.spec.ts b/packages/app/e2e/relay-stability.spec.ts new file mode 100644 index 000000000..79b029796 --- /dev/null +++ b/packages/app/e2e/relay-stability.spec.ts @@ -0,0 +1,57 @@ +import { test, expect } from './fixtures'; + +test('relay connection stays stable across multiple tabs', async ({ page }) => { + const relayPort = process.env.E2E_RELAY_PORT; + const serverId = process.env.E2E_SERVER_ID; + const daemonPublicKeyB64 = process.env.E2E_RELAY_DAEMON_PUBLIC_KEY; + if (!relayPort || !serverId || !daemonPublicKeyB64) { + throw new Error( + 'E2E_RELAY_PORT, E2E_SERVER_ID, or E2E_RELAY_DAEMON_PUBLIC_KEY is not set (expected from globalSetup).' + ); + } + + const nowIso = new Date().toISOString(); + const relayEndpoint = `127.0.0.1:${relayPort}`; + + const host = { + serverId, + label: 'relay-daemon', + connections: [ + { id: 'direct:127.0.0.1:9', type: 'direct', endpoint: '127.0.0.1:9' }, + { id: `relay:${relayEndpoint}`, type: 'relay', relayEndpoint, daemonPublicKeyB64 }, + ], + preferredConnectionId: 'direct:127.0.0.1:9', + createdAt: nowIso, + updatedAt: nowIso, + }; + + // Use relay by making the direct endpoint intentionally fail. + await page.goto('/settings'); + await page.evaluate((daemon) => { + const nonce = localStorage.getItem('@paseo:e2e-seed-nonce') ?? '1'; + localStorage.setItem('@paseo:e2e-disable-default-seed-once', nonce); + localStorage.setItem('@paseo:daemon-registry', JSON.stringify([daemon])); + localStorage.removeItem('@paseo:settings'); + }, host); + await page.reload(); + + const card = page.getByTestId(`daemon-card-${serverId}`); + await expect(card.getByText('Relay', { exact: true })).toBeVisible({ timeout: 20000 }); + await expect(card.getByText('Online', { exact: true })).toBeVisible({ timeout: 20000 }); + + // Open a second tab. It should be able to connect independently without forcing disconnect churn. + const page2 = await page.context().newPage(); + await page2.route(/:(6767)\b/, (route) => route.abort()); + await page2.routeWebSocket(/:(6767)\b/, async (ws) => { + await ws.close({ code: 1008, reason: 'Blocked connection to localhost:6767 during e2e.' }); + }); + await page2.goto('/settings'); + const card2 = page2.getByTestId(`daemon-card-${serverId}`); + await expect(card2.getByText('Relay', { exact: true })).toBeVisible({ timeout: 20000 }); + await expect(card2.getByText('Online', { exact: true })).toBeVisible({ timeout: 20000 }); + + // Stability window: keep both tabs open and ensure they remain online. + await page.waitForTimeout(30_000); + await expect(card.getByText('Online', { exact: true })).toBeVisible(); + await expect(card2.getByText('Online', { exact: true })).toBeVisible(); +});