From 4fbc902325167b6cf24c43141dc7c99f66ae77ed Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 30 Jul 2026 22:13:05 +0800 Subject: [PATCH] test: cover persistent browser navigation --- scripts/real-browser-e2e.mjs | 62 +++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/scripts/real-browser-e2e.mjs b/scripts/real-browser-e2e.mjs index 94ef861..8e51d80 100644 --- a/scripts/real-browser-e2e.mjs +++ b/scripts/real-browser-e2e.mjs @@ -19,6 +19,7 @@ const FORBIDDEN_PORT = 8788; const EXTERNAL_ORIGIN = parseExternalE2eOrigin(process.env.E2E_ORIGIN); const SCREENSHOT_DIR = process.env.E2E_SCREENSHOT_DIR?.trim(); const FLEET_FIXTURE = process.env.E2E_FLEET_FIXTURE !== '0'; +const requestPathnames = []; const CHROME_CANDIDATES = [ process.env.CHROME_BIN, @@ -77,11 +78,20 @@ const FLEET_FIXTURE_ITEMS = [ async function builtAssetHandler(request, response) { const pathname = new URL(request.url ?? '/', 'http://e2e.local').pathname; - if (pathname === '/favicon.ico' || pathname === '/api/v1/events') { + requestPathnames.push(pathname); + if (pathname === '/favicon.ico') { response.statusCode = 204; response.end(); return; } + if (pathname === '/api/v1/events') { + response.writeHead(200, { + 'content-type': 'text/event-stream', + 'cache-control': 'no-cache', + }); + response.flushHeaders(); + return; + } let body; if (pathname === '/api/v1/auth/status') body = { configured: false, protectionEnabled: false, authenticated: false }; @@ -884,7 +894,51 @@ try { ); await assertWidePageGutters(cdp, 'Nodes'); - await keyboardNavigate(cdp, '设置', '/settings/instances', '实例'); + await eventually( + cdp, + `document.querySelector('.connection-status [data-state="open"]') !== null`, + 'Event stream did not reach the open state', + ); + await evaluate(cdp, `window.__navigationLifetime = crypto.randomUUID()`); + const navigationLifetime = await evaluate(cdp, `window.__navigationLifetime`); + const requestCountsBeforeSettings = EXTERNAL_ORIGIN + ? undefined + : { + events: requestPathnames.filter((pathname) => pathname === '/api/v1/events').length, + fleet: requestPathnames.filter( + (pathname) => + pathname === '/api/v1/instances' || + /^\/api\/v1\/instances\/[^/]+\/resources$/u.test(pathname), + ).length, + }; + await keyboardNavigate(cdp, '设置', '/settings/system', '密码保护'); + assert.equal( + await evaluate(cdp, `window.__navigationLifetime`), + navigationLifetime, + 'Settings navigation reloaded the document', + ); + assert.equal( + await evaluate(cdp, `document.activeElement?.id`), + 'main-content', + 'Settings navigation must focus the main content', + ); + if (requestCountsBeforeSettings) { + await delay(100); + assert.equal( + requestPathnames.filter((pathname) => pathname === '/api/v1/events').length, + requestCountsBeforeSettings.events, + 'Settings navigation recreated the event subscription', + ); + assert.equal( + requestPathnames.filter( + (pathname) => + pathname === '/api/v1/instances' || + /^\/api\/v1\/instances\/[^/]+\/resources$/u.test(pathname), + ).length, + requestCountsBeforeSettings.fleet, + 'Settings navigation requested Fleet data', + ); + } await assertWidePageGutters(cdp, 'Settings'); await keyboardNavigate(cdp, '自动化', '/automation', '自动化'); await assertWidePageGutters(cdp, 'Automation'); @@ -980,7 +1034,7 @@ try { `document.querySelector('[role="tab"][aria-selected="true"]')?.textContent.trim() === '操作审计'`, '/audit alias did not select operation audit', ); - await keyboardNavigate(cdp, '设置', '/settings/instances', '实例'); + await keyboardNavigate(cdp, '设置', '/settings/system', '密码保护'); assert.deepEqual(failures, [], `Browser failures detected:\n${failures.join('\n')}`); console.log(`PASS real Chrome E2E (${chromeBinary})`); @@ -990,7 +1044,7 @@ try { : `PASS isolated built-asset server on ${origin} (legacy port 8788 untouched)`, ); console.log( - 'PASS three-item navigation, Automation drawer and aliases, batch selection, and 390/768/1024/1440/1920 responsive layouts', + 'PASS persistent navigation, live SSE status, Automation workflows, and 390/768/1024/1440/1920 responsive layouts', ); } catch (error) { console.error(error instanceof Error ? error.stack : error);