diff --git a/scripts/qa-cockpit-utility.mjs b/scripts/qa-cockpit-utility.mjs index 607889bbe..c28fabfe6 100644 --- a/scripts/qa-cockpit-utility.mjs +++ b/scripts/qa-cockpit-utility.mjs @@ -1572,6 +1572,10 @@ try { const cockpit = manager.cockpitView; const current = document.getElementById('cockpit-vision-current'); const label = document.getElementById('cockpit-vision-current-label'); + const settle = (ms = 900) => new Promise((resolve) => setTimeout(resolve, ms)); + const intensities = () => Object.fromEntries( + Object.entries(manager.stages || {}).map(([name, stage]) => [name, stage.uniforms?.intensity]), + ); const read = () => ({ mode: current?.dataset.cockpitVision, label: label?.textContent?.trim(), @@ -1582,62 +1586,106 @@ try { parametersCollapsed: document.getElementById('param-slider-panel')?.classList.contains('collapsed'), signalCollapsed: cockpit.signalCollapsed, focusRetained: document.activeElement === current, + activeStyle: manager.activeStyle, + intensities: intensities(), }); + const reenter = async () => { + cockpit.exit({ restoreTracking: true }); + await settle(); + for (let attempt = 0; attempt < 40; attempt += 1) { + if (cockpit.enter() === true) return true; + await settle(200); + } + return false; + }; manager._setCockpitDisclosure('display', false); manager._setCockpitDisclosure('radio', false); cockpit.setVisionMode('optical'); current.focus({ preventScroll: true }); const states = [read()]; - for (let index = 0; index < 5; index += 1) { + for (let index = 0; index < 7; index += 1) { current.click(); states.push(read()); } const originalStyle = manager.activeStyle; + // A Noir map preset: Cockpit enters on Noir, Normal clears every shader + // without changing the map preset, and Exit Cockpit restores Noir. + cockpit.exit({ restoreTracking: true }); manager.setStyle('noir'); - await new Promise((resolve) => setTimeout(resolve, 900)); + await settle(); + const enteredOnNoir = await reenter(); + const entryNoir = read(); 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, - }; + const normalInsideNoir = read(); + cockpit.setVisionMode('crt'); + const cockpitOnlyCrt = read(); + const exitedNoir = cockpit.exit({ restoreTracking: true }) === true; + await settle(); + const restoredNoir = { ...read(), cockpitActive: cockpit.active }; manager.setStyle(originalStyle); - await new Promise((resolve) => setTimeout(resolve, 900)); + await settle(); + const reentered = await reenter(); cockpit.setVisionMode('optical'); - return { states, inheritedNoir, explicitNoir, restoredNoir }; + return { + states, + enteredOnNoir, + entryNoir, + normalInsideNoir, + cockpitOnlyCrt, + exitedNoir, + restoredNoir, + reentered, + }; }); + const cycleModes = ['optical', 'crt', 'nvg', 'thermal', 'anime', 'noir', 'snow', 'optical']; + const cycleLabels = ['NORMAL', 'CRT', 'NVG', 'FLIR', 'ANIME', 'NOIR', 'SNOW', 'NORMAL']; 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), + 'Cockpit vision control exposes Normal, CRT, NVG, FLIR, Anime, Noir and Snow before wrapping', + JSON.stringify(visionCycle.states.map((state) => state.mode)) === JSON.stringify(cycleModes) + && JSON.stringify(visionCycle.states.map((state) => state.label)) === JSON.stringify(cycleLabels) + && visionCycle.states[5]?.aria === 'Current cockpit vision style: Noir. Activate for next style.' + && visionCycle.states[7]?.aria === 'Current cockpit vision style: Normal. Activate for next style.', + JSON.stringify(visionCycle.states.map(({ mode, label, aria }) => ({ mode, label, aria }))), ); check( 'user vision changes open Display for every temporary treatment and retain selector focus', - visionCycle.states[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.slice(1, 7).every((state) => state.displayOpen === 'true') + && visionCycle.states.slice(1, 7).every((state) => state.radioOpen === 'false') + && visionCycle.states.slice(1, 7).every((state) => state.parametersActive) + && visionCycle.states.slice(1, 7).every((state) => !state.parametersCollapsed) + && visionCycle.states.slice(1, 7).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' + 'each Cockpit vision mode drives only its own shader and Normal clears them all', + visionCycle.states.slice(1, 7).every((state) => { + const target = { + crt: 'retro', nvg: 'surveillance', thermal: 'thermal', anime: 'anime', noir: 'noir', snow: 'snow', + }[state.mode]; + return Object.entries(state.intensities).every(([name, value]) => + name === target ? value > 0 : value === 0); + }) + && Object.values(visionCycle.states[7].intensities).every((value) => value === 0), + JSON.stringify(visionCycle.states.map(({ mode, intensities }) => ({ mode, intensities }))), + ); + check( + 'Cockpit enters on the Noir map preset, Normal clears it, and Exit Cockpit restores it', + visionCycle.enteredOnNoir + && visionCycle.entryNoir?.mode === 'noir' + && visionCycle.entryNoir?.label === 'NOIR' + && visionCycle.normalInsideNoir?.mode === 'optical' + && visionCycle.normalInsideNoir?.label === 'NORMAL' + && visionCycle.normalInsideNoir?.activeStyle === 'noir' + && Object.values(visionCycle.normalInsideNoir?.intensities || {}).every((value) => value === 0) + && visionCycle.cockpitOnlyCrt?.mode === 'crt' + && visionCycle.cockpitOnlyCrt?.activeStyle === 'noir' + && visionCycle.exitedNoir + && !visionCycle.restoredNoir?.cockpitActive && visionCycle.restoredNoir?.activeStyle === 'noir' - && visionCycle.restoredNoir?.intensity === 1, + && visionCycle.restoredNoir?.intensities?.noir === 1 + && visionCycle.restoredNoir?.intensities?.retro === 0 + && visionCycle.reentered, JSON.stringify(visionCycle), ); await page.evaluate(() => window.__godsEyeView.styleManager.cockpitView.setVisionMode('noir')); @@ -2266,10 +2314,16 @@ try { // Both the UI and compatibility facade delegate to this navigation owner. const manager = window.__godsEyeView.styleManager._locationNavigation; const awareness = window.__godsEyeView.dataManager.layers.get('military-awareness')?.module; + const styleManager = window.__godsEyeView.styleManager; + const styleBefore = styleManager.activeStyle; + // A Cockpit-only choice that Reset must undo. + styleManager.cockpitView.setVisionMode('snow'); window.__qaCockpitReset = { calls: 0, original: manager.resetToGlobeView, subjectId: awareness?.getContextSnapshot?.()?.subject?.id || null, + styleBefore, + snowDuringCockpit: styleManager.stages?.snow?.uniforms?.intensity, }; manager.resetToGlobeView = function qaCountedCockpitReset(...args) { window.__qaCockpitReset.calls += 1; @@ -2305,16 +2359,26 @@ try { resetHidden: document.getElementById('cockpit-reset-globe')?.hidden, height, subjectPreserved: Boolean(qa.subjectId && subjectId === qa.subjectId), + styleRestored: gev.styleManager.activeStyle === qa.styleBefore, + snowDuringCockpit: qa.snowDuringCockpit, + // Cockpit-time intensities are Cockpit's own, so check against the map preset. + visionRestored: Object.entries(gev.styleManager.stages || {}).every(([name, stage]) => + name === gev.styleManager.activeStyle + ? stage.uniforms?.intensity > 0 + : stage.uniforms?.intensity === 0), }; }); check( - 'keyboard Cockpit Reset uses one canonical route and preserves Contact selection', + 'keyboard Cockpit Reset uses one canonical route, preserves Contact selection and restores the map style', resetState.calls === 1 && !resetState.cockpitActive && !resetState.trackedEntity && resetState.resetHidden && Math.abs(resetState.height - 18_000_000) < 150_000 - && resetState.subjectPreserved, + && resetState.subjectPreserved + && resetState.snowDuringCockpit > 0 + && resetState.styleRestored + && resetState.visionRestored, JSON.stringify(resetState), ); check('runtime console remains clean', consoleErrors.length === 0 && localHttpErrors.length === 0,