Merge main; keep inline panels polling while their host reports them hidden

Main now has #1282, which pauses periodic layer polling in a hidden page
with its own catch-up and listener cleanup, so this PR drops its duplicate
lifecycle.js change and test in favor of main's.

Review fixes:
- data.js passes panelAwareVisibility(document) (new in app/embed.js) as
  the lifecycle's visibility target, so an inline panel whose host reports
  it hidden keeps polling. The HUD's 250 ms readout follows the same
  document.hidden && !isEmbeddedInline() rule as tools.js.
- Tests: embed.test.mjs (layers poll in an inline panel reported hidden,
  pause without the flag) and hudPageVisibility.test.mjs (HUD readout).
  Both use mock timers and clean up in finally.
- changelog.d/1344-inline-panel-visibility.md and a note in
  docs/CURRENT-STATE.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
nu-statsandClaude Opus 5.5 committed 2026-10-24 19:47:18 -04:00
commit 1161899aea
83 files changed
+5697 -378

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
// The HUD's 250 ms telemetry readout skips ticks in a hidden page, except in
// an inline panel, whose host may report it hidden while it is on screen.
//
// hud.js imports `mgrs`, which Node's ESM loader cannot read named exports
// from, so a module hook swaps it for a stub before the import (the same
// setup as hudAltitudeDatum.test.mjs).
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { registerHooks } from 'node:module';
const MGRS_STUB_URL = 'gev-test-stub:mgrs';
registerHooks({
resolve(specifier, context, next) {
if (specifier === 'mgrs') return { url: MGRS_STUB_URL, shortCircuit: true };
return next(specifier, context);
},
load(url, context, next) {
if (url === MGRS_STUB_URL) {
return {
format: 'module',
shortCircuit: true,
source: 'export function forward() { return "10SEG55776339"; }\nexport default { forward };\n',
};
}
return next(url, context);
},
});
const { IntelHUD } = await import('./hud.js');
function installHiddenPage() {
const elements = new Map(
['hud-alt', 'hud-summary', 'hud-mgrs', 'hud-latlon', 'hud-bottom-line', 'hud-gsd', 'hud-coll', 'hud-ona', 'hud-mode']
.map((id) => [id, { textContent: '' }]),
);
const previousDocument = globalThis.document;
globalThis.document = {
hidden: true,
visibilityState: 'hidden',
getElementById: (id) => elements.get(id) ?? null,
querySelectorAll: () => [],
addEventListener() {},
removeEventListener() {},
};
const viewer = {
camera: {
pitch: -Math.PI / 2,
positionCartographic: { latitude: 0.7, longitude: -1.2, height: 500 },
computeViewRectangle: () => undefined,
moveEnd: { addEventListener() {}, removeEventListener() {} },
},
};
return {
alt: () => elements.get('hud-alt').textContent,
viewer,
restore() {
if (previousDocument === undefined) delete globalThis.document;
else globalThis.document = previousDocument;
},
};
}
test('the HUD readout pauses in a hidden page but keeps updating in an inline panel', (t) => {
t.mock.timers.enable({ apis: ['setInterval'] });
const page = installHiddenPage();
let hud;
try {
hud = new IntelHUD(page.viewer);
hud._visible = true;
t.mock.timers.tick(250);
assert.equal(page.alt(), '', 'a hidden page skips the telemetry tick');
globalThis.GEV_EMBED_INLINE = true;
t.mock.timers.tick(250);
assert.match(
page.alt(),
/^ALT: 500m/,
'an inline panel reported hidden keeps its readout updating',
);
} finally {
delete globalThis.GEV_EMBED_INLINE;
hud?.destroy();
page.restore();
}
});