2081 lines
93 KiB
JavaScript
2081 lines
93 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,
|
|
(() => { try { return puppeteer.executablePath(); } catch { return 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 } : {}),
|
|
args: ['--use-angle=metal', '--enable-gpu', '--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/opensky-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);
|
|
page.on('request', (request) => {
|
|
const url = new URL(request.url());
|
|
if (url.origin === new URL(appUrl).origin && url.pathname === '/api/ais-live') {
|
|
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/adsblol/mil') {
|
|
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/opensky-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();
|
|
});
|
|
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._contextMode) await styleManager._selectContextMode(null);
|
|
const contactsResult = await styleManager._selectContextMode('flights');
|
|
const missionResult = await styleManager._selectContextMode('space-missions');
|
|
const switched = {
|
|
contactsResult,
|
|
missionResult,
|
|
mode: styleManager._contextMode,
|
|
missionsEnabled: dataManager.isEnabled('rocket-launches'),
|
|
toasts: [...toasts],
|
|
};
|
|
const exitResult = await styleManager._selectContextMode(null);
|
|
return {
|
|
...switched,
|
|
exitResult,
|
|
modeAfterExit: styleManager._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._contextModeEntering === null
|
|
&& styleManager._contextSessionSnapshot === null
|
|
&& dataManager.isEnabled(siblingId)
|
|
&& !dataManager.isEffectivelyEnabled('rocket-launches')
|
|
) break;
|
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
}
|
|
timedOut = !(
|
|
styleManager._contextModeEntering === null
|
|
&& styleManager._contextSessionSnapshot === null
|
|
&& dataManager.isEnabled(siblingId)
|
|
&& !dataManager.isEffectivelyEnabled('rocket-launches')
|
|
);
|
|
return {
|
|
exercised: true,
|
|
transitionResult,
|
|
timedOut,
|
|
entering: styleManager._contextModeEntering,
|
|
snapshotRetained: Boolean(styleManager._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._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._contextMode,
|
|
entering: styleManager._contextModeEntering,
|
|
snapshotRetained: Boolean(styleManager._contextSessionSnapshot),
|
|
siblingIsolated: !dataManager.isEffectivelyEnabled(siblingId),
|
|
missionEnabled: dataManager.isEnabled('rocket-launches'),
|
|
missionEffective: dataManager.isEffectivelyEnabled('rocket-launches'),
|
|
toasts: [...toasts],
|
|
};
|
|
const exitResult = await styleManager._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._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._contextMode,
|
|
entering: styleManager._contextModeEntering,
|
|
snapshotRetained: Boolean(styleManager._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._contextMode,
|
|
entering: styleManager._contextModeEntering,
|
|
snapshotRetained: Boolean(styleManager._contextSessionSnapshot),
|
|
siblingIsolated: !dataManager.isEffectivelyEnabled(siblingId),
|
|
};
|
|
const snapshotBeforeOff = [...(styleManager._contextSessionSnapshot?.enabledLayerIds || [])];
|
|
const voiceOff = await dataManager.setEnabled('rocket-launches', false, { origin: 'voice' });
|
|
const reactionCountAfterOff = styleManager._contextLayerReactionPromises.size;
|
|
await styleManager._waitForContextLayerSettlement();
|
|
const restoredSiblingEntry = dataManager.layers.get(siblingId);
|
|
const voiceExit = {
|
|
voiceOff,
|
|
snapshotBeforeOff,
|
|
reactionCountAfterOff,
|
|
mode: styleManager._contextMode,
|
|
entering: styleManager._contextModeEntering,
|
|
snapshotRetained: Boolean(styleManager._contextSessionSnapshot),
|
|
siblingRestored: dataManager.isEffectivelyEnabled(siblingId),
|
|
siblingEnabled: restoredSiblingEntry?.enabled,
|
|
siblingLifecycle: restoredSiblingEntry?.lifecycleState,
|
|
restoreActive: Boolean(styleManager._contextRestoreState),
|
|
};
|
|
|
|
await dataManager.setEnabled('rocket-launches', true, { origin: 'programmatic' });
|
|
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
const internalState = {
|
|
mode: styleManager._contextMode,
|
|
entering: styleManager._contextModeEntering,
|
|
snapshotRetained: Boolean(styleManager._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._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._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._contextSessionSnapshot) await styleManager._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.
|
|
const deferredInstallationReadiness = await page.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._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._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._contextMode !== 'flights') {
|
|
await settleWithin(styleManager._selectContextMode('flights'), 15_000);
|
|
}
|
|
}
|
|
});
|
|
// 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);
|
|
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)
|
|
&& /unavailable|stale/i.test(String(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((r) => /unavailable|stale/i.test(String(r))),
|
|
JSON.stringify({
|
|
settledLifecycle: deferredInstallationReadiness.settledLifecycle,
|
|
fetchingCounts: deferredInstallationReadiness.fetchingCounts,
|
|
}),
|
|
);
|
|
check(
|
|
'the mapped-installations count appears once the first fetch actually answers',
|
|
deferredInstallationReadiness.exercised
|
|
&& deferredInstallationReadiness.answered?.settled
|
|
&& readsNumber(deferredInstallationReadiness.settledCount)
|
|
&& !/unavailable|stale/i.test(String(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._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;
|
|
await new Promise((resolve) => setTimeout(resolve, 180));
|
|
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._selectContextMode(null);
|
|
await settle(400);
|
|
const cleanupSnapshotCleared = styleManager._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._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._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._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._contextMode,
|
|
contextChanging: styleManager._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 listenersBeforeExit = viewer.scene.preUpdate.numberOfListeners;
|
|
const exited = styleManager.cockpitView.exit() === true;
|
|
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
const listenersAfterExit = viewer.scene.preUpdate.numberOfListeners;
|
|
const refocused = awareness?.focusCurrent?.() === true;
|
|
await new Promise((resolve) => setTimeout(resolve, 80));
|
|
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 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,
|
|
});
|
|
manager._setCockpitDisclosure('display', false);
|
|
manager._setCockpitDisclosure('radio', false);
|
|
cockpit.setVisionMode('optical');
|
|
current.focus({ preventScroll: true });
|
|
const states = [read()];
|
|
for (let index = 0; index < 5; index += 1) {
|
|
current.click();
|
|
states.push(read());
|
|
}
|
|
const originalStyle = manager.activeStyle;
|
|
manager.setStyle('noir');
|
|
await new Promise((resolve) => setTimeout(resolve, 900));
|
|
cockpit.setVisionMode('optical');
|
|
const inheritedNoir = read();
|
|
cockpit.setVisionMode('noir');
|
|
const explicitNoir = read();
|
|
cockpit.setVisionMode('optical');
|
|
const restoredNoir = {
|
|
...read(),
|
|
activeStyle: manager.activeStyle,
|
|
intensity: manager.stages.noir?.uniforms?.intensity,
|
|
};
|
|
manager.setStyle(originalStyle);
|
|
await new Promise((resolve) => setTimeout(resolve, 900));
|
|
cockpit.setVisionMode('optical');
|
|
return { states, inheritedNoir, explicitNoir, restoredNoir };
|
|
});
|
|
check(
|
|
'Cockpit vision control exposes inherited, CRT, NVG, FLIR, and NOIR before wrapping',
|
|
JSON.stringify(visionCycle.states.map((state) => state.mode))
|
|
=== JSON.stringify(['optical', 'crt', 'nvg', 'thermal', 'noir', 'optical'])
|
|
&& visionCycle.states[4]?.label === 'NOIR'
|
|
&& visionCycle.states[4]?.aria === 'Current cockpit vision style: Noir. Activate for next style.',
|
|
JSON.stringify(visionCycle),
|
|
);
|
|
check(
|
|
'user vision changes open Display for every temporary treatment and retain selector focus',
|
|
visionCycle.states[1]?.mode === 'crt'
|
|
&& visionCycle.states.slice(1, 5).every((state) => state.displayOpen === 'true')
|
|
&& visionCycle.states.slice(1, 5).every((state) => state.radioOpen === 'false')
|
|
&& visionCycle.states.slice(1, 5).every((state) => state.parametersActive)
|
|
&& visionCycle.states.slice(1, 5).every((state) => !state.parametersCollapsed)
|
|
&& visionCycle.states.slice(1, 5).every((state) => state.signalCollapsed)
|
|
&& visionCycle.states.every((state) => state.focusRetained),
|
|
JSON.stringify(visionCycle.states),
|
|
);
|
|
check(
|
|
'inherited Noir and explicit Noir keep distinct ownership and restore the Noir map preset',
|
|
visionCycle.inheritedNoir?.mode === 'optical'
|
|
&& visionCycle.inheritedNoir?.label === 'NOIR'
|
|
&& visionCycle.explicitNoir?.mode === 'noir'
|
|
&& visionCycle.explicitNoir?.label === 'NOIR'
|
|
&& visionCycle.restoredNoir?.mode === 'optical'
|
|
&& visionCycle.restoredNoir?.activeStyle === 'noir'
|
|
&& visionCycle.restoredNoir?.intensity === 1,
|
|
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 },
|
|
}),
|
|
);
|
|
|
|
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),
|
|
);
|
|
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(() => {
|
|
const manager = window.__godsEyeView.styleManager;
|
|
const awareness = window.__godsEyeView.dataManager.layers.get('military-awareness')?.module;
|
|
window.__qaCockpitReset = {
|
|
calls: 0,
|
|
original: manager.resetToGlobeView,
|
|
subjectId: awareness?.getContextSnapshot?.()?.subject?.id || null,
|
|
};
|
|
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.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),
|
|
};
|
|
});
|
|
check(
|
|
'keyboard Cockpit Reset uses one canonical route and preserves Contact selection',
|
|
resetState.calls === 1
|
|
&& !resetState.cockpitActive
|
|
&& !resetState.trackedEntity
|
|
&& resetState.resetHidden
|
|
&& Math.abs(resetState.height - 18_000_000) < 150_000
|
|
&& resetState.subjectPreserved,
|
|
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;
|