mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
Snow is no longer offered anywhere a style can be chosen: the Visual Presets tray, keyboard shortcut 7, the Cockpit vision carousel, and the set_visual_style voice action. The shader and its labels go with it; weather snowfall and the SNOW weather condition are unaffected. Share links keep working. Snow's enum code (6) stays reserved, so a compact or long-form link that names it opens on Normal and drops the Snow shader parameters without reporting them as unknown. A Scene Director shot saved with Snow plays on Normal too. Snow's frozen share defaults matched Normal's apart from those parameters, so no other setting in an old link changes meaning. The legacy voice-schema pins restore Snow before hashing, so the unchanged pins still prove nothing else moved; the complete Realtime payload digest is re-derived.
2410 lines
110 KiB
JavaScript
2410 lines
110 KiB
JavaScript
#!/usr/bin/env node
|
|
/** Focused rendered proof for adaptive Cockpit Display/Radio layout. */
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import puppeteer from 'puppeteer';
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
const shotsDir = path.join(repoRoot, 'qa-shots', 'cockpit-utility');
|
|
const appUrl = process.env.QA_BASE_URL || 'http://localhost:4173';
|
|
const headful = process.argv.includes('--headful');
|
|
fs.mkdirSync(shotsDir, { recursive: true });
|
|
|
|
const chromeCandidates = [
|
|
process.env.PUPPETEER_EXECUTABLE_PATH,
|
|
await puppeteer.executablePath().catch(() => null),
|
|
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
].filter(Boolean);
|
|
const executablePath = chromeCandidates.find((candidate) => fs.existsSync(candidate));
|
|
const browser = await puppeteer.launch({
|
|
headless: headful ? false : 'new',
|
|
...(executablePath ? { executablePath } : {}),
|
|
// Metal is a macOS-only ANGLE backend; anywhere else it fails WebGL init.
|
|
args: [
|
|
...(process.platform === 'darwin'
|
|
? ['--use-angle=metal', '--enable-gpu']
|
|
: ['--use-gl=angle', '--use-angle=swiftshader']),
|
|
'--no-sandbox',
|
|
],
|
|
});
|
|
const page = await browser.newPage();
|
|
const failures = [];
|
|
const consoleErrors = [];
|
|
const localHttpErrors = [];
|
|
page.on('console', (message) => {
|
|
if (message.type() === 'error' && !/Failed to load resource.*404/i.test(message.text())) {
|
|
const source = message.location()?.url;
|
|
consoleErrors.push(source ? `${message.text()} [${source}]` : message.text());
|
|
}
|
|
});
|
|
page.on('pageerror', (error) => consoleErrors.push(error.message));
|
|
page.on('response', (response) => {
|
|
const url = new URL(response.url());
|
|
const expectedOptionalTrackMiss = response.status() === 404
|
|
&& url.pathname === '/api/flights/track';
|
|
if (
|
|
url.origin === new URL(appUrl).origin
|
|
&& response.status() >= 400
|
|
&& !expectedOptionalTrackMiss
|
|
) {
|
|
localHttpErrors.push(`${response.status()} ${url.pathname}`);
|
|
}
|
|
});
|
|
|
|
const check = (name, passed, detail) => {
|
|
console.log(` [${passed ? 'PASS' : 'FAIL'}] ${name}${detail ? ` — ${detail}` : ''}`);
|
|
if (!passed) failures.push(name);
|
|
};
|
|
|
|
try {
|
|
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
|
|
await page.setRequestInterception(true);
|
|
const routeQaRequest = (request) => {
|
|
const url = new URL(request.url());
|
|
// These scenarios exercise Context lifecycle and keyboard ownership, not
|
|
// live orbit accuracy. Reuse the tracking suite's fixed element sets so
|
|
// CelesTrak outages cannot invalidate an otherwise clean UI run.
|
|
if (url.origin === new URL(appUrl).origin
|
|
&& ['/api/celestrak/active', '/api/celestrak/starlink'].includes(url.pathname)) {
|
|
const dense = url.pathname.endsWith('/starlink');
|
|
request.respond({
|
|
status: 200,
|
|
contentType: 'text/plain',
|
|
body: (dense ? [
|
|
'STARLINK-1007',
|
|
'1 44713U 19074A 24001.50000000 .00016717 00000-0 10270-3 0 9004',
|
|
'2 44713 53.0000 247.4627 0006703 130.5360 325.0288 15.06000000 12345',
|
|
] : [
|
|
'ISS (ZARYA)',
|
|
'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9004',
|
|
'2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.49814310 12345',
|
|
]).join('\n') + '\n',
|
|
});
|
|
return;
|
|
}
|
|
// Space Missions is toggled by the Context cancellation checks below.
|
|
// Its catalog is unrelated to aircraft/Cockpit behavior; keep the network
|
|
// error gate meaningful without depending on Launch Library availability.
|
|
if (url.origin === new URL(appUrl).origin && url.pathname === '/api/launches') {
|
|
request.respond({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ results: [] }),
|
|
});
|
|
return;
|
|
}
|
|
// This harness verifies UI/lifecycle behavior, not DEM accuracy. Keep an
|
|
// unrelated upstream terrain outage out of the rendered interaction gate.
|
|
if (url.origin === new URL(appUrl).origin && url.pathname === '/api/terrain/heights') {
|
|
const points = (url.searchParams.get('points') || '').split(';').filter(Boolean);
|
|
request.respond({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ results: points.map((point) => {
|
|
const [lon, lat] = point.split(',').map(Number);
|
|
return { lon, lat, elevation: 0, geoid: 0, ellipsoid: 0 };
|
|
}) }),
|
|
});
|
|
return;
|
|
}
|
|
if (url.origin === new URL(appUrl).origin && url.pathname === '/api/vessels') {
|
|
request.respond({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
rows: [],
|
|
source: 'AISStream QA',
|
|
status: 'open',
|
|
error: null,
|
|
refreshing: false,
|
|
newestPositionAt: null,
|
|
lastMessageAt: null,
|
|
}),
|
|
});
|
|
return;
|
|
}
|
|
if (url.origin === new URL(appUrl).origin && url.pathname === '/api/military') {
|
|
request.respond({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ msg: 'No error', now: Date.now(), ac: [] }),
|
|
});
|
|
return;
|
|
}
|
|
if (url.origin === new URL(appUrl).origin && url.pathname === '/api/flights/track') {
|
|
request.respond({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ path: [] }),
|
|
});
|
|
return;
|
|
}
|
|
if (url.origin === new URL(appUrl).origin && url.pathname === '/api/military-installations') {
|
|
request.respond({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
elements: [],
|
|
saturated: false,
|
|
elementCap: 1_500,
|
|
retrievedAt: new Date().toISOString(),
|
|
status: 'ready',
|
|
}),
|
|
});
|
|
return;
|
|
}
|
|
if (url.origin === new URL(appUrl).origin && url.pathname === '/api/openai/hud-summary') {
|
|
request.respond({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({ summary: 'Cockpit utility QA' }),
|
|
});
|
|
return;
|
|
}
|
|
request.continue();
|
|
};
|
|
page.on('request', routeQaRequest);
|
|
await page.goto(appUrl, { waitUntil: 'domcontentloaded', timeout: 60_000 });
|
|
await page.waitForFunction(() => window.__godsEyeView?.styleManager, { timeout: 60_000 });
|
|
await page.waitForFunction(
|
|
() => document.getElementById('loading-screen')?.classList.contains('hidden'),
|
|
{ timeout: 60_000 },
|
|
);
|
|
await page.waitForFunction(() => (
|
|
typeof window.__gevQaRegisterLayer === 'function'
|
|
&& typeof window.__gevQaUnregisterLayer === 'function'
|
|
));
|
|
await page.evaluate(() => {
|
|
const manager = window.__godsEyeView.styleManager;
|
|
const hud = document.getElementById('cockpit-hud');
|
|
const signal = document.getElementById('cockpit-signal-stream');
|
|
window.__qaCockpitUtilityPrior = {
|
|
cockpit: document.body.classList.contains('cockpit-mode'),
|
|
active: manager.cockpitView.active,
|
|
hudHidden: hud.hidden,
|
|
signalHidden: signal.hidden,
|
|
hudVisible: manager.hud.visible,
|
|
hudVariant: manager.hud.getVariant(),
|
|
utilityTop: hud.style.getPropertyValue('--cockpit-utility-top'),
|
|
};
|
|
});
|
|
const contactMissionHandoff = await page.evaluate(async () => {
|
|
const { styleManager, dataManager } = window.__godsEyeView;
|
|
const originalShowToast = styleManager._showToast;
|
|
const toasts = [];
|
|
styleManager._showToast = (message) => { toasts.push(String(message)); };
|
|
try {
|
|
if (styleManager._contextControls._contextMode) await styleManager._contextControls._selectContextMode(null);
|
|
const contactsResult = await styleManager._contextControls._selectContextMode('flights');
|
|
const missionResult = await styleManager._contextControls._selectContextMode('space-missions');
|
|
const switched = {
|
|
contactsResult,
|
|
missionResult,
|
|
mode: styleManager._contextControls._contextMode,
|
|
missionsEnabled: dataManager.isEnabled('rocket-launches'),
|
|
toasts: [...toasts],
|
|
};
|
|
const exitResult = await styleManager._contextControls._selectContextMode(null);
|
|
return {
|
|
...switched,
|
|
exitResult,
|
|
modeAfterExit: styleManager._contextControls._contextMode,
|
|
missionsOffAfterExit: !dataManager.isEffectivelyEnabled('rocket-launches'),
|
|
};
|
|
} finally {
|
|
styleManager._showToast = originalShowToast;
|
|
}
|
|
});
|
|
check(
|
|
'Contacts hands off to Space Missions on the first request without a failure toast',
|
|
contactMissionHandoff.contactsResult === true
|
|
&& contactMissionHandoff.missionResult === true
|
|
&& contactMissionHandoff.mode === 'space-missions'
|
|
&& contactMissionHandoff.missionsEnabled
|
|
&& contactMissionHandoff.toasts?.length === 0
|
|
&& contactMissionHandoff.exitResult === true
|
|
&& contactMissionHandoff.modeAfterExit === null
|
|
&& contactMissionHandoff.missionsOffAfterExit,
|
|
JSON.stringify(contactMissionHandoff),
|
|
);
|
|
const cancelledMissionEntry = await page.evaluate(async () => {
|
|
const styleManager = window.__godsEyeView.styleManager;
|
|
const dataManager = window.__godsEyeView.dataManager;
|
|
const rocketEntry = dataManager.layers.get('rocket-launches');
|
|
if (!rocketEntry || dataManager.isEffectivelyEnabled('rocket-launches')) {
|
|
return { exercised: false, reason: 'Space Missions was not in a clean OFF state' };
|
|
}
|
|
|
|
const siblingId = 'qa-cockpit-cancel-sibling';
|
|
const siblingModule = {
|
|
id: siblingId,
|
|
name: 'QA cancellation sibling',
|
|
icon: 'science',
|
|
source: 'QA',
|
|
updateInterval: -1,
|
|
async init() { return true; },
|
|
async enable() { return true; },
|
|
async update() { return true; },
|
|
async disable() { return true; },
|
|
async destroy() {},
|
|
getStats() { return { count: 1, status: 'live' }; },
|
|
};
|
|
window.__gevQaRegisterLayer(dataManager, siblingModule);
|
|
await dataManager.setEnabled(siblingId, true, { origin: 'programmatic' });
|
|
|
|
const originalInitialized = rocketEntry.initialized;
|
|
const originalLifecycle = rocketEntry.lifecycleState;
|
|
const originalMethods = {
|
|
init: rocketEntry.module.init,
|
|
enable: rocketEntry.module.enable,
|
|
update: rocketEntry.module.update,
|
|
disable: rocketEntry.module.disable,
|
|
};
|
|
const originalShowToast = styleManager._showToast;
|
|
const toasts = [];
|
|
let releaseEnable;
|
|
let markEnableStarted;
|
|
const enableStarted = new Promise((resolve) => { markEnableStarted = resolve; });
|
|
const enableGate = new Promise((resolve) => { releaseEnable = resolve; });
|
|
rocketEntry.initialized = true;
|
|
rocketEntry.module.enable = async () => {
|
|
markEnableStarted();
|
|
await enableGate;
|
|
return true;
|
|
};
|
|
rocketEntry.module.update = async () => true;
|
|
rocketEntry.module.disable = async () => true;
|
|
styleManager._showToast = (message) => { toasts.push(String(message)); };
|
|
|
|
let transitionResult = null;
|
|
let timedOut = false;
|
|
try {
|
|
const controller = new AbortController();
|
|
const transition = dataManager.setEnabled('rocket-launches', true, {
|
|
origin: 'user',
|
|
signal: controller.signal,
|
|
});
|
|
await enableStarted;
|
|
controller.abort(new DOMException('QA caller cancellation', 'AbortError'));
|
|
releaseEnable();
|
|
transitionResult = await transition;
|
|
const deadline = performance.now() + 5_000;
|
|
while (performance.now() < deadline) {
|
|
if (
|
|
styleManager._contextControls._contextModeEntering === null
|
|
&& styleManager._contextControls._contextSessionSnapshot === null
|
|
&& dataManager.isEnabled(siblingId)
|
|
&& !dataManager.isEffectivelyEnabled('rocket-launches')
|
|
) break;
|
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
}
|
|
timedOut = !(
|
|
styleManager._contextControls._contextModeEntering === null
|
|
&& styleManager._contextControls._contextSessionSnapshot === null
|
|
&& dataManager.isEnabled(siblingId)
|
|
&& !dataManager.isEffectivelyEnabled('rocket-launches')
|
|
);
|
|
return {
|
|
exercised: true,
|
|
transitionResult,
|
|
timedOut,
|
|
entering: styleManager._contextControls._contextModeEntering,
|
|
snapshotRetained: Boolean(styleManager._contextControls._contextSessionSnapshot),
|
|
siblingRestored: dataManager.isEnabled(siblingId),
|
|
missionEffective: dataManager.isEffectivelyEnabled('rocket-launches'),
|
|
toasts,
|
|
};
|
|
} finally {
|
|
styleManager._showToast = originalShowToast;
|
|
rocketEntry.module.init = originalMethods.init;
|
|
rocketEntry.module.enable = originalMethods.enable;
|
|
rocketEntry.module.update = originalMethods.update;
|
|
rocketEntry.module.disable = originalMethods.disable;
|
|
rocketEntry.initialized = originalInitialized;
|
|
rocketEntry.lifecycleState = originalLifecycle;
|
|
await dataManager.setEnabled(siblingId, false, { origin: 'programmatic' });
|
|
await window.__gevQaUnregisterLayer(dataManager, siblingId);
|
|
}
|
|
});
|
|
check(
|
|
'cancelled direct Space Missions entry restores its exact isolated sibling state',
|
|
cancelledMissionEntry.exercised
|
|
&& cancelledMissionEntry.transitionResult === false
|
|
&& !cancelledMissionEntry.timedOut
|
|
&& cancelledMissionEntry.entering === null
|
|
&& !cancelledMissionEntry.snapshotRetained
|
|
&& cancelledMissionEntry.siblingRestored
|
|
&& !cancelledMissionEntry.missionEffective,
|
|
JSON.stringify(cancelledMissionEntry),
|
|
);
|
|
check(
|
|
'successful Space Missions cancellation rollback stays silent',
|
|
cancelledMissionEntry.exercised && cancelledMissionEntry.toasts?.length === 0,
|
|
JSON.stringify(cancelledMissionEntry.toasts || []),
|
|
);
|
|
const replacementMissionEntry = await page.evaluate(async () => {
|
|
const styleManager = window.__godsEyeView.styleManager;
|
|
const dataManager = window.__godsEyeView.dataManager;
|
|
const rocketEntry = dataManager.layers.get('rocket-launches');
|
|
if (!rocketEntry || dataManager.isEffectivelyEnabled('rocket-launches')) {
|
|
return { exercised: false, reason: 'Space Missions was not in a clean OFF state' };
|
|
}
|
|
const siblingId = 'qa-cockpit-replacement-sibling';
|
|
window.__gevQaRegisterLayer(dataManager, {
|
|
id: siblingId,
|
|
name: 'QA replacement sibling',
|
|
icon: 'science',
|
|
source: 'QA',
|
|
updateInterval: -1,
|
|
async init() { return true; },
|
|
async enable() { return true; },
|
|
async update() { return true; },
|
|
async disable() { return true; },
|
|
async destroy() {},
|
|
getStats() { return { count: 1, status: 'live' }; },
|
|
});
|
|
await dataManager.setEnabled(siblingId, true, { origin: 'programmatic' });
|
|
const original = {
|
|
siblingInitialized: dataManager.layers.get(siblingId)?.initialized,
|
|
initialized: rocketEntry.initialized,
|
|
lifecycleState: rocketEntry.lifecycleState,
|
|
init: rocketEntry.module.init,
|
|
enable: rocketEntry.module.enable,
|
|
update: rocketEntry.module.update,
|
|
disable: rocketEntry.module.disable,
|
|
showToast: styleManager._showToast,
|
|
};
|
|
let firstUpdate = true;
|
|
let releaseUpdate;
|
|
let markUpdateStarted;
|
|
const updateStarted = new Promise((resolve) => { markUpdateStarted = resolve; });
|
|
const updateGate = new Promise((resolve) => { releaseUpdate = resolve; });
|
|
const toasts = [];
|
|
rocketEntry.initialized = true;
|
|
rocketEntry.module.enable = async () => true;
|
|
rocketEntry.module.update = async () => {
|
|
if (!firstUpdate) return true;
|
|
firstUpdate = false;
|
|
markUpdateStarted();
|
|
await updateGate;
|
|
return true;
|
|
};
|
|
rocketEntry.module.disable = async () => true;
|
|
styleManager._showToast = (message) => { toasts.push(String(message)); };
|
|
try {
|
|
const entry = styleManager._contextControls._selectContextMode('space-missions');
|
|
await updateStarted;
|
|
const replacement = dataManager.setEnabled('rocket-launches', true, { origin: 'programmatic' });
|
|
releaseUpdate();
|
|
const [entryResult, replacementResult] = await Promise.all([entry, replacement]);
|
|
const committed = {
|
|
entryResult,
|
|
replacementResult,
|
|
mode: styleManager._contextControls._contextMode,
|
|
entering: styleManager._contextControls._contextModeEntering,
|
|
snapshotRetained: Boolean(styleManager._contextControls._contextSessionSnapshot),
|
|
siblingIsolated: !dataManager.isEffectivelyEnabled(siblingId),
|
|
missionEnabled: dataManager.isEnabled('rocket-launches'),
|
|
missionEffective: dataManager.isEffectivelyEnabled('rocket-launches'),
|
|
toasts: [...toasts],
|
|
};
|
|
const exitResult = await styleManager._contextControls._selectContextMode(null);
|
|
return {
|
|
exercised: true,
|
|
...committed,
|
|
exitResult,
|
|
siblingRestored: dataManager.isEnabled(siblingId),
|
|
missionRestoredOff: !dataManager.isEffectivelyEnabled('rocket-launches'),
|
|
};
|
|
} finally {
|
|
styleManager._showToast = original.showToast;
|
|
rocketEntry.module.init = original.init;
|
|
rocketEntry.module.enable = original.enable;
|
|
rocketEntry.module.update = original.update;
|
|
rocketEntry.module.disable = original.disable;
|
|
rocketEntry.initialized = original.initialized;
|
|
rocketEntry.lifecycleState = original.lifecycleState;
|
|
await dataManager.setEnabled(siblingId, false, { origin: 'programmatic' });
|
|
await window.__gevQaUnregisterLayer(dataManager, siblingId);
|
|
}
|
|
});
|
|
check(
|
|
'right-rail Space Missions adopts a newer authoritative ON without stale restoration',
|
|
replacementMissionEntry.exercised
|
|
&& replacementMissionEntry.entryResult === true
|
|
&& replacementMissionEntry.replacementResult === true
|
|
&& replacementMissionEntry.mode === 'space-missions'
|
|
&& replacementMissionEntry.entering === null
|
|
&& replacementMissionEntry.snapshotRetained
|
|
&& replacementMissionEntry.siblingIsolated
|
|
&& replacementMissionEntry.missionEnabled
|
|
&& replacementMissionEntry.missionEffective
|
|
&& replacementMissionEntry.toasts?.length === 0
|
|
&& replacementMissionEntry.exitResult === true
|
|
&& replacementMissionEntry.siblingRestored
|
|
&& replacementMissionEntry.missionRestoredOff,
|
|
JSON.stringify(replacementMissionEntry),
|
|
);
|
|
const supersededMissionEntry = await page.evaluate(async () => {
|
|
const styleManager = window.__godsEyeView.styleManager;
|
|
const dataManager = window.__godsEyeView.dataManager;
|
|
const rocketEntry = dataManager.layers.get('rocket-launches');
|
|
if (!rocketEntry || dataManager.isEffectivelyEnabled('rocket-launches')) {
|
|
return { exercised: false, reason: 'Space Missions was not in a clean OFF state' };
|
|
}
|
|
const siblingId = 'qa-cockpit-superseded-sibling';
|
|
window.__gevQaRegisterLayer(dataManager, {
|
|
id: siblingId,
|
|
name: 'QA superseded sibling',
|
|
icon: 'science',
|
|
source: 'QA',
|
|
updateInterval: -1,
|
|
async init() { return true; },
|
|
async enable() { return true; },
|
|
async update() { return true; },
|
|
async disable() { return true; },
|
|
async destroy() {},
|
|
getStats() { return { count: 1, status: 'live' }; },
|
|
});
|
|
await dataManager.setEnabled(siblingId, true, { origin: 'programmatic' });
|
|
const original = {
|
|
initialized: rocketEntry.initialized,
|
|
lifecycleState: rocketEntry.lifecycleState,
|
|
init: rocketEntry.module.init,
|
|
enable: rocketEntry.module.enable,
|
|
update: rocketEntry.module.update,
|
|
disable: rocketEntry.module.disable,
|
|
showToast: styleManager._showToast,
|
|
};
|
|
let releaseUpdate;
|
|
let markUpdateStarted;
|
|
const updateStarted = new Promise((resolve) => { markUpdateStarted = resolve; });
|
|
const updateGate = new Promise((resolve) => { releaseUpdate = resolve; });
|
|
const toasts = [];
|
|
rocketEntry.initialized = true;
|
|
rocketEntry.module.enable = async () => true;
|
|
rocketEntry.module.update = async () => {
|
|
markUpdateStarted();
|
|
await updateGate;
|
|
return true;
|
|
};
|
|
rocketEntry.module.disable = async () => true;
|
|
styleManager._showToast = (message) => { toasts.push(String(message)); };
|
|
try {
|
|
const entry = styleManager._runUserFacingContextAction(
|
|
(notificationToken) => styleManager._contextControls._selectContextMode(
|
|
'space-missions',
|
|
{ notificationToken },
|
|
),
|
|
'Space Missions could not complete the requested transition; try again',
|
|
);
|
|
await updateStarted;
|
|
const supersedingOff = dataManager.setEnabled('rocket-launches', false, {
|
|
origin: 'programmatic',
|
|
});
|
|
releaseUpdate();
|
|
const [entryResult, offResult] = await Promise.all([entry, supersedingOff]);
|
|
return {
|
|
exercised: true,
|
|
entryResult,
|
|
offResult,
|
|
mode: styleManager._contextControls._contextMode,
|
|
entering: styleManager._contextControls._contextModeEntering,
|
|
snapshotRetained: Boolean(styleManager._contextControls._contextSessionSnapshot),
|
|
siblingRestored: dataManager.isEnabled(siblingId),
|
|
missionEffective: dataManager.isEffectivelyEnabled('rocket-launches'),
|
|
toasts,
|
|
};
|
|
} finally {
|
|
styleManager._showToast = original.showToast;
|
|
rocketEntry.module.init = original.init;
|
|
rocketEntry.module.enable = original.enable;
|
|
rocketEntry.module.update = original.update;
|
|
rocketEntry.module.disable = original.disable;
|
|
rocketEntry.initialized = original.initialized;
|
|
rocketEntry.lifecycleState = original.lifecycleState;
|
|
await dataManager.setEnabled(siblingId, false, { origin: 'programmatic' });
|
|
await window.__gevQaUnregisterLayer(dataManager, siblingId);
|
|
}
|
|
});
|
|
check(
|
|
'right-rail Space Missions superseded by OFF restores exactly without a failure toast',
|
|
supersededMissionEntry.exercised
|
|
&& supersededMissionEntry.entryResult === null
|
|
&& supersededMissionEntry.mode === null
|
|
&& supersededMissionEntry.entering === null
|
|
&& !supersededMissionEntry.snapshotRetained
|
|
&& supersededMissionEntry.siblingRestored
|
|
&& !supersededMissionEntry.missionEffective
|
|
&& supersededMissionEntry.toasts?.length === 0,
|
|
JSON.stringify(supersededMissionEntry),
|
|
);
|
|
const voiceMissionEntry = await page.evaluate(async () => {
|
|
const { dataManager, styleManager } = window.__godsEyeView;
|
|
const siblingId = '__qa_voice_context_sibling__';
|
|
const rocketEntry = dataManager.layers.get('rocket-launches');
|
|
window.__gevQaRegisterLayer(dataManager, {
|
|
id: siblingId,
|
|
name: 'QA voice Context sibling',
|
|
icon: 'science',
|
|
source: 'QA',
|
|
updateInterval: -1,
|
|
async init() { return true; },
|
|
async enable() { return true; },
|
|
async update() { return true; },
|
|
async disable() { return true; },
|
|
async destroy() {},
|
|
getStats() { return { count: 1, status: 'live' }; },
|
|
});
|
|
const original = {
|
|
initialized: rocketEntry.initialized,
|
|
lifecycleState: rocketEntry.lifecycleState,
|
|
init: rocketEntry.module.init,
|
|
enable: rocketEntry.module.enable,
|
|
update: rocketEntry.module.update,
|
|
disable: rocketEntry.module.disable,
|
|
};
|
|
const siblingLayerEntry = dataManager.layers.get(siblingId);
|
|
siblingLayerEntry.initialized = true;
|
|
await dataManager.setEnabled(siblingId, true, { origin: 'programmatic' });
|
|
rocketEntry.initialized = true;
|
|
rocketEntry.module.init = async () => true;
|
|
rocketEntry.module.enable = async () => true;
|
|
rocketEntry.module.update = async () => true;
|
|
rocketEntry.module.disable = async () => true;
|
|
try {
|
|
const voiceOn = await dataManager.setEnabled('rocket-launches', true, { origin: 'voice' });
|
|
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
const voiceState = {
|
|
voiceOn,
|
|
mode: styleManager._contextControls._contextMode,
|
|
entering: styleManager._contextControls._contextModeEntering,
|
|
snapshotRetained: Boolean(styleManager._contextControls._contextSessionSnapshot),
|
|
siblingIsolated: !dataManager.isEffectivelyEnabled(siblingId),
|
|
};
|
|
const snapshotBeforeOff = [...(styleManager._contextControls._contextSessionSnapshot?.enabledLayerIds || [])];
|
|
const voiceOff = await dataManager.setEnabled('rocket-launches', false, { origin: 'voice' });
|
|
const reactionCountAfterOff = styleManager._contextControls._contextLayerReactionPromises.size;
|
|
await styleManager._waitForContextLayerSettlement();
|
|
const restoredSiblingEntry = dataManager.layers.get(siblingId);
|
|
const voiceExit = {
|
|
voiceOff,
|
|
snapshotBeforeOff,
|
|
reactionCountAfterOff,
|
|
mode: styleManager._contextControls._contextMode,
|
|
entering: styleManager._contextControls._contextModeEntering,
|
|
snapshotRetained: Boolean(styleManager._contextControls._contextSessionSnapshot),
|
|
siblingRestored: dataManager.isEffectivelyEnabled(siblingId),
|
|
siblingEnabled: restoredSiblingEntry?.enabled,
|
|
siblingLifecycle: restoredSiblingEntry?.lifecycleState,
|
|
restoreActive: Boolean(styleManager._contextControls._contextRestoreState),
|
|
};
|
|
|
|
await dataManager.setEnabled('rocket-launches', true, { origin: 'programmatic' });
|
|
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
const internalState = {
|
|
mode: styleManager._contextControls._contextMode,
|
|
entering: styleManager._contextControls._contextModeEntering,
|
|
snapshotRetained: Boolean(styleManager._contextControls._contextSessionSnapshot),
|
|
missionEnabled: dataManager.isEffectivelyEnabled('rocket-launches'),
|
|
};
|
|
await dataManager.setEnabled('rocket-launches', false, { origin: 'programmatic' });
|
|
return { exercised: true, voiceState, voiceExit, internalState };
|
|
} finally {
|
|
rocketEntry.module.init = original.init;
|
|
rocketEntry.module.enable = original.enable;
|
|
rocketEntry.module.update = original.update;
|
|
rocketEntry.module.disable = original.disable;
|
|
rocketEntry.initialized = original.initialized;
|
|
rocketEntry.lifecycleState = original.lifecycleState;
|
|
await dataManager.setEnabled(siblingId, false, { origin: 'programmatic' });
|
|
siblingLayerEntry.initialized = original.siblingInitialized;
|
|
await window.__gevQaUnregisterLayer(dataManager, siblingId);
|
|
}
|
|
});
|
|
check(
|
|
'voice Space Missions uses the same Context snapshot and exact-restore transaction as UI entry',
|
|
voiceMissionEntry.exercised
|
|
&& voiceMissionEntry.voiceState?.voiceOn
|
|
&& voiceMissionEntry.voiceState.mode === 'space-missions'
|
|
&& voiceMissionEntry.voiceState.entering === null
|
|
&& voiceMissionEntry.voiceState.snapshotRetained
|
|
&& voiceMissionEntry.voiceState.siblingIsolated
|
|
&& voiceMissionEntry.voiceExit?.voiceOff
|
|
&& voiceMissionEntry.voiceExit.mode === null
|
|
&& voiceMissionEntry.voiceExit.entering === null
|
|
&& !voiceMissionEntry.voiceExit.snapshotRetained
|
|
&& voiceMissionEntry.voiceExit.siblingRestored,
|
|
JSON.stringify(voiceMissionEntry),
|
|
);
|
|
check(
|
|
'internal programmatic Space Missions activation does not create a user Context session',
|
|
voiceMissionEntry.internalState?.missionEnabled
|
|
&& voiceMissionEntry.internalState.mode === null
|
|
&& voiceMissionEntry.internalState.entering === null
|
|
&& !voiceMissionEntry.internalState.snapshotRetained,
|
|
JSON.stringify(voiceMissionEntry),
|
|
);
|
|
await page.evaluate(() => window.__godsEyeView.dataManager.setEnabled(
|
|
'flights',
|
|
true,
|
|
{ origin: 'user' },
|
|
));
|
|
await page.waitForFunction(() => {
|
|
const layer = window.__godsEyeView?.dataManager?.layers?.get('flights')?.module;
|
|
return (layer?.getAllPositions?.(500) || []).some(
|
|
(candidate) => Number(candidate.altitudeM) > 1_000,
|
|
);
|
|
}, { timeout: 60_000 });
|
|
const tracked = await page.evaluate(() => {
|
|
const layer = window.__godsEyeView.dataManager.layers.get('flights')?.module;
|
|
const candidates = layer?.getAllPositions?.(500) || [];
|
|
const airborne = candidates.find((candidate) => Number(candidate.altitudeM) > 1_000);
|
|
return {
|
|
id: airborne?.id || null,
|
|
tracked: Boolean(airborne && layer.trackById?.(airborne.id)),
|
|
};
|
|
});
|
|
check('real airborne flight is tracked before Contacts activation', tracked.tracked, JSON.stringify(tracked));
|
|
await page.waitForFunction(
|
|
() => Boolean(window.__godsEyeView.viewer.trackedEntity?.position),
|
|
{ timeout: 10_000 },
|
|
);
|
|
const preselectedContactAdoption = await page.evaluate(async () => {
|
|
const { styleManager, dataManager, viewer } = window.__godsEyeView;
|
|
const flights = dataManager.layers.get('flights')?.module;
|
|
await dataManager.setEnabled('military', true, { origin: 'programmatic' });
|
|
const before = flights?.getTrackedInfo?.() || null;
|
|
const trackedEntityBefore = viewer.trackedEntity;
|
|
const blockerId = '__qa_slow_contacts_sibling__';
|
|
const gateTimeoutMs = 5_000;
|
|
// The transition settles behind real dependency enables (network-bound), so its
|
|
// guard matches the harness's other network waits — a hang stop, not a budget.
|
|
const settleTimeoutMs = 60_000;
|
|
let releaseDisable = () => {};
|
|
let reportDisableStarted = () => {};
|
|
const disableGate = new Promise((resolve) => { releaseDisable = resolve; });
|
|
const disableStarted = new Promise((resolve) => { reportDisableStarted = resolve; });
|
|
// Every await in this probe is bounded: the gated sibling teardown is the
|
|
// behaviour under test, so a no-show has to fail the check with evidence
|
|
// rather than hang the whole harness on an unresolved promise.
|
|
const settleWithin = (promise, ms) => Promise.race([
|
|
promise.then(
|
|
(value) => ({ settled: true, value }),
|
|
(error) => ({ settled: true, value: `error: ${String(error?.message || error)}` }),
|
|
),
|
|
new Promise((resolve) => { setTimeout(() => resolve({ settled: false, value: null }), ms); }),
|
|
]);
|
|
window.__gevQaRegisterLayer(dataManager, {
|
|
id: blockerId,
|
|
name: 'QA slow Contacts sibling',
|
|
icon: 'science',
|
|
source: 'QA',
|
|
updateInterval: -1,
|
|
async init() { return true; },
|
|
async enable() { return true; },
|
|
async update() { return true; },
|
|
async disable() {
|
|
reportDisableStarted();
|
|
await disableGate;
|
|
return true;
|
|
},
|
|
async destroy() {},
|
|
getStats() { return { count: 1, status: 'live' }; },
|
|
});
|
|
let deferCleanup = false;
|
|
try {
|
|
const blockerEntry = dataManager.layers.get(blockerId);
|
|
blockerEntry.initialized = true;
|
|
await dataManager.setEnabled(blockerId, true, { origin: 'programmatic' });
|
|
const transition = styleManager._contextControls._selectContextMode('flights');
|
|
const disableStart = await settleWithin(disableStarted, gateTimeoutMs);
|
|
if (!disableStart.settled) {
|
|
releaseDisable();
|
|
const abandoned = await settleWithin(transition, settleTimeoutMs);
|
|
return {
|
|
disableObserved: false,
|
|
beforeId: before?.icao24 || null,
|
|
timeoutReason: `QA blocker disable() never started within ${gateTimeoutMs}ms — `
|
|
+ 'the Contacts transition never tore its sibling down, so the pending-window '
|
|
+ 'probe could not be exercised',
|
|
transitionSettled: abandoned.settled,
|
|
transitionResult: abandoned.value,
|
|
};
|
|
}
|
|
styleManager.cockpitView.syncEntry();
|
|
const entry = document.getElementById('cockpit-entry');
|
|
const pending = {
|
|
changing: styleManager._contextControls._contextModeChanging,
|
|
entryHidden: Boolean(entry?.hidden),
|
|
enterResult: styleManager.cockpitView.enter(),
|
|
trackerPreserved: viewer.trackedEntity === trackedEntityBefore,
|
|
};
|
|
releaseDisable();
|
|
const settleStartedAt = performance.now();
|
|
const settledTransition = await settleWithin(transition, settleTimeoutMs);
|
|
const settleMs = Math.round(performance.now() - settleStartedAt);
|
|
styleManager.cockpitView.syncEntry();
|
|
const awareness = dataManager.layers.get('military-awareness')?.module;
|
|
const snapshot = awareness?.getContextSnapshot?.() || null;
|
|
const trackedInfo = flights?.getTrackedInfo?.() || null;
|
|
deferCleanup = true;
|
|
return {
|
|
disableObserved: true,
|
|
beforeId: before?.icao24 || null,
|
|
pending,
|
|
transitionSettled: settledTransition.settled,
|
|
transitionResult: settledTransition.value,
|
|
settleMs,
|
|
subject: snapshot?.subject ? {
|
|
layerId: snapshot.subject.layerId,
|
|
id: snapshot.subject.id,
|
|
} : null,
|
|
navigation: snapshot?.navigation || null,
|
|
afterId: trackedInfo?.icao24 || null,
|
|
viewerTrackedId: viewer.trackedEntity?.gevTrackedId || null,
|
|
contextVisible: !document.getElementById('military-awareness-panel')?.hidden,
|
|
entryAvailableAfterSettlement: Boolean(entry && !entry.hidden),
|
|
blockerStillRegistered: dataManager.layers.has(blockerId),
|
|
blockerEffectivelyOff: !dataManager.isEffectivelyEnabled(blockerId),
|
|
};
|
|
} finally {
|
|
// Never leave the injected layer (or a gated disable) behind — a failed
|
|
// probe must not poison the checks that run after it.
|
|
releaseDisable();
|
|
if (!deferCleanup && dataManager.layers.has(blockerId)) {
|
|
if (styleManager._contextControls._contextSessionSnapshot) await styleManager._contextControls._selectContextMode(null);
|
|
await window.__gevQaUnregisterLayer(dataManager, blockerId);
|
|
styleManager.cockpitView.syncEntry();
|
|
}
|
|
}
|
|
});
|
|
check(
|
|
'Contacts blocks early Cockpit entry, then adopts the already tracked flight after settlement',
|
|
preselectedContactAdoption.disableObserved
|
|
&& preselectedContactAdoption.transitionSettled
|
|
&& preselectedContactAdoption.beforeId
|
|
&& preselectedContactAdoption.pending?.changing
|
|
&& preselectedContactAdoption.pending.entryHidden
|
|
&& preselectedContactAdoption.pending.enterResult === false
|
|
&& preselectedContactAdoption.pending.trackerPreserved
|
|
&& preselectedContactAdoption.transitionResult === true
|
|
&& preselectedContactAdoption.subject?.layerId === 'flights'
|
|
&& String(preselectedContactAdoption.subject.id).toLowerCase()
|
|
=== String(preselectedContactAdoption.beforeId).toLowerCase()
|
|
&& String(preselectedContactAdoption.afterId).toLowerCase()
|
|
=== String(preselectedContactAdoption.beforeId).toLowerCase()
|
|
&& String(preselectedContactAdoption.viewerTrackedId).toLowerCase()
|
|
=== `flights:${String(preselectedContactAdoption.beforeId).toLowerCase()}`
|
|
&& preselectedContactAdoption.navigation?.canPrevious === false
|
|
&& typeof preselectedContactAdoption.navigation?.canNext === 'boolean'
|
|
&& preselectedContactAdoption.contextVisible
|
|
&& preselectedContactAdoption.entryAvailableAfterSettlement
|
|
&& preselectedContactAdoption.blockerStillRegistered
|
|
&& preselectedContactAdoption.blockerEffectivelyOff,
|
|
JSON.stringify(preselectedContactAdoption),
|
|
);
|
|
// The slow thing in the field is the FETCH, so that is what this holds — the
|
|
// REAL militaryInstallations module runs throughout and reports its own
|
|
// getStats(). An earlier cut stubbed `enable` and `getStats` instead. Both
|
|
// were wrong, and provably so:
|
|
// * `enable()` is SYNCHRONOUS in production (the module's own comment says
|
|
// "DataLayerManager invokes update() immediately after enable(), which
|
|
// owns the first fetch"), so gating it blocked the Contacts activation
|
|
// that brings the PANEL up. The scenario then sampled markup an earlier
|
|
// scenario had left behind, which is why it read a stale `0`.
|
|
// * The panel does not observe a `getStats` stub in this scenario at all —
|
|
// verified by making the stub report an ANSWERED source mid-window and
|
|
// watching every assertion stay green. A check written against a stub the
|
|
// panel cannot see has no teeth, which is the same defect this scenario
|
|
// was created to fix.
|
|
// Holding the network reproduces the field exactly and the panel tracks it,
|
|
// which is what gives these assertions teeth.
|
|
// Earlier scenarios have already loaded installations. Use a fresh page so
|
|
// a legitimate retained snapshot cannot satisfy or invalidate a FIRST-fetch
|
|
// assertion when camera motion supersedes the manager's initial update.
|
|
const readinessPage = await browser.newPage();
|
|
let deferredInstallationReadiness;
|
|
try {
|
|
await readinessPage.setViewport({ width: 1440, height: 900 });
|
|
readinessPage.on('pageerror', (error) => consoleErrors.push(error.message));
|
|
await readinessPage.setRequestInterception(true);
|
|
readinessPage.on('request', (request) => {
|
|
const url = new URL(request.url());
|
|
if (url.origin === new URL(appUrl).origin && url.pathname === '/api/flights') {
|
|
const now = Math.floor(Date.now() / 1000);
|
|
request.respond({
|
|
status: 200, contentType: 'application/json',
|
|
body: JSON.stringify({ time: now, states: [
|
|
['aaa051', 'QA051', 'Synthetic', now, now, -97.7431, 30.2672,
|
|
9000, false, 230, 90, 0, null, 9000, null, false, 0],
|
|
] }),
|
|
});
|
|
return;
|
|
}
|
|
routeQaRequest(request);
|
|
});
|
|
const readinessUrl = new URL(appUrl);
|
|
readinessUrl.searchParams.set('welcome', '0');
|
|
await readinessPage.goto(readinessUrl.href, { waitUntil: 'domcontentloaded' });
|
|
await readinessPage.waitForFunction(() => window.__godsEyeView?.dataManager
|
|
&& document.getElementById('loading-screen')?.classList.contains('hidden'), { timeout: 60000 });
|
|
await readinessPage.evaluate(async () => {
|
|
const manager = window.__godsEyeView.dataManager;
|
|
await manager.setEnabled('flights', true, { origin: 'user' });
|
|
if (!manager.layers.get('flights').module.trackById('aaa051')) {
|
|
throw new Error('First-fetch fixture aircraft was not tracked');
|
|
}
|
|
if (manager.layers.get('military-installations').module.getStats().lastUpdate != null) {
|
|
throw new Error('First-fetch fixture already has an installation snapshot');
|
|
}
|
|
});
|
|
deferredInstallationReadiness = await readinessPage.evaluate(async () => {
|
|
const { styleManager, dataManager } = window.__godsEyeView;
|
|
const entry = dataManager.layers.get('military-installations');
|
|
if (!entry?.module) return { exercised: false, reason: 'military-installations missing' };
|
|
const settleWithin = (promise, ms) => Promise.race([
|
|
promise.then(
|
|
(value) => ({ settled: true, value }),
|
|
(error) => ({ settled: true, value: `error: ${String(error?.message || error)}` }),
|
|
),
|
|
new Promise((resolve) => { setTimeout(() => resolve({ settled: false, value: null }), ms); }),
|
|
]);
|
|
const row = () => [...document.querySelectorAll('#military-awareness-panel .military-awareness-row')]
|
|
.find((candidate) => candidate.querySelector('strong')?.textContent?.trim() === 'Mapped installations');
|
|
const installationCount = () => row()?.querySelector('b')?.textContent?.trim() || null;
|
|
/** The REASON text, which is what tracks availability: the cohort engine
|
|
* answers 'feed unavailable' / 'feed stale' when it refuses to count. */
|
|
const installationReason = () => row()?.querySelector('small')?.textContent?.trim() || null;
|
|
|
|
const realFetch = window.fetch;
|
|
let released = false;
|
|
let requestSeen = false;
|
|
let transition = null;
|
|
try {
|
|
await styleManager._contextControls._selectContextMode(null);
|
|
await dataManager.setEnabled('military-installations', false, { origin: 'programmatic' });
|
|
|
|
// Hold the first Overpass request open, honouring the module's own
|
|
// AbortSignal so its camera-settle abort/refetch behaves as it does live.
|
|
window.fetch = (input, init) => {
|
|
const raw = typeof input === 'string' || input instanceof URL ? String(input) : input?.url;
|
|
let url = null;
|
|
try { url = new URL(raw, window.location.href); } catch { return realFetch(input, init); }
|
|
if (url.pathname !== '/api/military-installations') return realFetch(input, init);
|
|
requestSeen = true;
|
|
const signal = init?.signal;
|
|
return new Promise((resolve, reject) => {
|
|
let done = false;
|
|
const fail = () => {
|
|
if (done) return;
|
|
done = true;
|
|
const error = new Error('aborted');
|
|
error.name = 'AbortError';
|
|
reject(error);
|
|
};
|
|
if (signal) {
|
|
if (signal.aborted) return fail();
|
|
signal.addEventListener('abort', fail, { once: true });
|
|
}
|
|
const tick = () => {
|
|
if (done) return;
|
|
if (released) {
|
|
done = true;
|
|
resolve(new Response(
|
|
JSON.stringify({ elements: [], retrievedAt: new Date().toISOString() }),
|
|
{ status: 200, headers: { 'Content-Type': 'application/json' } },
|
|
));
|
|
return;
|
|
}
|
|
setTimeout(tick, 100);
|
|
};
|
|
tick();
|
|
});
|
|
};
|
|
|
|
transition = styleManager._contextControls._selectContextMode('flights');
|
|
// The activation promise stays PENDING while the first fetch is out, and
|
|
// the Contacts panel comes up behind it — that is what a deferred
|
|
// dependency is for. Verified live on :4272: the panel renders and reads
|
|
// a non-numeric count for the whole of a 17 s first fetch.
|
|
const started = await settleWithin((async () => {
|
|
for (let i = 0; i < 100 && !(requestSeen && row()); i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
}
|
|
return requestSeen && Boolean(row());
|
|
})(), 12_000);
|
|
styleManager.cockpitView.syncEntry();
|
|
const pendingLifecycle = dataManager.getLayerLifecycleState('military-installations');
|
|
const pendingCount = installationCount();
|
|
const pendingReason = installationReason();
|
|
|
|
// THE WINDOW: first request outstanding, panel live. Sampled more than
|
|
// once — the field report was a panel sitting on a fabricated 0 for ~13 s,
|
|
// so a single lucky read is not evidence.
|
|
const fetchingCounts = [];
|
|
const fetchingReasons = [];
|
|
for (let i = 0; i < 6; i++) {
|
|
fetchingCounts.push(installationCount());
|
|
fetchingReasons.push(installationReason());
|
|
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
}
|
|
|
|
released = true;
|
|
const contacts = await settleWithin(transition, 20_000);
|
|
styleManager.cockpitView.syncEntry();
|
|
// Measured AFTER activation settles, not during it. The sibling pin above
|
|
// ("Contacts blocks early Cockpit entry, then adopts the already tracked
|
|
// flight after settlement") establishes that Contacts HIDES the entry
|
|
// while the transition is changing — that is the designed contract, and
|
|
// asserting the opposite (as this scenario used to) could never pass.
|
|
// What matters is that a slow dependency does not lock it away for good.
|
|
const cockpitAvailableAfterActivation = !document.getElementById('cockpit-entry')?.hidden;
|
|
const installed = await settleWithin((async () => {
|
|
while (dataManager.getLayerLifecycleState('military-installations')?.lifecycleState !== 'enabled') {
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
return true;
|
|
})(), 20_000);
|
|
const answered = await settleWithin((async () => {
|
|
for (let i = 0; i < 60 && !/^\d+$/.test(String(installationCount())); i++) {
|
|
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
}
|
|
return true;
|
|
})(), 12_000);
|
|
|
|
return {
|
|
exercised: true,
|
|
started,
|
|
contacts,
|
|
pendingLifecycle,
|
|
pendingCount,
|
|
pendingReason,
|
|
cockpitAvailableAfterActivation,
|
|
installed,
|
|
settledLifecycle: dataManager.getLayerLifecycleState('military-installations'),
|
|
fetchingCounts,
|
|
fetchingReasons,
|
|
answered,
|
|
settledCount: installationCount(),
|
|
settledReason: installationReason(),
|
|
};
|
|
} finally {
|
|
released = true;
|
|
window.fetch = realFetch;
|
|
if (transition) await settleWithin(transition, 20_000);
|
|
if (styleManager._contextControls._contextMode !== 'flights') {
|
|
await settleWithin(styleManager._contextControls._selectContextMode('flights'), 15_000);
|
|
}
|
|
}
|
|
});
|
|
} finally {
|
|
await readinessPage.close();
|
|
await page.bringToFront();
|
|
}
|
|
// The panel has two truthful ways to say "nothing has answered yet" — the
|
|
// cohort count renders `?`, and an UNKNOWN relationship renders as that word.
|
|
// Which one is on screen depends on which panel surface is mounted, and the
|
|
// property under test is not the token: it is that the operator is NEVER
|
|
// shown a fabricated NUMBER for a source that has not answered. So these
|
|
// assert numeric-vs-not, which is exactly the field report.
|
|
const readsUnknown = (value) => typeof value === 'string' && value.length > 0 && !/^\d/.test(value);
|
|
const readsNumber = (value) => typeof value === 'string' && /^\d+$/.test(value);
|
|
// Loading has explicit copy since the installations reliability update.
|
|
// Keep the numeric/count gates below: wording alone never proves readiness.
|
|
const readsPendingReason = (value) => (
|
|
/unavailable|stale|Mapped sites not loaded|Fetching mapped sites…/i.test(String(value))
|
|
);
|
|
check(
|
|
'mapped installations load behind Contacts without a false all-clear or a locked Cockpit',
|
|
deferredInstallationReadiness.exercised
|
|
&& deferredInstallationReadiness.started?.settled
|
|
&& deferredInstallationReadiness.contacts?.settled
|
|
&& deferredInstallationReadiness.contacts.value === true
|
|
&& deferredInstallationReadiness.pendingLifecycle?.lifecycleState === 'enabling'
|
|
&& readsUnknown(deferredInstallationReadiness.pendingCount)
|
|
&& readsPendingReason(deferredInstallationReadiness.pendingReason)
|
|
&& deferredInstallationReadiness.cockpitAvailableAfterActivation
|
|
&& deferredInstallationReadiness.installed?.settled,
|
|
JSON.stringify(deferredInstallationReadiness),
|
|
);
|
|
check(
|
|
'installations still fetching behind a live Contacts panel never reads a number',
|
|
deferredInstallationReadiness.exercised
|
|
&& deferredInstallationReadiness.pendingLifecycle?.lifecycleState === 'enabling'
|
|
&& Array.isArray(deferredInstallationReadiness.fetchingCounts)
|
|
&& deferredInstallationReadiness.fetchingCounts.length > 0
|
|
&& deferredInstallationReadiness.fetchingCounts.every(readsUnknown)
|
|
&& Array.isArray(deferredInstallationReadiness.fetchingReasons)
|
|
&& deferredInstallationReadiness.fetchingReasons.every(readsPendingReason),
|
|
JSON.stringify({
|
|
settledLifecycle: deferredInstallationReadiness.settledLifecycle,
|
|
fetchingCounts: deferredInstallationReadiness.fetchingCounts,
|
|
fetchingReasons: deferredInstallationReadiness.fetchingReasons,
|
|
}),
|
|
);
|
|
check(
|
|
'the mapped-installations count appears once the first fetch actually answers',
|
|
deferredInstallationReadiness.exercised
|
|
&& deferredInstallationReadiness.answered?.settled
|
|
&& readsNumber(deferredInstallationReadiness.settledCount)
|
|
&& !readsPendingReason(deferredInstallationReadiness.settledReason),
|
|
JSON.stringify({
|
|
answered: deferredInstallationReadiness.answered,
|
|
settledCount: deferredInstallationReadiness.settledCount,
|
|
}),
|
|
);
|
|
const locationContactHandoff = await page.evaluate(async () => {
|
|
const { styleManager, dataManager, viewer } = window.__godsEyeView;
|
|
const awareness = dataManager.layers.get('military-awareness')?.module;
|
|
const before = awareness?.getContextSnapshot?.()?.subject || null;
|
|
document.querySelector('#location-pills .location-pill')?.click();
|
|
await new Promise((resolve) => setTimeout(resolve, 3600));
|
|
const after = awareness?.getContextSnapshot?.()?.subject || null;
|
|
const released = !viewer.trackedEntity;
|
|
const refocused = awareness?.focusCurrent?.() === true;
|
|
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
const owningLayer = after?.layerId === 'militaryFlights' ? 'military-flights' : after?.layerId;
|
|
const trackedInfo = dataManager.layers.get(owningLayer)?.module?.getTrackedInfo?.() || null;
|
|
return {
|
|
before: before ? { layerId: before.layerId, id: before.id } : null,
|
|
after: after ? { layerId: after.layerId, id: after.id } : null,
|
|
mode: styleManager._contextControls._contextMode,
|
|
released,
|
|
refocused,
|
|
trackedId: trackedInfo?.icao24 || trackedInfo?.id || null,
|
|
};
|
|
});
|
|
check(
|
|
'Location moves independently while Contact selection persists and Focus restores it',
|
|
locationContactHandoff.before?.id
|
|
&& locationContactHandoff.after?.id === locationContactHandoff.before.id
|
|
&& locationContactHandoff.after?.layerId === locationContactHandoff.before.layerId
|
|
&& locationContactHandoff.mode === 'flights'
|
|
&& locationContactHandoff.released
|
|
&& locationContactHandoff.refocused
|
|
&& String(locationContactHandoff.trackedId).toLowerCase()
|
|
=== String(locationContactHandoff.before.id).toLowerCase(),
|
|
JSON.stringify(locationContactHandoff),
|
|
);
|
|
const zoomedOutContactRefocus = await page.evaluate(async () => {
|
|
const { dataManager, viewer } = window.__godsEyeView;
|
|
const awareness = dataManager.layers.get('military-awareness')?.module;
|
|
const before = awareness?.getContextSnapshot?.()?.subject || null;
|
|
const entityBefore = viewer.trackedEntity;
|
|
viewer.camera.zoomOut(1_500_000);
|
|
const cameraRange = () => Math.hypot(
|
|
viewer.camera.position.x,
|
|
viewer.camera.position.y,
|
|
viewer.camera.position.z,
|
|
);
|
|
const zoomedRange = cameraRange();
|
|
const refocused = awareness?.focusCurrent?.() === true;
|
|
// The follow frame commits in Cesium preUpdate. A fixed 180ms delay can
|
|
// sample before that frame on a busy software renderer.
|
|
const frameDeadline = performance.now() + 5000;
|
|
while (cameraRange() >= 50_000 && performance.now() < frameDeadline) {
|
|
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
}
|
|
const focusedRange = cameraRange();
|
|
const after = awareness?.getContextSnapshot?.()?.subject || null;
|
|
return {
|
|
before: before ? { layerId: before.layerId, id: before.id } : null,
|
|
after: after ? { layerId: after.layerId, id: after.id } : null,
|
|
sameEntity: viewer.trackedEntity === entityBefore,
|
|
refocused,
|
|
zoomedRange: Math.round(zoomedRange),
|
|
focusedRange: Math.round(focusedRange),
|
|
};
|
|
});
|
|
check(
|
|
'Contact Focus restores the canonical follow frame after a manual zoom-out',
|
|
zoomedOutContactRefocus.before?.id
|
|
&& zoomedOutContactRefocus.after?.id === zoomedOutContactRefocus.before.id
|
|
&& zoomedOutContactRefocus.after?.layerId === zoomedOutContactRefocus.before.layerId
|
|
&& zoomedOutContactRefocus.sameEntity
|
|
&& zoomedOutContactRefocus.refocused
|
|
&& zoomedOutContactRefocus.zoomedRange > 1_000_000
|
|
&& zoomedOutContactRefocus.focusedRange < 50_000,
|
|
JSON.stringify(zoomedOutContactRefocus),
|
|
);
|
|
await page.waitForFunction(() => {
|
|
const entry = document.getElementById('cockpit-entry');
|
|
return entry && !entry.hidden && !entry.disabled;
|
|
}, { timeout: 10_000 });
|
|
await page.$eval('#cockpit-entry', (entry) => entry.click());
|
|
await page.waitForFunction(
|
|
() => document.body.classList.contains('cockpit-mode')
|
|
&& window.__godsEyeView.styleManager.cockpitView.active
|
|
&& !document.getElementById('cockpit-hud').hidden,
|
|
{ timeout: 10_000 },
|
|
);
|
|
await page.waitForFunction(
|
|
() => {
|
|
// Require the node: `!missing?.hidden` is vacuously true, so a dropped
|
|
// #cockpit-context would sail past this gate instead of failing here.
|
|
const context = document.getElementById('cockpit-context');
|
|
return Boolean(context) && !context.hidden;
|
|
},
|
|
{ timeout: 10_000 },
|
|
);
|
|
const firstCockpitContact = await page.evaluate(() => {
|
|
const awareness = window.__godsEyeView.dataManager.layers
|
|
.get('military-awareness')?.module;
|
|
const snapshot = awareness?.getContextSnapshot?.() || null;
|
|
const context = document.getElementById('cockpit-context');
|
|
const subjectText = document.getElementById('cockpit-context-subject')?.textContent || '';
|
|
const previous = document.getElementById('cockpit-context-previous');
|
|
const next = document.getElementById('cockpit-context-next');
|
|
const signalText = document.getElementById('cockpit-signal-list')?.textContent || '';
|
|
return {
|
|
subject: snapshot?.subject ? {
|
|
layerId: snapshot.subject.layerId,
|
|
id: snapshot.subject.id,
|
|
label: snapshot.subject.label,
|
|
} : null,
|
|
contextVisible: Boolean(context && !context.hidden
|
|
&& getComputedStyle(context).display !== 'none'),
|
|
subjectText,
|
|
previousDisabled: previous?.disabled,
|
|
nextDisabled: next?.disabled,
|
|
navigation: snapshot?.navigation || null,
|
|
contextStandby: signalText.includes('CONTEXT STANDBY'),
|
|
};
|
|
});
|
|
check(
|
|
'first Cockpit entry shows matching Contact Previous/Next controls',
|
|
firstCockpitContact.contextVisible
|
|
&& firstCockpitContact.subject?.layerId === 'flights'
|
|
&& String(firstCockpitContact.subject.id).toLowerCase()
|
|
=== String(tracked.id).toLowerCase()
|
|
&& firstCockpitContact.subjectText.startsWith(
|
|
`${firstCockpitContact.subject.label} ·`,
|
|
)
|
|
&& firstCockpitContact.previousDisabled
|
|
=== !firstCockpitContact.navigation?.canPrevious
|
|
&& firstCockpitContact.nextDisabled
|
|
=== !firstCockpitContact.navigation?.canNext
|
|
&& !firstCockpitContact.contextStandby,
|
|
JSON.stringify(firstCockpitContact),
|
|
);
|
|
// Owner playtest 2026-08-18: "when you click on Contacts, detections should
|
|
// just turn on, and they should stay on in Cockpit or in third-person
|
|
// tracking inside Contacts or inside Cockpit, both… when I leave the Cockpit,
|
|
// detections go off" — that last part being the bug. Driven through the REAL
|
|
// context-mode transaction and the REAL CockpitViewController with a live
|
|
// tracked flight: the one path a Node unit test cannot boot.
|
|
const contactsDetection = await page.evaluate(async () => {
|
|
const { styleManager } = window.__godsEyeView;
|
|
const cockpit = styleManager.cockpitView;
|
|
const mode = () => styleManager.getDetectionState().detectionMode;
|
|
const density = () => styleManager.getDetectionState().densityPct;
|
|
const settle = (ms = 160) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
// Cockpit entry needs an auto-focused subject, which arrives a beat after
|
|
// the Contacts transaction settles. Bounded so a stall fails the check
|
|
// rather than hanging the harness.
|
|
const enterCockpit = async () => {
|
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
if (cockpit.enter() === true) return true;
|
|
await settle(200);
|
|
}
|
|
return false;
|
|
};
|
|
const originalOverridden = styleManager._detectionUserOverridden;
|
|
|
|
// Start from outside both, so activation is a real edge.
|
|
cockpit.exit({ restoreTracking: true });
|
|
await settle();
|
|
await styleManager._contextControls._selectContextMode(null);
|
|
await settle(400);
|
|
const cleanupSnapshotCleared = styleManager._contextControls._contextSessionSnapshot === null;
|
|
const cleanupBlockerId = '__qa_slow_contacts_sibling__';
|
|
const cleanupUnregistered = await window.__gevQaUnregisterLayer(
|
|
window.__godsEyeView.dataManager,
|
|
cleanupBlockerId,
|
|
);
|
|
const cleanupBlockerAbsent = !window.__godsEyeView.dataManager.layers.has(cleanupBlockerId);
|
|
// Adversarial precondition: detection explicitly OFF at a NON-tactical
|
|
// density, and the operator already flagged as having overridden detection
|
|
// this session — the flag that suppresses the military-style auto-enable.
|
|
styleManager.setDetection({ enabled: false, densityPct: 25 });
|
|
await settle();
|
|
const beforeContacts = mode();
|
|
const overriddenBeforeContacts = styleManager._detectionUserOverridden;
|
|
|
|
// 1. Activating Contacts forces the tactical preset on.
|
|
const contactsOn = await styleManager._contextControls._selectContextMode('flights');
|
|
await settle(400);
|
|
const afterContactsOn = mode();
|
|
const afterContactsOnDensity = density();
|
|
|
|
// 2. Cockpit enter and exit are moves WITHIN the session: detection unchanged.
|
|
const entered = await enterCockpit();
|
|
await settle();
|
|
const insideCockpit = mode();
|
|
cockpit.cycleVisionMode(1);
|
|
await settle();
|
|
const afterVisionCycle = mode();
|
|
cockpit.setVisionMode('optical');
|
|
await settle();
|
|
const exited = cockpit.exit({ restoreTracking: true }) === true;
|
|
await settle();
|
|
const afterCockpitExit = mode();
|
|
const afterCockpitExitDensity = density();
|
|
|
|
// 3. A manual off during the session holds — even across a cockpit re-entry.
|
|
document.getElementById('detection-toggle')?.click();
|
|
await settle();
|
|
const afterManualOff = mode();
|
|
const reentered = await enterCockpit();
|
|
await settle();
|
|
const afterCockpitReentry = mode();
|
|
cockpit.exit({ restoreTracking: true });
|
|
await settle();
|
|
|
|
// 4. Deactivating Contacts restores the pre-Contacts state.
|
|
await styleManager._contextControls._selectContextMode(null);
|
|
await settle(400);
|
|
const afterContactsOff = mode();
|
|
const afterContactsOffDensity = density();
|
|
// The operator's next manual enable must come back at THEIR profile, not
|
|
// the tactical density Contacts stamped on.
|
|
document.getElementById('detection-toggle')?.click();
|
|
await settle();
|
|
const manualEnableMode = mode();
|
|
const manualEnableDensity = density();
|
|
styleManager.setDetection({ enabled: false });
|
|
await settle();
|
|
|
|
// Leave the session exactly as the following checks expect it.
|
|
const restoredContacts = await styleManager._contextControls._selectContextMode('flights');
|
|
await settle(400);
|
|
const restoredCockpit = await enterCockpit();
|
|
styleManager._detectionUserOverridden = originalOverridden;
|
|
await settle();
|
|
return {
|
|
beforeContacts,
|
|
overriddenBeforeContacts,
|
|
contactsOn,
|
|
afterContactsOn,
|
|
afterContactsOnDensity,
|
|
entered,
|
|
insideCockpit,
|
|
afterVisionCycle,
|
|
exited,
|
|
afterCockpitExit,
|
|
afterCockpitExitDensity,
|
|
afterManualOff,
|
|
reentered,
|
|
afterCockpitReentry,
|
|
afterContactsOff,
|
|
afterContactsOffDensity,
|
|
manualEnableMode,
|
|
manualEnableDensity,
|
|
restoredContacts,
|
|
restoredCockpit,
|
|
cockpitActive: cockpit.active,
|
|
cleanupSnapshotCleared,
|
|
cleanupUnregistered,
|
|
cleanupBlockerAbsent,
|
|
};
|
|
});
|
|
check(
|
|
'Contacts owns detection: activation forces the tactical preset, cockpit transitions leave it alone, deactivation restores',
|
|
contactsDetection.contactsOn === true
|
|
&& contactsDetection.entered
|
|
&& contactsDetection.exited
|
|
&& contactsDetection.reentered
|
|
&& contactsDetection.restoredContacts === true
|
|
&& contactsDetection.restoredCockpit
|
|
&& contactsDetection.cockpitActive
|
|
&& contactsDetection.cleanupSnapshotCleared
|
|
&& contactsDetection.cleanupUnregistered
|
|
&& contactsDetection.cleanupBlockerAbsent
|
|
&& contactsDetection.beforeContacts === 'OFF'
|
|
&& contactsDetection.overriddenBeforeContacts === true
|
|
// Activation forces the military look — Dense @ 75%, not the 25% the
|
|
// operator was sitting at, and regardless of the override flag.
|
|
&& contactsDetection.afterContactsOn === 'DENSE'
|
|
&& contactsDetection.afterContactsOnDensity === 75
|
|
// Cockpit is a move WITHIN Contacts: enter, vision cycle and — the actual
|
|
// bug — EXIT must all leave detection exactly where it was.
|
|
&& contactsDetection.insideCockpit === 'DENSE'
|
|
&& contactsDetection.afterVisionCycle === 'DENSE'
|
|
&& contactsDetection.afterCockpitExit === 'DENSE'
|
|
&& contactsDetection.afterCockpitExitDensity === 75
|
|
// A manual off holds for the session, including across a cockpit re-entry.
|
|
&& contactsDetection.afterManualOff === 'OFF'
|
|
&& contactsDetection.afterCockpitReentry === 'OFF'
|
|
// Leaving Contacts restores the pre-Contacts state — mode AND density, so
|
|
// the operator's next manual enable returns their own 25% profile rather
|
|
// than the tactical 75% Contacts stamped on.
|
|
&& contactsDetection.afterContactsOff === 'OFF'
|
|
&& contactsDetection.afterContactsOffDensity === 25
|
|
&& contactsDetection.manualEnableMode === 'SPARSE'
|
|
&& contactsDetection.manualEnableDensity === 25,
|
|
JSON.stringify(contactsDetection),
|
|
);
|
|
const densityNavigation = await page.evaluate(async () => {
|
|
const { styleManager, dataManager, viewer } = window.__godsEyeView;
|
|
const cockpit = styleManager.cockpitView;
|
|
const awareness = dataManager.layers.get('military-awareness')?.module;
|
|
const settle = (ms = 260) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
const snapshot = (step) => {
|
|
const context = awareness?.getContextSnapshot?.() || null;
|
|
const tracker = viewer.trackedEntity || cockpit.trackedEntity;
|
|
return {
|
|
step,
|
|
cockpitActive: cockpit.active,
|
|
bodyCockpit: document.body.classList.contains('cockpit-mode'),
|
|
contextMode: styleManager._contextControls._contextMode,
|
|
contextChanging: styleManager._contextControls._contextModeChanging,
|
|
subject: context?.subject ? `${context.subject.layerId}:${context.subject.id}` : null,
|
|
tracked: tracker?.gevTrackedId || null,
|
|
density: styleManager.getDetectionState().densityPct,
|
|
detectionMode: styleManager.getDetectionState().detectionMode,
|
|
installations: dataManager.getLayerLifecycleState('military-installations'),
|
|
};
|
|
};
|
|
const setDensityFromUi = (value) => {
|
|
const slider = document.getElementById('detection-density-slider');
|
|
slider.value = String(value);
|
|
slider.dispatchEvent(new Event('input', { bubbles: true }));
|
|
};
|
|
|
|
const states = [snapshot('before')];
|
|
setDensityFromUi(25);
|
|
states.push(snapshot('sparse'));
|
|
document.getElementById('cockpit-context-next')?.click();
|
|
await settle(1700);
|
|
states.push(snapshot('ui-next'));
|
|
setDensityFromUi(75);
|
|
document.getElementById('cockpit-context-previous')?.click();
|
|
await settle(1700);
|
|
states.push(snapshot('ui-previous'));
|
|
const voiceNext = styleManager.controlCockpit('next');
|
|
await settle(1700);
|
|
states.push(snapshot('voice-next'));
|
|
const voicePrevious = styleManager.controlCockpit('previous');
|
|
await settle(1700);
|
|
states.push(snapshot('voice-previous'));
|
|
|
|
const exited = cockpit.exit({ restoreTracking: true }) === true;
|
|
await settle(100);
|
|
const mapBefore = snapshot('map-before');
|
|
const mapNext = awareness?.navigateNext?.({ origin: 'user' }) === true;
|
|
await settle();
|
|
const mapAfterNext = snapshot('map-next');
|
|
const mapPrevious = awareness?.navigatePrevious?.({ origin: 'user' }) === true;
|
|
await settle();
|
|
const mapAfterPrevious = snapshot('map-previous');
|
|
const reentered = cockpit.enter() === true;
|
|
await settle(100);
|
|
states.push(snapshot('reentered'));
|
|
return {
|
|
states,
|
|
voiceNext: voiceNext.ok,
|
|
voicePrevious: voicePrevious.ok,
|
|
exited,
|
|
mapBefore,
|
|
mapNext,
|
|
mapAfterNext,
|
|
mapPrevious,
|
|
mapAfterPrevious,
|
|
reentered,
|
|
};
|
|
});
|
|
const cockpitStatesStable = densityNavigation.states.every((state) => (
|
|
state.cockpitActive
|
|
&& state.bodyCockpit
|
|
&& state.contextMode === 'flights'
|
|
&& !state.contextChanging
|
|
&& state.subject
|
|
&& state.tracked
|
|
));
|
|
check(
|
|
'Dense/Sparse UI and voice Next/Previous keep Cockpit active and standard Flights tracking stable',
|
|
cockpitStatesStable
|
|
&& densityNavigation.states.find(({ step }) => step === 'sparse')?.density === 25
|
|
&& densityNavigation.states.find(({ step }) => step === 'ui-next')?.density === 25
|
|
&& densityNavigation.states.find(({ step }) => step === 'ui-previous')?.density === 75
|
|
&& densityNavigation.voiceNext
|
|
&& densityNavigation.voicePrevious
|
|
&& densityNavigation.exited
|
|
&& !densityNavigation.mapBefore.cockpitActive
|
|
&& densityNavigation.mapBefore.contextMode === 'flights'
|
|
&& densityNavigation.mapNext
|
|
&& densityNavigation.mapAfterNext.tracked
|
|
&& densityNavigation.mapPrevious
|
|
&& densityNavigation.mapAfterPrevious.tracked
|
|
&& densityNavigation.reentered,
|
|
JSON.stringify(densityNavigation),
|
|
);
|
|
const cockpitExitOwnership = await page.evaluate(async () => {
|
|
const { styleManager, dataManager, viewer } = window.__godsEyeView;
|
|
const awareness = dataManager.layers.get('military-awareness')?.module;
|
|
const entity = viewer.trackedEntity || styleManager.cockpitView.trackedEntity;
|
|
const nextFrame = () => new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
remove();
|
|
reject(new Error('Cockpit camera ownership check did not render a frame'));
|
|
}, 5000);
|
|
const remove = viewer.scene.postRender.addEventListener(() => {
|
|
remove();
|
|
clearTimeout(timer);
|
|
resolve();
|
|
});
|
|
});
|
|
// Let revoked listeners retire before comparing ownership across exit.
|
|
await nextFrame();
|
|
const listenersBeforeExit = viewer.scene.preUpdate.numberOfListeners;
|
|
const exited = styleManager.cockpitView.exit() === true;
|
|
await nextFrame();
|
|
const listenersAfterExit = viewer.scene.preUpdate.numberOfListeners;
|
|
const refocused = awareness?.focusCurrent?.() === true;
|
|
await nextFrame();
|
|
return {
|
|
exited,
|
|
refocused,
|
|
sameEntity: viewer.trackedEntity === entity,
|
|
listenersBeforeExit,
|
|
listenersAfterExit,
|
|
listenersAfterFocus: viewer.scene.preUpdate.numberOfListeners,
|
|
};
|
|
});
|
|
check(
|
|
'Cockpit exit and later Contact Focus retain one source-owned camera-frame listener',
|
|
cockpitExitOwnership.exited
|
|
&& cockpitExitOwnership.refocused
|
|
&& cockpitExitOwnership.sameEntity
|
|
&& cockpitExitOwnership.listenersAfterExit === cockpitExitOwnership.listenersBeforeExit + 1
|
|
&& cockpitExitOwnership.listenersAfterFocus === cockpitExitOwnership.listenersAfterExit,
|
|
JSON.stringify(cockpitExitOwnership),
|
|
);
|
|
const cockpitPanelRoundTrip = await page.evaluate(async () => {
|
|
const manager = window.__godsEyeView.styleManager;
|
|
const panelIds = [
|
|
'data-panel',
|
|
'cctv-panel',
|
|
'scene-panel',
|
|
'pp-toggles',
|
|
'global-context-panel',
|
|
'radio-panel',
|
|
];
|
|
const read = () => Object.fromEntries(panelIds.map((panelId) => [
|
|
panelId,
|
|
document.getElementById(panelId)?.classList.contains('collapsed'),
|
|
]));
|
|
const desired = new Map([
|
|
['data-panel', false],
|
|
['cctv-panel', true],
|
|
['scene-panel', true],
|
|
['pp-toggles', true],
|
|
['global-context-panel', false],
|
|
['radio-panel', true],
|
|
]);
|
|
for (const [panelId, collapsed] of desired) {
|
|
manager.setPanelCollapsed(panelId, collapsed, { persist: false, syncShare: false });
|
|
}
|
|
const before = read();
|
|
document.getElementById('cockpit-entry')?.click();
|
|
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
const during = read();
|
|
manager.setPanelCollapsed('data-panel', false, { persist: false, syncShare: false });
|
|
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
const dataRect = document.getElementById('data-panel')?.getBoundingClientRect();
|
|
const contactRect = document.getElementById('cockpit-context')?.getBoundingClientRect();
|
|
const clearancePx = dataRect && contactRect ? contactRect.top - dataRect.bottom : null;
|
|
const contactAutoCollapsed = manager.cockpitView.contextCollapsed === true;
|
|
manager.setPanelCollapsed('data-panel', true, { persist: false, syncShare: false });
|
|
const contactAutoRestored = manager.cockpitView.contextCollapsed === false;
|
|
manager.cockpitView.setContextCollapsed(true);
|
|
manager.setPanelCollapsed('data-panel', false, { persist: false, syncShare: false });
|
|
manager.setPanelCollapsed('data-panel', true, { persist: false, syncShare: false });
|
|
const manualContactCollapseRetained = manager.cockpitView.contextCollapsed === true;
|
|
manager.cockpitView.setContextCollapsed(false);
|
|
const exited = manager.cockpitView.exit() === true;
|
|
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
return {
|
|
before,
|
|
during,
|
|
after: read(),
|
|
exited,
|
|
clearancePx,
|
|
contactAutoCollapsed,
|
|
contactAutoRestored,
|
|
manualContactCollapseRetained,
|
|
};
|
|
});
|
|
check(
|
|
'Cockpit temporarily collapses standard panels and restores the exact map layout on exit',
|
|
cockpitPanelRoundTrip.exited
|
|
&& Object.values(cockpitPanelRoundTrip.during).every(Boolean)
|
|
&& JSON.stringify(cockpitPanelRoundTrip.after)
|
|
=== JSON.stringify(cockpitPanelRoundTrip.before)
|
|
&& Number.isFinite(cockpitPanelRoundTrip.clearancePx)
|
|
&& cockpitPanelRoundTrip.clearancePx >= 0
|
|
&& cockpitPanelRoundTrip.contactAutoCollapsed
|
|
&& cockpitPanelRoundTrip.contactAutoRestored
|
|
&& cockpitPanelRoundTrip.manualContactCollapseRetained,
|
|
JSON.stringify(cockpitPanelRoundTrip),
|
|
);
|
|
await page.waitForFunction(() => {
|
|
const entry = document.getElementById('cockpit-entry');
|
|
return entry && !entry.hidden && !entry.disabled;
|
|
}, { timeout: 10_000 });
|
|
await page.$eval('#cockpit-entry', (entry) => entry.click());
|
|
await page.waitForFunction(
|
|
() => document.body.classList.contains('cockpit-mode')
|
|
&& window.__godsEyeView.styleManager.cockpitView.active,
|
|
{ timeout: 10_000 },
|
|
);
|
|
await page.evaluate(() => {
|
|
const manager = window.__godsEyeView.styleManager;
|
|
manager._setCockpitDisclosure('display', false);
|
|
manager._setCockpitDisclosure('radio', false);
|
|
});
|
|
|
|
const visionCycle = await page.evaluate(async () => {
|
|
const manager = window.__godsEyeView.styleManager;
|
|
const cockpit = manager.cockpitView;
|
|
const current = document.getElementById('cockpit-vision-current');
|
|
const label = document.getElementById('cockpit-vision-current-label');
|
|
const settle = (ms = 900) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
const intensities = () => Object.fromEntries(
|
|
Object.entries(manager.stages || {}).map(([name, stage]) => [name, stage.uniforms?.intensity]),
|
|
);
|
|
const read = () => ({
|
|
mode: current?.dataset.cockpitVision,
|
|
label: label?.textContent?.trim(),
|
|
aria: current?.getAttribute('aria-label'),
|
|
displayOpen: document.getElementById('cockpit-display-toggle-btn')?.getAttribute('aria-expanded'),
|
|
radioOpen: document.getElementById('cockpit-radio-toggle-btn')?.getAttribute('aria-expanded'),
|
|
parametersActive: document.getElementById('param-slider-panel')?.classList.contains('active'),
|
|
parametersCollapsed: document.getElementById('param-slider-panel')?.classList.contains('collapsed'),
|
|
signalCollapsed: cockpit.signalCollapsed,
|
|
focusRetained: document.activeElement === current,
|
|
activeStyle: manager.activeStyle,
|
|
intensities: intensities(),
|
|
});
|
|
const reenter = async () => {
|
|
cockpit.exit({ restoreTracking: true });
|
|
await settle();
|
|
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
if (cockpit.enter() === true) return true;
|
|
await settle(200);
|
|
}
|
|
return false;
|
|
};
|
|
manager._setCockpitDisclosure('display', false);
|
|
manager._setCockpitDisclosure('radio', false);
|
|
cockpit.setVisionMode('optical');
|
|
current.focus({ preventScroll: true });
|
|
const states = [read()];
|
|
for (let index = 0; index < 6; index += 1) {
|
|
current.click();
|
|
states.push(read());
|
|
}
|
|
const originalStyle = manager.activeStyle;
|
|
// A Noir map preset: Cockpit enters on Noir, Normal clears every shader
|
|
// without changing the map preset, and Exit Cockpit restores Noir.
|
|
cockpit.exit({ restoreTracking: true });
|
|
manager.setStyle('noir');
|
|
await settle();
|
|
const enteredOnNoir = await reenter();
|
|
const entryNoir = read();
|
|
cockpit.setVisionMode('optical');
|
|
const normalInsideNoir = read();
|
|
cockpit.setVisionMode('crt');
|
|
const cockpitOnlyCrt = read();
|
|
const exitedNoir = cockpit.exit({ restoreTracking: true }) === true;
|
|
await settle();
|
|
const restoredNoir = { ...read(), cockpitActive: cockpit.active };
|
|
manager.setStyle(originalStyle);
|
|
await settle();
|
|
const reentered = await reenter();
|
|
cockpit.setVisionMode('optical');
|
|
return {
|
|
states,
|
|
enteredOnNoir,
|
|
entryNoir,
|
|
normalInsideNoir,
|
|
cockpitOnlyCrt,
|
|
exitedNoir,
|
|
restoredNoir,
|
|
reentered,
|
|
};
|
|
});
|
|
const cycleModes = ['optical', 'crt', 'nvg', 'thermal', 'anime', 'noir', 'optical'];
|
|
const cycleLabels = ['NORMAL', 'CRT', 'NVG', 'FLIR', 'ANIME', 'NOIR', 'NORMAL'];
|
|
check(
|
|
'Cockpit vision control exposes Normal, CRT, NVG, FLIR, Anime and Noir before wrapping',
|
|
JSON.stringify(visionCycle.states.map((state) => state.mode)) === JSON.stringify(cycleModes)
|
|
&& JSON.stringify(visionCycle.states.map((state) => state.label)) === JSON.stringify(cycleLabels)
|
|
&& visionCycle.states[5]?.aria === 'Current cockpit vision style: Noir. Activate for next style.'
|
|
&& visionCycle.states[6]?.aria === 'Current cockpit vision style: Normal. Activate for next style.',
|
|
JSON.stringify(visionCycle.states.map(({ mode, label, aria }) => ({ mode, label, aria }))),
|
|
);
|
|
check(
|
|
'user vision changes open Display for every temporary treatment and retain selector focus',
|
|
visionCycle.states.slice(1, 6).every((state) => state.displayOpen === 'true')
|
|
&& visionCycle.states.slice(1, 6).every((state) => state.radioOpen === 'false')
|
|
&& visionCycle.states.slice(1, 6).every((state) => state.parametersActive)
|
|
&& visionCycle.states.slice(1, 6).every((state) => !state.parametersCollapsed)
|
|
&& visionCycle.states.slice(1, 6).every((state) => state.signalCollapsed)
|
|
&& visionCycle.states.every((state) => state.focusRetained),
|
|
JSON.stringify(visionCycle.states),
|
|
);
|
|
check(
|
|
'each Cockpit vision mode drives only its own shader and Normal clears them all',
|
|
visionCycle.states.slice(1, 6).every((state) => {
|
|
const target = {
|
|
crt: 'retro', nvg: 'surveillance', thermal: 'thermal', anime: 'anime', noir: 'noir',
|
|
}[state.mode];
|
|
return Object.entries(state.intensities).every(([name, value]) =>
|
|
name === target ? value > 0 : value === 0);
|
|
})
|
|
&& Object.values(visionCycle.states[6].intensities).every((value) => value === 0),
|
|
JSON.stringify(visionCycle.states.map(({ mode, intensities }) => ({ mode, intensities }))),
|
|
);
|
|
check(
|
|
'Cockpit enters on the Noir map preset, Normal clears it, and Exit Cockpit restores it',
|
|
visionCycle.enteredOnNoir
|
|
&& visionCycle.entryNoir?.mode === 'noir'
|
|
&& visionCycle.entryNoir?.label === 'NOIR'
|
|
&& visionCycle.normalInsideNoir?.mode === 'optical'
|
|
&& visionCycle.normalInsideNoir?.label === 'NORMAL'
|
|
&& visionCycle.normalInsideNoir?.activeStyle === 'noir'
|
|
&& Object.values(visionCycle.normalInsideNoir?.intensities || {}).every((value) => value === 0)
|
|
&& visionCycle.cockpitOnlyCrt?.mode === 'crt'
|
|
&& visionCycle.cockpitOnlyCrt?.activeStyle === 'noir'
|
|
&& visionCycle.exitedNoir
|
|
&& !visionCycle.restoredNoir?.cockpitActive
|
|
&& visionCycle.restoredNoir?.activeStyle === 'noir'
|
|
&& visionCycle.restoredNoir?.intensities?.noir === 1
|
|
&& visionCycle.restoredNoir?.intensities?.retro === 0
|
|
&& visionCycle.reentered,
|
|
JSON.stringify(visionCycle),
|
|
);
|
|
await page.evaluate(() => window.__godsEyeView.styleManager.cockpitView.setVisionMode('noir'));
|
|
await page.screenshot({ path: path.join(shotsDir, 'vision-noir.png') });
|
|
await page.evaluate(() => window.__godsEyeView.styleManager.cockpitView.setVisionMode('optical'));
|
|
|
|
const desktopState = async (variant, openKind) => page.evaluate(async ({ variantName, kind }) => {
|
|
const manager = window.__godsEyeView.styleManager;
|
|
const hud = document.getElementById('cockpit-hud');
|
|
const signal = document.getElementById('cockpit-signal-stream');
|
|
const utility = document.getElementById('cockpit-utility-controls');
|
|
const display = document.getElementById('cockpit-display-toggle-btn');
|
|
const radio = document.getElementById('cockpit-radio-toggle-btn');
|
|
const displayControl = display.closest('.cockpit-utility-control');
|
|
const radioControl = radio.closest('.cockpit-utility-control');
|
|
manager._setCockpitDisclosure('display', false);
|
|
manager._setCockpitDisclosure('radio', false);
|
|
if (variantName === 'off') manager.hud.setMode('off');
|
|
else {
|
|
manager._setHudVariant(variantName);
|
|
manager.hud.setMode('on');
|
|
}
|
|
manager._updateHudButtonState();
|
|
await new Promise((resolve) => setTimeout(resolve, 360));
|
|
document.body.classList.add('cockpit-mode');
|
|
manager.cockpitView.active = true;
|
|
hud.hidden = false;
|
|
signal.hidden = false;
|
|
// Cockpit owns --cockpit-utility-top and republishes it every layout tick,
|
|
// so the corridor is driven through its real input: how far up the briefing
|
|
// card reaches. A short card leaves the strip a roomy corridor.
|
|
const priorSignalMaxHeight = signal.style.maxHeight;
|
|
signal.style.maxHeight = '150px';
|
|
manager._setCockpitDisclosure(kind, true);
|
|
// One pass must be enough. Record the result of the FIRST layout call, then
|
|
// confirm a second changes nothing, so a convergence regression cannot hide
|
|
// behind a repeated call.
|
|
manager.cockpitView.syncSignalLayout();
|
|
const firstPassTop = hud.style.getPropertyValue('--cockpit-utility-top');
|
|
const firstPassPrimaryOnly = utility.classList.contains('layout-primary-only');
|
|
manager.cockpitView.syncSignalLayout();
|
|
const signalTop = signal.getBoundingClientRect().top;
|
|
const expanded = kind === 'display' ? displayControl : radioControl;
|
|
const sibling = kind === 'display' ? radioControl : displayControl;
|
|
const state = {
|
|
variant: variantName,
|
|
kind,
|
|
signalTop: Math.round(signalTop),
|
|
firstPassTop,
|
|
idempotent: firstPassTop === hud.style.getPropertyValue('--cockpit-utility-top')
|
|
&& firstPassPrimaryOnly === utility.classList.contains('layout-primary-only'),
|
|
utilityTop: hud.style.getPropertyValue('--cockpit-utility-top'),
|
|
primaryOnly: utility.classList.contains('layout-primary-only'),
|
|
expandedRect: expanded.getBoundingClientRect().toJSON(),
|
|
siblingRect: sibling.getBoundingClientRect().toJSON(),
|
|
siblingDisplay: getComputedStyle(sibling).display,
|
|
siblingAriaHidden: sibling.getAttribute('aria-hidden'),
|
|
siblingExpanded: sibling.querySelector('[aria-expanded]')?.getAttribute('aria-expanded'),
|
|
contained: expanded.getBoundingClientRect().bottom <= signalTop - 7,
|
|
};
|
|
signal.style.maxHeight = priorSignalMaxHeight;
|
|
manager.cockpitView.syncSignalLayout();
|
|
return state;
|
|
}, { variantName: variant, kind: openKind });
|
|
|
|
for (const variant of ['off', 'minimal', 'tactical', 'operator']) {
|
|
for (const kind of ['display', 'radio']) {
|
|
const state = await desktopState(variant, kind);
|
|
check(
|
|
`${variant} ${kind} keeps its collapsed sibling visible in a roomy corridor`,
|
|
!state.primaryOnly
|
|
&& state.expandedRect.width > 0
|
|
&& state.siblingRect.width > 0
|
|
&& state.siblingRect.height > 0
|
|
&& state.siblingDisplay !== 'none'
|
|
&& state.siblingAriaHidden === 'false'
|
|
&& state.siblingExpanded === 'false'
|
|
&& state.contained,
|
|
JSON.stringify(state),
|
|
);
|
|
check(
|
|
`${variant} ${kind} settles the strip anchor in one layout pass`,
|
|
state.idempotent && state.firstPassTop === state.utilityTop,
|
|
`first=${state.firstPassTop} settled=${state.utilityTop}`,
|
|
);
|
|
}
|
|
}
|
|
await page.screenshot({ path: path.join(shotsDir, 'roomy-desktop.png') });
|
|
check(
|
|
'roomy screenshot remains in a real Cockpit session',
|
|
await page.evaluate(() => document.body.classList.contains('cockpit-mode')
|
|
&& window.__godsEyeView.styleManager.cockpitView.active
|
|
&& getComputedStyle(document.getElementById('cockpit-utility-controls')).display !== 'none'),
|
|
);
|
|
|
|
const portalScroll = await page.evaluate(async () => {
|
|
const manager = window.__godsEyeView.styleManager;
|
|
const standard = document.getElementById('pp-toggles');
|
|
const cockpit = document.getElementById('cockpit-display-panel');
|
|
const waitFrames = () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
const priorStandardMaxHeight = standard.style.maxHeight;
|
|
const priorStandardHeight = standard.style.height;
|
|
const priorStandardOverflow = standard.style.overflowY;
|
|
const priorCockpitMaxHeight = cockpit.style.maxHeight;
|
|
const priorCockpitHeight = cockpit.style.height;
|
|
const priorCockpitOverflow = cockpit.style.overflowY;
|
|
const standardWasCollapsed = standard.classList.contains('collapsed');
|
|
manager._setCockpitDisplayPortalActive(false);
|
|
document.body.classList.remove('cockpit-mode');
|
|
await waitFrames();
|
|
standard.classList.remove('collapsed');
|
|
standard.style.height = '120px';
|
|
standard.style.maxHeight = '120px';
|
|
standard.style.overflowY = 'auto';
|
|
standard.scrollTop = Math.min(80, standard.scrollHeight - standard.clientHeight);
|
|
await waitFrames();
|
|
const standardBefore = standard.scrollTop;
|
|
document.body.classList.add('cockpit-mode');
|
|
manager._setCockpitDisplayPortalActive(true);
|
|
manager._setCockpitDisclosure('display', true);
|
|
await waitFrames();
|
|
cockpit.style.height = '120px';
|
|
cockpit.style.maxHeight = '120px';
|
|
cockpit.style.overflowY = 'auto';
|
|
cockpit.scrollTop = Math.min(60, cockpit.scrollHeight - cockpit.clientHeight);
|
|
await waitFrames();
|
|
const cockpitBefore = cockpit.scrollTop;
|
|
document.body.classList.remove('cockpit-mode');
|
|
manager._setCockpitDisplayPortalActive(false);
|
|
await waitFrames();
|
|
const standardAfter = standard.scrollTop;
|
|
const standardSaved = manager._standardDisplayScrollTop;
|
|
const standardClientHeight = standard.clientHeight;
|
|
const standardScrollHeight = standard.scrollHeight;
|
|
document.body.classList.add('cockpit-mode');
|
|
manager._setCockpitDisplayPortalActive(true);
|
|
manager._setCockpitDisclosure('display', true);
|
|
await waitFrames();
|
|
const cockpitAfter = cockpit.scrollTop;
|
|
standard.style.height = priorStandardHeight;
|
|
standard.style.maxHeight = priorStandardMaxHeight;
|
|
standard.style.overflowY = priorStandardOverflow;
|
|
cockpit.style.height = priorCockpitHeight;
|
|
cockpit.style.maxHeight = priorCockpitMaxHeight;
|
|
cockpit.style.overflowY = priorCockpitOverflow;
|
|
standard.classList.toggle('collapsed', standardWasCollapsed);
|
|
return {
|
|
standardBefore,
|
|
standardAfter,
|
|
standardSaved,
|
|
standardClientHeight,
|
|
standardScrollHeight,
|
|
cockpitBefore,
|
|
cockpitAfter,
|
|
};
|
|
});
|
|
check(
|
|
'Display portal round trip preserves standard and Cockpit scroll owners',
|
|
portalScroll.standardBefore > 0
|
|
&& portalScroll.cockpitBefore > 0
|
|
&& portalScroll.standardAfter === portalScroll.standardBefore
|
|
&& portalScroll.cockpitAfter === portalScroll.cockpitBefore,
|
|
JSON.stringify(portalScroll),
|
|
);
|
|
|
|
const boundary = await page.evaluate(() => {
|
|
const manager = window.__godsEyeView.styleManager;
|
|
const hud = document.getElementById('cockpit-hud');
|
|
const signal = document.getElementById('cockpit-signal-stream');
|
|
const utility = document.getElementById('cockpit-utility-controls');
|
|
const display = document.getElementById('cockpit-display-toggle-btn');
|
|
const radio = document.getElementById('cockpit-radio-toggle-btn');
|
|
const displayControl = display.closest('.cockpit-utility-control');
|
|
const radioControl = radio.closest('.cockpit-utility-control');
|
|
document.body.classList.add('cockpit-mode');
|
|
manager.cockpitView.active = true;
|
|
hud.hidden = false;
|
|
signal.hidden = false;
|
|
manager._setCockpitDisclosure('radio', false);
|
|
manager._setCockpitDisclosure('display', true);
|
|
const priorSignalMaxHeight = signal.style.maxHeight;
|
|
const priorSignalHeight = signal.style.height;
|
|
// The strip is pinned to the viewport ceiling once the briefing card grows
|
|
// into its lane, so the corridor is sized by moving that card's top edge —
|
|
// the wall Cockpit actually solves against — not by injecting an anchor.
|
|
// The card is bottom-anchored, so an explicit height moves its top edge.
|
|
const minTop = Math.max(96, window.innerHeight * 0.12);
|
|
const moveSignalTopTo = (wantTop) => {
|
|
for (let pass = 0; pass < 8; pass += 1) {
|
|
const rect = signal.getBoundingClientRect();
|
|
const delta = wantTop - rect.top;
|
|
if (Math.abs(delta) < 0.2) break;
|
|
const current = parseFloat(signal.style.height) || rect.height;
|
|
signal.style.height = `${Math.max(40, current - delta)}px`;
|
|
}
|
|
// One layout pass owns the decision; the second only proves it settled.
|
|
// (A pass that newly hides the collapsed sibling legitimately re-anchors
|
|
// the now-shorter strip, so idempotence is asserted where the strip's
|
|
// composition is unchanged.)
|
|
manager.cockpitView.syncSignalLayout();
|
|
const firstPassTop = hud.style.getPropertyValue('--cockpit-utility-top');
|
|
const firstPassPrimaryOnly = utility.classList.contains('layout-primary-only');
|
|
manager.cockpitView.syncSignalLayout();
|
|
return {
|
|
firstPassTop,
|
|
firstPassPrimaryOnly,
|
|
idempotent: firstPassTop === hud.style.getPropertyValue('--cockpit-utility-top')
|
|
&& firstPassPrimaryOnly === utility.classList.contains('layout-primary-only'),
|
|
};
|
|
};
|
|
signal.style.maxHeight = 'none';
|
|
signal.style.height = '150px';
|
|
manager.cockpitView.syncSignalLayout();
|
|
const expandedHeight = Math.max(displayControl.scrollHeight, displayControl.getBoundingClientRect().height);
|
|
const collapsedHeight = Math.max(50, radioControl.scrollHeight);
|
|
const stripHeight = expandedHeight + collapsedHeight + 7;
|
|
// Straddle the fit by a pixel instead of aiming AT it. `moveSignalTopTo` only
|
|
// converges to within 0.2 px and the production decision is a strict `>`
|
|
// against exactly that budget, so a corridor sized to the boundary itself
|
|
// resolves on sub-pixel residue — a coin flip, not a behaviour. It happened to
|
|
// land heads until Display grew 36 px on 2026-08-22 (the 3D Proximity/All row
|
|
// now ships open), which is what surfaced it. One pixel inside the fit and one
|
|
// pixel outside proves the same transition, deterministically.
|
|
const BOUNDARY_EPSILON_PX = 1;
|
|
const exactSignalTop = minTop + 8 + stripHeight + BOUNDARY_EPSILON_PX;
|
|
// The boundary is only meaningful when approached from the TWO-control
|
|
// composition. The anchor is solved against the strip's currently RENDERED
|
|
// height, so arriving here already collapsed to primary-only makes the strip
|
|
// short, pushes the resolved top above minTop, and the "exact" corridor is
|
|
// then never evaluated at the boundary at all — the check would fail while
|
|
// production is behaving correctly.
|
|
//
|
|
// The fixed `height: 150px` card seed above used to guarantee that; it stopped
|
|
// once Display grew (the 3D Proximity/All row ships open from 2026-08-22, +36 px),
|
|
// because 150 px of card leaves a corridor sized for the OLD strip. Seed from
|
|
// the measured requirement instead, which holds for any future panel height,
|
|
// and assert the precondition rather than assuming it.
|
|
const roomySeed = moveSignalTopTo(exactSignalTop + 80);
|
|
const roomySeedExpanded = !utility.classList.contains('layout-primary-only')
|
|
&& roomySeed.firstPassPrimaryOnly === false;
|
|
const exact = {
|
|
...moveSignalTopTo(exactSignalTop),
|
|
utilityTop: hud.style.getPropertyValue('--cockpit-utility-top'),
|
|
primaryOnly: utility.classList.contains('layout-primary-only'),
|
|
siblingVisible: getComputedStyle(radioControl).display !== 'none',
|
|
siblingAriaHidden: radioControl.getAttribute('aria-hidden'),
|
|
};
|
|
radio.focus({ preventScroll: true });
|
|
const constrained = {
|
|
// One pixel on the far side of the same fit (`exactSignalTop` already sits
|
|
// one pixel inside it), so this is still the one-pixel constraint it claims.
|
|
...moveSignalTopTo(exactSignalTop - 2 * BOUNDARY_EPSILON_PX),
|
|
utilityTop: hud.style.getPropertyValue('--cockpit-utility-top'),
|
|
primaryOnly: utility.classList.contains('layout-primary-only'),
|
|
siblingDisplay: getComputedStyle(radioControl).display,
|
|
siblingRect: radioControl.getBoundingClientRect().toJSON(),
|
|
siblingAriaHidden: radioControl.getAttribute('aria-hidden'),
|
|
focusReturned: document.activeElement === display,
|
|
contained: displayControl.getBoundingClientRect().bottom
|
|
<= signal.getBoundingClientRect().top - 7,
|
|
};
|
|
const restored = {
|
|
...moveSignalTopTo(exactSignalTop + 80),
|
|
utilityTop: hud.style.getPropertyValue('--cockpit-utility-top'),
|
|
primaryOnly: utility.classList.contains('layout-primary-only'),
|
|
siblingVisible: getComputedStyle(radioControl).display !== 'none',
|
|
siblingAriaHidden: radioControl.getAttribute('aria-hidden'),
|
|
};
|
|
signal.style.maxHeight = priorSignalMaxHeight;
|
|
signal.style.height = priorSignalHeight;
|
|
manager.cockpitView.syncSignalLayout();
|
|
return { minTop, stripHeight, exactSignalTop, roomySeedExpanded, exact, constrained, restored };
|
|
});
|
|
check(
|
|
'exact fit keeps the sibling; one-pixel constraint hides it, transfers focus, and restores it',
|
|
boundary.roomySeedExpanded
|
|
&& !boundary.exact.primaryOnly
|
|
&& boundary.exact.siblingVisible
|
|
&& boundary.exact.siblingAriaHidden === 'false'
|
|
&& boundary.constrained.primaryOnly
|
|
&& boundary.constrained.siblingDisplay === 'none'
|
|
&& boundary.constrained.siblingRect.width === 0
|
|
&& boundary.constrained.siblingAriaHidden === 'true'
|
|
&& boundary.constrained.focusReturned
|
|
&& boundary.constrained.contained
|
|
&& !boundary.restored.primaryOnly
|
|
&& boundary.restored.siblingVisible
|
|
&& boundary.restored.siblingAriaHidden === 'false',
|
|
JSON.stringify(boundary),
|
|
);
|
|
check(
|
|
'each corridor change is decided by the first layout pass, not a repeated one',
|
|
boundary.exact.idempotent
|
|
&& boundary.restored.idempotent
|
|
&& boundary.constrained.firstPassPrimaryOnly,
|
|
JSON.stringify({
|
|
exact: { first: boundary.exact.firstPassTop, idempotent: boundary.exact.idempotent },
|
|
constrained: {
|
|
first: boundary.constrained.firstPassTop,
|
|
firstPassPrimaryOnly: boundary.constrained.firstPassPrimaryOnly,
|
|
},
|
|
restored: { first: boundary.restored.firstPassTop, idempotent: boundary.restored.idempotent },
|
|
}),
|
|
);
|
|
|
|
// Cyber relocates its collapsed briefing header into the expanded utility
|
|
// lane. Exercise actual height changes without reload and measure both cards.
|
|
await page.evaluate(() => {
|
|
const manager = window.__godsEyeView.styleManager;
|
|
manager._setHudVariant('cyber');
|
|
manager.hud.setMode('on');
|
|
manager._setCockpitDisclosure('display', false);
|
|
manager._setCockpitDisclosure('radio', true);
|
|
});
|
|
const cyberRadioOrder = await page.evaluate(() => {
|
|
const manager = window.__godsEyeView.styleManager;
|
|
manager.cockpitView.syncSignalLayout();
|
|
const display = document
|
|
.getElementById('cockpit-display-toggle-btn')
|
|
.closest('.cockpit-utility-control');
|
|
const radio = document
|
|
.getElementById('cockpit-radio-toggle-btn')
|
|
.closest('.cockpit-utility-control');
|
|
const displayRect = display.getBoundingClientRect();
|
|
const radioRect = radio.getBoundingClientRect();
|
|
return {
|
|
display: displayRect.toJSON(),
|
|
radio: radioRect.toJSON(),
|
|
displayExpanded: display.classList.contains('is-expanded'),
|
|
radioExpanded: radio.classList.contains('is-expanded'),
|
|
gap: radioRect.top - displayRect.bottom,
|
|
};
|
|
});
|
|
check(
|
|
'Cyber expanding Radio preserves the authored Display-then-Radio order',
|
|
!cyberRadioOrder.displayExpanded
|
|
&& cyberRadioOrder.radioExpanded
|
|
&& cyberRadioOrder.display.height > 0
|
|
&& cyberRadioOrder.radio.height > 0
|
|
&& cyberRadioOrder.gap >= 7,
|
|
JSON.stringify(cyberRadioOrder),
|
|
);
|
|
await page.screenshot({ path: path.join(shotsDir, 'cyber-radio-order.png') });
|
|
await page.evaluate(() => {
|
|
const manager = window.__godsEyeView.styleManager;
|
|
manager._setCockpitDisclosure('radio', false);
|
|
manager._setCockpitDisclosure('display', true);
|
|
});
|
|
const cyberStyleStates = await page.evaluate(() => {
|
|
const manager = window.__godsEyeView.styleManager;
|
|
const states = [];
|
|
for (const style of ['normal', 'surveillance', 'thermal', 'retro']) {
|
|
document.querySelector(`.style-btn[data-style="${style}"]`).click();
|
|
states.push({
|
|
requestedStyle: style,
|
|
activeStyle: manager.activeStyle,
|
|
hud: manager.hud.getVariant(),
|
|
theme: document.documentElement.dataset.uiTheme,
|
|
selector: document.getElementById('hud-layout-select').value,
|
|
savedHud: manager.getVisualState().hud.variant,
|
|
});
|
|
}
|
|
return states;
|
|
});
|
|
check('Cockpit visual-style controls retain Cyber HUD, selector, shell and saved layout',
|
|
cyberStyleStates.every(state => state.activeStyle === state.requestedStyle
|
|
&& state.hud === 'cyber' && state.theme === 'cyber'
|
|
&& state.selector === 'cyber' && state.savedHud === 'cyber'),
|
|
JSON.stringify(cyberStyleStates));
|
|
for (const height of [987, 720, 640, 900]) {
|
|
await page.setViewport({ width: 1440, height, deviceScaleFactor: 1 });
|
|
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
const geometry = await page.evaluate(() => {
|
|
const signal = document.getElementById('cockpit-signal-stream');
|
|
const utility = document.getElementById('cockpit-utility-controls');
|
|
const expanded = utility.querySelector('.cockpit-utility-control.is-expanded');
|
|
const s = signal.getBoundingClientRect();
|
|
const u = expanded.getBoundingClientRect();
|
|
const toggle = document.getElementById('cockpit-signal-toggle');
|
|
const t = toggle.getBoundingClientRect();
|
|
const hit = document.elementFromPoint(t.x + t.width / 2, t.y + t.height / 2);
|
|
return { height: innerHeight, theme: document.documentElement.dataset.uiTheme,
|
|
collapsed: signal.dataset.collapsed, signal: s.toJSON(), utility: u.toJSON(),
|
|
reachable: Boolean(hit && toggle.contains(hit)),
|
|
contained: u.bottom <= s.top - 7 && s.bottom < innerHeight,
|
|
};
|
|
});
|
|
check(`Cyber collapsed Live Signals remains visible and reachable after resize to ${height}px`,
|
|
geometry.theme === 'cyber' && geometry.collapsed === 'true'
|
|
&& geometry.signal.width > 0 && geometry.signal.height > 0
|
|
&& geometry.reachable && geometry.contained, JSON.stringify(geometry));
|
|
if (height === 640 || height === 900) {
|
|
await page.screenshot({ path: path.join(shotsDir, `cyber-resize-${height}.png`) });
|
|
}
|
|
}
|
|
await page.evaluate(() => window.__godsEyeView.styleManager._setHudVariant('operator'));
|
|
|
|
const signalTransition = await page.evaluate(() => {
|
|
const manager = window.__godsEyeView.styleManager;
|
|
const cockpit = manager.cockpitView;
|
|
const signalToggle = document.getElementById('cockpit-signal-toggle');
|
|
let expandedEvents = 0;
|
|
const onExpanded = () => { expandedEvents += 1; };
|
|
cockpit.signalUserCollapsed = false;
|
|
manager._setCockpitDisclosure('display', false);
|
|
manager._setCockpitDisclosure('radio', false);
|
|
cockpit.setSignalCollapsed(true);
|
|
window.addEventListener('gev:cockpit-signal-expanded', onExpanded);
|
|
try {
|
|
cockpit.setSignalCollapsed(false);
|
|
const afterExpansion = expandedEvents;
|
|
cockpit.setSignalCollapsed(false);
|
|
const afterRepeatedExpansion = expandedEvents;
|
|
|
|
manager._setCockpitDisclosure('display', true);
|
|
const displayCollapsedSignals = cockpit.signalCollapsed;
|
|
manager._setCockpitDisclosure('display', false);
|
|
const afterUtilityClose = expandedEvents;
|
|
const utilityCloseReopenedSignals = !cockpit.signalCollapsed;
|
|
|
|
cockpit.setSignalCollapsed(true, { user: true });
|
|
manager._setCockpitDisclosure('radio', true);
|
|
manager._setCockpitDisclosure('radio', false);
|
|
const manualCollapseRetained = cockpit.signalCollapsed && cockpit.signalUserCollapsed;
|
|
return {
|
|
afterExpansion,
|
|
afterRepeatedExpansion,
|
|
afterUtilityClose,
|
|
displayCollapsedSignals,
|
|
utilityCloseReopenedSignals,
|
|
manualCollapseRetained,
|
|
signalAriaExpanded: signalToggle?.getAttribute('aria-expanded'),
|
|
};
|
|
} finally {
|
|
window.removeEventListener('gev:cockpit-signal-expanded', onExpanded);
|
|
cockpit.signalUserCollapsed = false;
|
|
cockpit.setSignalCollapsed(false);
|
|
}
|
|
});
|
|
check(
|
|
'Live Signals expansion is edge-triggered and manual collapse remains authoritative',
|
|
signalTransition.afterExpansion === 1
|
|
&& signalTransition.afterRepeatedExpansion === 1
|
|
&& signalTransition.afterUtilityClose === 2
|
|
&& signalTransition.displayCollapsedSignals
|
|
&& signalTransition.utilityCloseReopenedSignals
|
|
&& signalTransition.manualCollapseRetained
|
|
&& signalTransition.signalAriaExpanded === 'false',
|
|
JSON.stringify(signalTransition),
|
|
);
|
|
const contextTransition = await page.evaluate(() => {
|
|
const cockpit = window.__godsEyeView.styleManager.cockpitView;
|
|
const priorCollapsed = cockpit.contextCollapsed;
|
|
const contextToggle = document.getElementById('cockpit-context-toggle');
|
|
let expandedEvents = 0;
|
|
const onExpanded = () => { expandedEvents += 1; };
|
|
cockpit.setContextCollapsed(true);
|
|
window.addEventListener('gev:cockpit-context-expanded', onExpanded);
|
|
try {
|
|
cockpit.setContextCollapsed(false);
|
|
const afterExpansion = expandedEvents;
|
|
cockpit.setContextCollapsed(false);
|
|
return {
|
|
afterExpansion,
|
|
afterRepeatedExpansion: expandedEvents,
|
|
ariaExpanded: contextToggle?.getAttribute('aria-expanded'),
|
|
label: contextToggle?.getAttribute('aria-label'),
|
|
};
|
|
} finally {
|
|
window.removeEventListener('gev:cockpit-context-expanded', onExpanded);
|
|
cockpit.setContextCollapsed(priorCollapsed);
|
|
}
|
|
});
|
|
check(
|
|
'Contact expansion is edge-triggered and retains its disclosure semantics',
|
|
contextTransition.afterExpansion === 1
|
|
&& contextTransition.afterRepeatedExpansion === 1
|
|
&& contextTransition.ariaExpanded === 'true'
|
|
&& contextTransition.label === 'Collapse Contact panel',
|
|
JSON.stringify(contextTransition),
|
|
);
|
|
// Use the real connected DOM to catch focus loss caused by moving live rows.
|
|
const signalFocus = await page.evaluate(() => {
|
|
const cockpit = window.__godsEyeView.styleManager.cockpitView;
|
|
const originalItems = cockpit.signalItems;
|
|
const wasCollapsed = cockpit.signalCollapsed;
|
|
cockpit.setSignalCollapsed(false);
|
|
const item = (id) => ({ key: `qa-${id}`, tone: 'info', timestamp: Date.now(),
|
|
title: `QA ${id}`, detail: 'Keyboard refresh fixture', target: { layerId: 'flights', id } });
|
|
try {
|
|
cockpit.signalItems = [item('focus-a'), item('focus-b')];
|
|
cockpit.renderCockpitSignals();
|
|
const button = cockpit.signalList.querySelector('[data-signal-id="focus-a"]');
|
|
button.focus();
|
|
const acquired = document.activeElement === button;
|
|
cockpit.signalItems = [item('focus-c'), item('focus-b'), item('focus-a')];
|
|
cockpit.renderCockpitSignals();
|
|
const retained = document.activeElement === button && button.isConnected;
|
|
const order = [...cockpit.signalList.querySelectorAll('[data-signal-id]')].map((node) => node.dataset.signalId);
|
|
cockpit.signalItems = [item('focus-b')];
|
|
cockpit.renderCockpitSignals();
|
|
const continuation = cockpit.briefTabs[cockpit.briefPageIndex] || cockpit.signalToggle;
|
|
const continued = document.activeElement === continuation;
|
|
cockpit.signalItems = [item('focus-a'), item('focus-b')];
|
|
cockpit.renderCockpitSignals();
|
|
return { acquired, retained, order, continued, notReclaimed: document.activeElement === continuation };
|
|
} finally {
|
|
cockpit.signalItems = originalItems;
|
|
cockpit.renderCockpitSignals();
|
|
cockpit.setSignalCollapsed(wasCollapsed);
|
|
}
|
|
});
|
|
check('Cockpit live reorder retains the same connected focus owner',
|
|
signalFocus.acquired && signalFocus.retained
|
|
&& JSON.stringify(signalFocus.order) === JSON.stringify(['focus-c', 'focus-b', 'focus-a']),
|
|
JSON.stringify(signalFocus));
|
|
check('a departed Cockpit contact continues at the footer without later reclaiming focus',
|
|
signalFocus.continued && signalFocus.notReclaimed, JSON.stringify(signalFocus));
|
|
|
|
// Real keyboard events also verify the nested lightbox does not exit Cockpit.
|
|
await page.focus('#cesium-credits .cesium-credit-expand-link');
|
|
await page.keyboard.press('Enter');
|
|
check('keyboard attribution opening focuses Close inside Cockpit', await page.evaluate(() => (
|
|
document.activeElement?.classList.contains('cesium-credit-lightbox-close')
|
|
&& document.querySelector('.cesium-credit-expand-link').getAttribute('aria-expanded') === 'true'
|
|
)));
|
|
await page.screenshot({ path: path.join(shotsDir, 'keyboard-attribution.png') });
|
|
await page.keyboard.press('Escape');
|
|
check('attribution Escape restores focus and keeps Cockpit active', await page.evaluate(() => (
|
|
document.activeElement?.classList.contains('cesium-credit-expand-link')
|
|
&& document.activeElement.getAttribute('aria-expanded') === 'false'
|
|
&& window.__godsEyeView.styleManager.cockpitView.active
|
|
)));
|
|
await page.screenshot({ path: path.join(shotsDir, 'restored-desktop.png') });
|
|
check(
|
|
'restored screenshot remains in a real Cockpit session',
|
|
await page.evaluate(() => document.body.classList.contains('cockpit-mode')
|
|
&& window.__godsEyeView.styleManager.cockpitView.active
|
|
&& getComputedStyle(document.getElementById('cockpit-utility-controls')).display !== 'none'),
|
|
);
|
|
const desktopViewActions = await page.evaluate(() => {
|
|
const reset = document.getElementById('cockpit-reset-globe')?.getBoundingClientRect();
|
|
const exit = document.getElementById('map-view-switch')?.getBoundingClientRect();
|
|
return {
|
|
reset: reset?.toJSON() || null,
|
|
exit: exit?.toJSON() || null,
|
|
viewport: { width: innerWidth, height: innerHeight },
|
|
};
|
|
});
|
|
check(
|
|
'desktop Cockpit Reset and Exit remain readable without overlap',
|
|
desktopViewActions.reset?.width > 44
|
|
&& desktopViewActions.exit?.width > 70
|
|
&& desktopViewActions.reset.right <= desktopViewActions.exit.left
|
|
&& desktopViewActions.reset.left >= 0
|
|
&& desktopViewActions.exit.right <= desktopViewActions.viewport.width,
|
|
JSON.stringify(desktopViewActions),
|
|
);
|
|
|
|
await page.setViewport({ width: 390, height: 844, deviceScaleFactor: 1 });
|
|
const mobile = await page.evaluate(async () => {
|
|
const manager = window.__godsEyeView.styleManager;
|
|
const hud = document.getElementById('cockpit-hud');
|
|
const signal = document.getElementById('cockpit-signal-stream');
|
|
const display = document.getElementById('cockpit-display-toggle-btn');
|
|
const radio = document.getElementById('cockpit-radio-toggle-btn');
|
|
const displayControl = display.closest('.cockpit-utility-control');
|
|
const radioControl = radio.closest('.cockpit-utility-control');
|
|
document.body.classList.add('cockpit-mode');
|
|
manager.cockpitView.active = true;
|
|
hud.hidden = false;
|
|
signal.hidden = false;
|
|
manager._setCockpitDisclosure('display', false);
|
|
manager._setCockpitDisclosure('radio', false);
|
|
manager.cockpitView.syncSignalLayout();
|
|
const collapsed = {
|
|
display: displayControl.getBoundingClientRect().toJSON(),
|
|
radio: radioControl.getBoundingClientRect().toJSON(),
|
|
};
|
|
display.click();
|
|
manager.cockpitView.syncSignalLayout();
|
|
return {
|
|
collapsed,
|
|
expandedDisplay: displayControl.getBoundingClientRect().toJSON(),
|
|
radioDisplay: getComputedStyle(radioControl).display,
|
|
radioAriaHidden: radioControl.getAttribute('aria-hidden'),
|
|
};
|
|
});
|
|
check(
|
|
'mobile keeps both collapsed launchers and the existing fixed expanded behavior',
|
|
mobile.collapsed.display.width > 0
|
|
&& mobile.collapsed.radio.width > 0
|
|
&& mobile.expandedDisplay.width > 0
|
|
&& mobile.expandedDisplay.width <= 366
|
|
&& mobile.radioDisplay === 'none'
|
|
&& mobile.radioAriaHidden === 'true',
|
|
JSON.stringify(mobile),
|
|
);
|
|
await page.screenshot({ path: path.join(shotsDir, 'mobile.png') });
|
|
check(
|
|
'mobile screenshot remains in a real Cockpit session',
|
|
await page.evaluate(() => document.body.classList.contains('cockpit-mode')
|
|
&& window.__godsEyeView.styleManager.cockpitView.active
|
|
&& getComputedStyle(document.getElementById('cockpit-utility-controls')).display !== 'none'),
|
|
);
|
|
const narrowViewActions = await page.evaluate(() => {
|
|
const reset = document.getElementById('cockpit-reset-globe')?.getBoundingClientRect();
|
|
const exit = document.getElementById('map-view-switch')?.getBoundingClientRect();
|
|
return {
|
|
reset: reset?.toJSON() || null,
|
|
exit: exit?.toJSON() || null,
|
|
viewport: { width: innerWidth, height: innerHeight },
|
|
};
|
|
});
|
|
check(
|
|
'narrow Cockpit Reset and Exit remain readable without overlap',
|
|
narrowViewActions.reset?.width > 44
|
|
&& narrowViewActions.exit?.width > 70
|
|
&& narrowViewActions.reset.right <= narrowViewActions.exit.left
|
|
&& narrowViewActions.reset.left >= 0
|
|
&& narrowViewActions.exit.right <= narrowViewActions.viewport.width,
|
|
JSON.stringify(narrowViewActions),
|
|
);
|
|
|
|
const resetResult = await page.evaluate(() => {
|
|
// Both the UI and compatibility facade delegate to this navigation owner.
|
|
const manager = window.__godsEyeView.styleManager._locationNavigation;
|
|
const awareness = window.__godsEyeView.dataManager.layers.get('military-awareness')?.module;
|
|
const styleManager = window.__godsEyeView.styleManager;
|
|
const styleBefore = styleManager.activeStyle;
|
|
// A Cockpit-only choice that Reset must undo.
|
|
styleManager.cockpitView.setVisionMode('anime');
|
|
window.__qaCockpitReset = {
|
|
calls: 0,
|
|
original: manager.resetToGlobeView,
|
|
subjectId: awareness?.getContextSnapshot?.()?.subject?.id || null,
|
|
styleBefore,
|
|
animeDuringCockpit: styleManager.stages?.anime?.uniforms?.intensity,
|
|
};
|
|
manager.resetToGlobeView = function qaCountedCockpitReset(...args) {
|
|
window.__qaCockpitReset.calls += 1;
|
|
return window.__qaCockpitReset.original.apply(this, args);
|
|
};
|
|
return Boolean(document.getElementById('cockpit-reset-globe'));
|
|
});
|
|
check('Cockpit Reset is present before keyboard activation', resetResult);
|
|
await page.focus('#cockpit-reset-globe');
|
|
await page.keyboard.press('Enter');
|
|
await page.waitForFunction(
|
|
() => !window.__godsEyeView.styleManager.cockpitView.active,
|
|
{ timeout: 6_000 },
|
|
);
|
|
await page.waitForFunction(() => {
|
|
const viewer = window.__godsEyeView.viewer;
|
|
if (!viewer) return false;
|
|
const height = viewer.camera.positionCartographic?.height;
|
|
return Math.abs(height - 18_000_000) < 150_000;
|
|
}, { timeout: 6_000 });
|
|
const resetState = await page.evaluate(() => {
|
|
const gev = window.__godsEyeView;
|
|
const qa = window.__qaCockpitReset;
|
|
const awareness = gev.dataManager.layers.get('military-awareness')?.module;
|
|
const subjectId = awareness?.getContextSnapshot?.()?.subject?.id || null;
|
|
const height = gev.viewer.camera.positionCartographic?.height;
|
|
gev.styleManager._locationNavigation.resetToGlobeView = qa.original;
|
|
delete window.__qaCockpitReset;
|
|
return {
|
|
calls: qa.calls,
|
|
cockpitActive: gev.styleManager.cockpitView.active,
|
|
trackedEntity: Boolean(gev.viewer.trackedEntity),
|
|
resetHidden: document.getElementById('cockpit-reset-globe')?.hidden,
|
|
height,
|
|
subjectPreserved: Boolean(qa.subjectId && subjectId === qa.subjectId),
|
|
styleRestored: gev.styleManager.activeStyle === qa.styleBefore,
|
|
animeDuringCockpit: qa.animeDuringCockpit,
|
|
// Cockpit-time intensities are Cockpit's own, so check against the map preset.
|
|
visionRestored: Object.entries(gev.styleManager.stages || {}).every(([name, stage]) =>
|
|
name === gev.styleManager.activeStyle
|
|
? stage.uniforms?.intensity > 0
|
|
: stage.uniforms?.intensity === 0),
|
|
};
|
|
});
|
|
check(
|
|
'keyboard Cockpit Reset uses one canonical route, preserves Contact selection and restores the map style',
|
|
resetState.calls === 1
|
|
&& !resetState.cockpitActive
|
|
&& !resetState.trackedEntity
|
|
&& resetState.resetHidden
|
|
&& Math.abs(resetState.height - 18_000_000) < 150_000
|
|
&& resetState.subjectPreserved
|
|
&& resetState.animeDuringCockpit > 0
|
|
&& resetState.styleRestored
|
|
&& resetState.visionRestored,
|
|
JSON.stringify(resetState),
|
|
);
|
|
check('runtime console remains clean', consoleErrors.length === 0 && localHttpErrors.length === 0,
|
|
[...localHttpErrors, ...consoleErrors].slice(0, 6).join(' | '));
|
|
} finally {
|
|
await page.evaluate(() => {
|
|
const manager = window.__godsEyeView?.styleManager;
|
|
const prior = window.__qaCockpitUtilityPrior;
|
|
if (!manager || !prior) return;
|
|
manager._setCockpitDisclosure('display', false);
|
|
manager._setCockpitDisclosure('radio', false);
|
|
if (manager.cockpitView.active && !prior.active) manager.cockpitView.exit();
|
|
else manager.cockpitView.active = prior.active;
|
|
const hud = document.getElementById('cockpit-hud');
|
|
const signal = document.getElementById('cockpit-signal-stream');
|
|
hud.hidden = prior.hudHidden;
|
|
signal.hidden = prior.signalHidden;
|
|
manager._setHudVariant(prior.hudVariant);
|
|
manager.hud.setMode(prior.hudVisible ? 'on' : 'off');
|
|
if (prior.utilityTop) hud.style.setProperty('--cockpit-utility-top', prior.utilityTop);
|
|
else hud.style.removeProperty('--cockpit-utility-top');
|
|
if (!prior.cockpit) document.body.classList.remove('cockpit-mode');
|
|
}).catch(() => {});
|
|
await browser.close();
|
|
}
|
|
|
|
console.log(`RESULT: ${failures.length ? 'NOT_READY' : 'READY'} (${failures.length} failures)`);
|
|
process.exitCode = failures.length ? 1 : 0;
|