From 73fd4da42f9f1db55395cd78e2d30b5266025c4e Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 07:41:45 +0100 Subject: [PATCH] test(web): retain failed foreground activation control A successful lifecycle resume leaves the diagnostic target hidden. Add an opt-in tab activation observation with explicit visibility and focus assertions so it cannot be mistaken for foreground convergence. Preserve both failed runs and the short timer-sampling limitation; no application or release qualification is claimed. Change-source: pulse-maintainer --- .../browser-tests/lifecycle-control.md | 37 +++++++++++++++++++ .../check-browser-single-session-control.mjs | 16 +++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/frontend-modern/browser-tests/lifecycle-control.md b/frontend-modern/browser-tests/lifecycle-control.md index b234bdf04..65fb8432b 100644 --- a/frontend-modern/browser-tests/lifecycle-control.md +++ b/frontend-modern/browser-tests/lifecycle-control.md @@ -60,3 +60,40 @@ a Playwright page attachment may reintroduce the confounder. Before testing foreground convergence, separately observe return to visible. Installed acceptance still requires the authorised synthetic installation and exact byte identities listed in `alert-recovery-acceptance.md`; neither is supplied by this control. + +## Foreground activation control — 8 September 2026 + +`pulse-heavy-run -- node scripts/check-browser-single-session-control.mjs --foreground` +adds `Page.bringToFront` after the separate post-resume snapshot, then takes a +foreground snapshot after 250ms. Default invocation retains the original +freeze/resume-only behaviour. The optional mode requires visible **and** focused +state plus timer progress; it does not force focus emulation back on to obtain a +passing result. No Playwright page is attached. + +Fresh [CDP documentation](https://chromedevtools.github.io/devtools-protocol/tot/Page/#method-bringToFront) +describes tab activation separately from lifecycle state. The command's success +is not evidence of visibility restoration. On the same Chrome revision and +runtime listed above, both the initial implementation and the final optional-mode +run exited 1: + +- Forced-focus negative: ticks 5 → 52 → 57; visible/focused throughout, no events. +- Unforced positive: freeze and resume at tick 5, hidden/unfocused after resume; + after activation, **hidden/focused**, without a visible visibilitychange event. +- The first run stayed at tick 5 through the foreground sample; the optional-mode + run reached tick 6 only at that final sample. Both failed the earlier + post-resume timer-progress assertion before reaching the foreground assertion. + +The 250ms post-resume sample is therefore not a dependable timer-progress bound +for this hidden target. Do not interpret that failed assertion as missing resume: +ordered freeze/resume events were recorded. Independently, visibility restoration +failed in both observed foreground samples. Neither threshold was relaxed and +neither failure is cleared by the earlier successful freeze-only control. + +No Pulse fixture was exercised: the prerequisite foreground control is still +unestablished. Before applying this arrangement to application convergence, +choose and verify an actual visibility transition (potentially a headed browser +with owned window activation), retaining the suspension probe and all failed +observations. Repeating this activation unchanged, synthetically dispatching a +visibility event, or enabling forced focus would not resolve the evidence gap. +This diagnostic is not a release gate and proves nothing about installed +incident state or destination receipt. diff --git a/scripts/check-browser-single-session-control.mjs b/scripts/check-browser-single-session-control.mjs index e05385903..ce2f5b824 100644 --- a/scripts/check-browser-single-session-control.mjs +++ b/scripts/check-browser-single-session-control.mjs @@ -6,6 +6,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createRequire } from 'node:module'; import { chromium } from '@playwright/test'; +const checkForeground = process.argv.includes('--foreground'); const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); const profile = await mkdtemp(join(tmpdir(), 'pulse-lifecycle-')); const child = spawn(chromium.executablePath(), ['--headless', '--no-sandbox', @@ -83,13 +84,26 @@ try { const resume = after.events.find(e => e.type === 'resume'); const suspended = Boolean(freeze && resume && freeze.ticks === resume.ticks && after.events.indexOf(freeze) < after.events.indexOf(resume) && after.ticks > resume.ticks); - const result = { focusEnabled, before, after, commands, suspended }; + // Resume is not foreground activation. Observe the two transitions separately. + let foreground; + if (checkForeground) { + await command('Page.bringToFront', {}); + await wait(250); + foreground = await evaluate(snapshot); + } + const result = { focusEnabled, before, after, foreground, commands, suspended }; results.push(result); console.log(JSON.stringify(result)); } finally { await send('Target.closeTarget', { targetId }); } } assert.ok(!results[0].suspended && results[0].after.ticks - results[0].before.ticks >= 20, 'Negative control must continue ticking'); assert.ok(results[1].suspended, 'Positive control must suspend and resume timers'); + if (checkForeground) { + assert.ok(results[1].foreground.visibility === 'visible' && results[1].foreground.focus, + 'Positive control must return to visible and focused after tab activation'); + assert.ok(results[1].foreground.ticks > results[1].after.ticks, + 'Foreground timer must continue advancing'); + } } finally { socket?.close(); const exited = new Promise(resolve => child.once('exit', resolve));