From 15b3deff9ff691c7850b521e0e7ae24d7494c14a Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 08:01:53 +0100 Subject: [PATCH] test(web): establish native tab lifecycle positive control Bare Xvfb window bounds requests never established native backgrounding. Use an owned second tab and verify background/foreground states before suspension so future convergence checks do not mistake protocol acknowledgement for visibility evidence. Preserve earlier adverse controls and separate this passing diagnostic from application and installed-release acceptance. Change-source: pulse-maintainer --- .../browser-tests/lifecycle-control.md | 27 +++++++++++++++ .../check-browser-single-session-control.mjs | 33 ++++++++++++++++--- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/frontend-modern/browser-tests/lifecycle-control.md b/frontend-modern/browser-tests/lifecycle-control.md index a7fc19009..1d897d449 100644 --- a/frontend-modern/browser-tests/lifecycle-control.md +++ b/frontend-modern/browser-tests/lifecycle-control.md @@ -137,3 +137,30 @@ not merely a larger timeout or successful CDP acknowledgements. No Pulse fixture installed convergence, destination receipt or release qualification was exercised. Complete output is retained in maintainer run `20260908T065015Z-web-product`, `headed-window-control.log`. Earlier failed controls remain relevant. + +## Native tab-switch control — 8 September 2026 + +Run the same owned Xvfb command with `--headed-tab` instead of +`--headed-window`. This does not request minimisation or require a window +manager: it creates a second owned blank tab and activates it. Before freezing, +it requires observed hidden → visible/focused → hidden states, each bounded to +ten seconds with 250ms samples. It then uses the existing 2100ms host-side frozen +wait, resumes while backgrounded, and activates the original tab. Ordered +freeze/resume with unchanged ticks, a subsequent visible event, final focus and +timer progress remain required. No synthetic visibility events or forced focus +are used; no Playwright page session is attached. + +One run exited 0 on the same Chrome revision, Playwright and Node versions above, +owned display :99. Preflight recorded hidden at tick 5, visible/focused at tick +11, then hidden at tick 12. Freeze and resume both recorded tick 12. +Post-resume remained hidden/unfocused at tick 12; tab activation produced a +visible event and visible/focused state at tick 18. Complete runtime identities, +intervention commands/responses and samples are retained in maintainer run +`20260908T070012Z-web-product/headed-tab-control.log`. + +This establishes the positive control for the **tab-switch** mechanism, not +window minimisation. Earlier adverse results and the default mode's unreliable +250ms hidden-timer sample remain unchanged. No Pulse fixture or installed-pair +acceptance ran. The next source-level convergence fixture must retain raw +single-session ownership, the genuine background preflight, and the suspension +probe rather than attach a Playwright page and assume equivalent behaviour. diff --git a/scripts/check-browser-single-session-control.mjs b/scripts/check-browser-single-session-control.mjs index 503a677d2..21f14bce1 100644 --- a/scripts/check-browser-single-session-control.mjs +++ b/scripts/check-browser-single-session-control.mjs @@ -6,7 +6,8 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createRequire } from 'node:module'; import { chromium } from '@playwright/test'; -const headedWindow = process.argv.includes('--headed-window'); +const headedTab = process.argv.includes('--headed-tab'); +const headedWindow = headedTab || process.argv.includes('--headed-window'); const checkForeground = headedWindow || process.argv.includes('--foreground'); if (headedWindow && !process.env.DISPLAY) throw new Error('--headed-window requires an owned X display'); const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); @@ -50,7 +51,7 @@ try { console.log(JSON.stringify({ browser: await send('Browser.getVersion'), playwright: require('@playwright/test/package.json').version, node: process.version, platform: process.platform, arch: process.arch, executable: chromium.executablePath(), - headedWindow, display: process.env.DISPLAY, observationBoundMs: headedWindow ? 10000 : 250 })); + headedWindow, headedTab, display: process.env.DISPLAY, observationBoundMs: headedWindow ? 10000 : 250 })); const results = []; // Preselected negative (focus forced) and positive (no forced focus), fresh targets. for (const focusEnabled of (headedWindow ? [false] : [true, false])) { @@ -85,7 +86,28 @@ try { const snapshot = `({...window.probe, visibility: document.visibilityState, focus: document.hasFocus()})`; await wait(250); const before = await evaluate(snapshot); - if (headedWindow) { + let otherTarget; + const observe = async (stage, visibility) => { + const deadline = Date.now() + 10000; + let sample; + do { + await wait(250); + sample = await evaluate(snapshot); + console.log(JSON.stringify({ stage, snapshot: sample })); + if (sample.visibility === visibility && (visibility !== 'visible' || sample.focus)) return sample; + } while (Date.now() < deadline); + throw new Error(stage + ': native visibility transition not observed'); + }; + if (headedTab) { + assert.ok(before.visibility === 'visible' && before.focus, 'Tab baseline must be active'); + otherTarget = (await windowCommand('Target.createTarget', { url: 'about:blank' })).targetId; + await windowCommand('Target.activateTarget', { targetId: otherTarget }); + await observe('tab-background-preflight', 'hidden'); + await windowCommand('Target.activateTarget', { targetId }); + await observe('tab-foreground-preflight', 'visible'); + await windowCommand('Target.activateTarget', { targetId: otherTarget }); + await observe('tab-background-before-freeze', 'hidden'); + } else if (headedWindow) { assert.ok(before.visibility === 'visible' && before.focus, 'Headed baseline must be visible and focused'); await windowCommand('Browser.setWindowBounds', { windowId: windowInfo.windowId, bounds: { windowState: 'minimized' } }); await wait(250); @@ -103,7 +125,9 @@ try { // Resume is not foreground activation. Observe the two transitions separately. let foreground; if (checkForeground) { - if (headedWindow) { + if (headedTab) { + await windowCommand('Target.activateTarget', { targetId }); + } else if (headedWindow) { await windowCommand('Browser.setWindowBounds', { windowId: windowInfo.windowId, bounds: { windowState: 'normal' } }); } await command('Page.bringToFront', {}); @@ -117,6 +141,7 @@ try { } const result = { focusEnabled, before, after, foreground, commands, suspended }; results.push(result); console.log(JSON.stringify(result)); + if (otherTarget) await send('Target.closeTarget', { targetId: otherTarget }); } finally { await send('Target.closeTarget', { targetId }); } } if (!headedWindow) assert.ok(!results[0].suspended && results[0].after.ticks - results[0].before.ticks >= 20,