import { getScopeMaskFeather, isScopeMaskEnabled, setScopeMaskEnabled, setScopeMaskFeather } from '../scopeMask.js'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import test from 'node:test'; import { applyHudUiTheme, CYBER_VISUAL_DEFAULTS, cyberVisualDefaultsForHudTransition, HUD_LAYOUTS, normalizeHudLayout, } from '../hudLayouts.js'; import { GEV_ACTION_SCHEMAS } from '../voice/actionSchemas.js'; import { createGevActionRunner } from '../voice/gevActions.js'; import { StyleManager } from './applicationShell.js'; import { bindDisplayControls } from './displayControls.js'; import { VisualSettings } from './visualSettings.js'; import { STYLES } from './visualPresets.js'; test('CCTV and Context keep their headers outside Cyber-only scroll bodies', () => { for (const file of ['panels/CctvPanel.vue', 'ContextView.vue']) { const markup = readFileSync( new URL(`./views/${file}`, import.meta.url), 'utf8', ); // The body wears the shared scrollbar skin where Cyber makes it scroll. assert.match( markup, /class="cyber-panel-body ui-scroll" data-rail-scroller/, ); assert.ok( markup.indexOf('class="panel-header"') < markup.indexOf('class="cyber-panel-body ui-scroll"'), ); } const shared = readFileSync( new URL('./styles/controls.css', import.meta.url), 'utf8', ); const cyber = readFileSync( new URL('./styles/cyber.css', import.meta.url), 'utf8', ); assert.match(shared, /\.cyber-panel-body\s*\{\s*display: contents;/); assert.match( cyber, /:root\[data-ui-theme='cyber'\] \.cyber-panel-body\s*\{[^}]*overflow-y: auto;[^}]*overscroll-behavior: contain;/, ); }); function styleOwner(t, initialVariant = 'cyber') { const previous = { document: globalThis.document, window: globalThis.window }; const root = { dataset: {} }; globalThis.document = { documentElement: root, querySelectorAll: () => [] }; globalThis.window = { dispatchEvent() {} }; t.after(() => Object.assign(globalThis, previous)); let variant = initialVariant; const calls = []; const hud = { getVariant: () => variant, setVariant: (value) => { // Normalize like the real HUD so retired layouts fall back. variant = normalizeHudLayout(value); applyHudUiTheme(root, variant); }, setMode: (value) => calls.push(['mode', value]), onStyleChange: (value) => calls.push(['style', value]), }; hud.setVariant(initialVariant); const owner = Object.create(VisualSettings.prototype); Object.assign(owner, { activeStyle: 'normal', store: { publish() {} }, readHud: () => hud, readShareLinks: () => ({ onStyleChange() {} }), _visualEffects: { stages: Object.fromEntries( Object.entries(STYLES).map(([name, config]) => [ name, { uniforms: { ...config.uniforms, intensity: 0 } }, ]), ), }, services: { governorRequestRender() {}, setDetectionStyle() {} }, _setBloomEnabled: (value) => calls.push(['bloom', value]), _setSharpenEnabled: (value) => calls.push(['sharpen', value]), _applyDetectionPreset: (value) => calls.push(['detection', value]), _syncShareState: () => calls.push(['share', variant]), }); for (const name of [ 'setCelestialRingEnabled', '_startTransition', '_publishActiveStyle', '_updateSliderPanel', '_updateHudButtonState', '_syncIrBoost', '_syncCockpitInheritedStyle', '_syncCyberSonarControl', '_scheduleAdaptivePanelLayout', '_setDetectionFade', // Intensities: the fixture has no post-process stages to drive. '_setBloomIntensity', '_setSharpenIntensity', ]) owner[name] = () => {}; return { owner, hud, root, calls }; } test('visual styles retain Cyber through real preset application without dropping other defaults', (t) => { const { owner, hud, root, calls } = styleOwner(t); for (const style of [ 'surveillance', 'normal', 'noir', 'anime', 'thermal', 'retro', ]) { owner.setStyle(style); assert.equal(owner.activeStyle, style); assert.equal(hud.getVariant(), 'cyber'); assert.equal(root.dataset.uiTheme, 'cyber'); assert.deepEqual(calls.at(-1), ['share', 'cyber']); } assert.equal(owner.stages.thermal.uniforms.sensitivity, 0.85); assert.equal(calls.filter(([name]) => name === 'detection').length, 3); assert.equal( calls.filter(([name, value]) => name === 'bloom' && value === false).length, 3, ); assert.equal( calls.filter(([name, value]) => name === 'sharpen' && value === true) .length, 3, ); }); test('non-Cyber style presets and explicit HUD choices remain authoritative', (t) => { const { owner, hud, root } = styleOwner(t); for (const variant of ['tactical', 'minimal']) { owner._setHudVariant(variant); assert.equal(hud.getVariant(), variant); assert.equal(root.dataset.uiTheme, undefined); owner.setStyle('normal'); owner.setStyle('surveillance'); assert.equal(hud.getVariant(), 'tactical'); } }); test('leaving Cyber without a preset choice restores the prior visual preset', (t) => { const { owner, hud } = styleOwner(t, 'tactical'); owner.setStyle('noir'); owner._setHudVariant('cyber', { applyVisualDefaults: true }); assert.equal(owner.activeStyle, 'thermal'); assert.equal(owner._preCyberStyle, 'noir'); assert.equal(owner._cyberStyleUserSelected, false); owner._setHudVariant('minimal', { applyVisualDefaults: true }); assert.equal(hud.getVariant(), 'minimal'); assert.equal(owner.activeStyle, 'noir'); assert.equal(owner._preCyberStyle, null); assert.equal(owner._cyberStyleUserSelected, false); }); test('a preset explicitly selected in Cyber survives exit and resets for re-entry', (t) => { const { owner, hud } = styleOwner(t, 'tactical'); owner.setStyle('noir'); owner._setHudVariant('cyber', { applyVisualDefaults: true }); owner.setStyle('anime', { userInitiated: true }); assert.equal(owner.activeStyle, 'anime'); assert.equal(owner._preCyberStyle, 'noir'); assert.equal(owner._cyberStyleUserSelected, true); // Reselecting the active mode must not erase the user's preset intent. owner._setHudVariant('cyber', { applyVisualDefaults: true }); assert.equal(owner._preCyberStyle, 'noir'); owner._setHudVariant('minimal', { applyVisualDefaults: true }); assert.equal(hud.getVariant(), 'minimal'); assert.equal(owner.activeStyle, 'anime'); assert.equal(owner._preCyberStyle, null); assert.equal(owner._cyberStyleUserSelected, false); // A later untouched Cyber round trip restores the new pre-Cyber preset. owner._setHudVariant('cyber', { applyVisualDefaults: true }); assert.equal(owner.activeStyle, 'thermal'); assert.equal(owner._preCyberStyle, 'anime'); owner._setHudVariant('minimal', { applyVisualDefaults: true }); assert.equal(owner.activeStyle, 'anime'); }); test('an explicit same-value FLIR choice is preserved on Cyber exit', (t) => { const { owner } = styleOwner(t, 'tactical'); owner.setStyle('noir'); owner._setHudVariant('cyber', { applyVisualDefaults: true }); owner.setStyle('thermal', { userInitiated: true }); assert.equal(owner.activeStyle, 'thermal'); assert.equal(owner._cyberStyleUserSelected, true); owner._setHudVariant('minimal', { applyVisualDefaults: true }); assert.equal(owner.activeStyle, 'thermal'); }); test('an authoritative same-Cyber reset clears local preset ownership', (t) => { const { owner } = styleOwner(t, 'tactical'); owner.setStyle('noir'); owner._setHudVariant('cyber', { applyVisualDefaults: true }); owner.setStyle('anime', { userInitiated: true }); assert.equal(owner._cyberStyleUserSelected, true); owner._setHudVariant('cyber'); assert.equal(owner.activeStyle, 'anime'); assert.equal(owner._preCyberStyle, null); assert.equal(owner._cyberStyleUserSelected, false); owner._setHudVariant('minimal', { applyVisualDefaults: true }); assert.equal(owner.activeStyle, 'anime'); }); test('Display and voice HUD routes preserve explicit Cyber entry and exit authority', async (t) => { const makeOwner = () => { const { owner, hud } = styleOwner(t, 'tactical'); owner.setStyle('noir'); owner.setHudLayout = (...args) => StyleManager.prototype.setHudLayout.call(owner, ...args); owner.getControlState = () => ({ hud: { layout: hud.getVariant(), visible: true }, }); return { owner, hud }; }; const display = makeOwner(); const hudLayout = new EventTarget(); hudLayout.value = 'cyber'; const animeButton = new EventTarget(); animeButton.dataset = { style: 'anime' }; const controls = bindDisplayControls({ elements: { hudLayout, styleButtons: [animeButton] }, actions: { setHudLayout: (value) => display.owner.setHudLayout(value), setStyle: (value) => display.owner.setStyle(value, { userInitiated: true }), }, }); hudLayout.dispatchEvent(new Event('change')); assert.equal(display.hud.getVariant(), 'cyber'); assert.equal(display.owner.activeStyle, 'thermal'); animeButton.dispatchEvent(new Event('click')); assert.equal(display.owner.activeStyle, 'anime'); hudLayout.value = 'minimal'; hudLayout.dispatchEvent(new Event('change')); assert.equal(display.hud.getVariant(), 'minimal'); assert.equal(display.owner.activeStyle, 'anime'); controls.destroy(); const voice = makeOwner(); const eventSource = { addEventListener() {}, removeEventListener() {} }; const viewer = { camera: { moveEnd: { addEventListener() {} } }, clock: { onTick: { addEventListener: () => () => {} } }, scene: { canvas: eventSource }, }; const runVoiceAction = createGevActionRunner({ viewer, styleManager: voice.owner, dataManager: {}, }); const entered = await runVoiceAction('set_hud', { layout: 'cyber' }); assert.equal(entered.ok, true); assert.equal(voice.hud.getVariant(), 'cyber'); assert.equal(voice.owner.activeStyle, 'thermal'); const selected = await runVoiceAction('set_visual_style', { style: 'anime' }); assert.equal(selected.ok, true); assert.equal(voice.owner.activeStyle, 'anime'); const exited = await runVoiceAction('set_hud', { layout: 'minimal' }); assert.equal(exited.ok, true); assert.equal(voice.hud.getVariant(), 'minimal'); assert.equal(voice.owner.activeStyle, 'anime'); }); test('scope choice and expanded feather survive every HUD layout and both restore paths', async (t) => { const { owner } = styleOwner(t, 'tactical'); const previous = { enabled: isScopeMaskEnabled(), feather: getScopeMaskFeather() }; t.after(() => { setScopeMaskEnabled(previous.enabled); setScopeMaskFeather(previous.feather); }); Object.assign(owner.services, { setScopeMaskEnabled, setScopeMaskFeather }); let shown = null; owner.store = { publish: (view) => (shown = view.scope) }; await owner.restoreShareState({ scopeEnabled: false, scopeFeatherPct: 200 }); assert.deepEqual(shown, { enabled: false, featherPct: 200 }); for (const layout of ['minimal', 'cyber', 'tactical']) { owner._setHudVariant(layout, { applyVisualDefaults: true }); assert.equal(isScopeMaskEnabled(), false, layout); assert.equal(getScopeMaskFeather(), 2, layout); assert.equal(shown.enabled, false, layout); } await owner.applyVisualState({ scope: { enabled: true, featherPct: 175 } }); assert.equal(isScopeMaskEnabled(), true); assert.equal(getScopeMaskFeather(), 1.75); assert.deepEqual(shown, { enabled: true, featherPct: 175 }); await owner.applyVisualState({ scope: { featherPct: 500 } }); assert.equal(getScopeMaskFeather(), 2); assert.deepEqual(shown, { enabled: true, featherPct: 200 }); }); test('a scene shot saved with the retired Snow style plays on Normal', async (t) => { const { owner } = styleOwner(t, 'tactical'); owner.setStyle('noir'); await owner.applyVisualState({ style: 'snow' }); assert.equal(owner.activeStyle, 'normal'); }); test('scene and share HUD transitions do not restore a local pre-Cyber preset', async (t) => { const { owner, hud } = styleOwner(t, 'tactical'); owner.setStyle('noir'); owner._setHudVariant('cyber', { applyVisualDefaults: true }); owner.setStyle('anime', { userInitiated: true }); assert.equal(owner._cyberStyleUserSelected, true); await owner.applyVisualState({ style: 'retro', hud: { variant: 'cyber' } }); assert.equal(hud.getVariant(), 'cyber'); assert.equal(owner.activeStyle, 'retro'); assert.equal(owner._preCyberStyle, null); assert.equal(owner._cyberStyleUserSelected, false); owner._setHudVariant('minimal', { applyVisualDefaults: true }); assert.equal(owner.activeStyle, 'retro'); owner.setStyle('surveillance'); owner._setHudVariant('cyber', { applyVisualDefaults: true }); assert.equal(owner._preCyberStyle, 'surveillance'); owner.setStyle('anime', { userInitiated: true }); await owner.restoreShareState({ style: 'thermal', hudVariant: 'cyber', }); assert.equal(hud.getVariant(), 'cyber'); assert.equal(owner.activeStyle, 'thermal'); assert.equal(owner._preCyberStyle, null); assert.equal(owner._cyberStyleUserSelected, false); owner._setHudVariant('tactical', { applyVisualDefaults: true }); assert.equal(owner.activeStyle, 'thermal'); assert.equal(owner._preCyberStyle, null); }); test('scene and share restoration override Cyber and preserve restored Cyber parameters', async (t) => { const { owner, hud } = styleOwner(t); await owner.applyVisualState({ style: 'retro', hud: { variant: 'minimal' } }); assert.equal(hud.getVariant(), 'minimal'); owner._setHudVariant('cyber'); await owner.restoreShareState({ style: 'surveillance', hudVariant: 'operator', }); // A link saved with the retired Operator layout restores as Tactical. assert.equal(hud.getVariant(), 'tactical'); await owner.restoreShareState({ style: 'thermal', hudVariant: 'cyber', styleParams: { palette: 0.73 }, }); assert.equal(hud.getVariant(), 'cyber'); assert.equal(owner.activeStyle, 'thermal'); assert.equal(owner.stages.thermal.uniforms.palette, 0.73); owner._setHudVariant('cyber', { applyVisualDefaults: true }); assert.equal(owner.stages.thermal.uniforms.palette, 0.73); owner._setHudVariant('tactical'); owner._setHudVariant('cyber', { applyVisualDefaults: true }); assert.equal(owner.stages.thermal.uniforms.palette, 0.42); }); const read = (relative) => readFileSync(new URL(relative, import.meta.url), 'utf8'); test('Cyber setup uses a compact toolbar icon and highlights the north-up letter', () => { const css = read('./styles/cyber.css'); assert.match( css, /#top-center-actions #key-setup-chip\s*\{[^}]*inset: auto;[^}]*width: 36px;/, ); assert.match(css, /#key-setup-chip\[hidden\]\s*\{\s*display: none;/); assert.match( css, /#north-up-view\[data-north-up='true'\]\s+\.camera-compass-n\s*\{[^}]*color: var\(--cyber-red-bright\)/, ); }); const cyberStyles = read('./styles/cyber.css'); // Source guards supplement rendered popup/hit-target checks. Inspect one rule // at a time so a safe declaration on a child cannot excuse a clipped ancestor. function cssRuleBody(styles, selector) { const match = styles.match(new RegExp(`${selector.source}\\s*\\{([^}]+)\\}`)); assert.ok(match, `Missing CSS rule: ${selector.source}`); return match[1]; } test('Cyber Voice clips its decoration without clipping descendant help and errors', () => { const owner = cssRuleBody( cyberStyles, /:root\[data-ui-theme='cyber'\] #command-dock > #gev-voice-control/, ); assert.match(owner, /isolation:\s*isolate;/); assert.match(owner, /overflow:\s*visible;/); assert.match(owner, /clip-path:\s*none;/); const decoration = cssRuleBody( cyberStyles, /:root\[data-ui-theme='cyber'\] #command-dock > #gev-voice-control::before/, ); assert.match(decoration, /clip-path:\s*polygon\(/); assert.match(decoration, /pointer-events:\s*none;/); assert.match(decoration, /inset:\s*0;/); assert.match(decoration, /z-index:\s*-1;/); }); test('Cyber dock pins escape the tray mask and its decoration overrides the legacy arrow geometry', () => { const owner = cssRuleBody( cyberStyles, /:root\[data-ui-theme='cyber'\] #command-dock \.dock-popover-content/, ); assert.match(owner, /clip-path:\s*none;/); assert.match(owner, /isolation:\s*isolate;/); // Including the tray owners beats #control-panel's more specific old // ::after arrow positioning. Otherwise left:auto can collapse the frame. const decoration = cssRuleBody( cyberStyles, /:root\[data-ui-theme='cyber'\]\s+#command-dock\s+:is\(#location-bar, #control-panel\)\s+\.dock-popover-content::after/, ); assert.match(decoration, /inset:\s*0;/); assert.match(decoration, /width:\s*auto;/); assert.match(decoration, /height:\s*auto;/); assert.match(decoration, /transform:\s*none;/); assert.match(decoration, /clip-path:\s*var\(--cyber-panel-frame\);/); assert.match(decoration, /pointer-events:\s*none;/); }); test('Cyber lower-left telemetry tracks the credit anchor with clearance for its logo row', () => { const telemetry = cssRuleBody( cyberStyles, /#intel-hud\[data-variant='cyber'\] \.hud-bottom-left/, ); const offset = telemetry.match(/bottom:\s*calc\(2vh \+ ([\d.]+)rem\);/); assert.ok(offset, 'telemetry must retain the attribution viewport anchor'); const credits = read('./styles/command-dock-trays.css'); const creditOffsets = [ ...credits.matchAll( /#cesium-credits\s*\{[^}]*bottom:\s*calc\(2vh \+ ([\d.]+)rem\);/g, ), ]; assert.ok(creditOffsets.length > 0); for (const credit of creditOffsets) { assert.ok( Number(offset[1]) - Number(credit[1]) >= 2.75, 'reserve the 28px logo row, baseline descent and 12px clearance at the 16px root size', ); } assert.doesNotMatch(cyberStyles, /\.hud-bottom-left\s*\{\s*bottom:\s*60px;/); }); test('shared panel hooks affect only opted-in rail surfaces without overriding disclosure and concealment', () => { const styles = read('./styles/panel-surfaces.css'); const rail = cssRuleBody( styles, /:is\(#left-panel-stack, #right-context-rail\)\s+> \[data-panel-id\]:has\(> \[data-panel-surface\]\)/, ); assert.match(rail, /pointer-events:\s*auto;/); assert.doesNotMatch(rail, /(?:display|visibility):/); assert.doesNotMatch( styles, /:is\(#left-panel-stack, #right-context-rail\)\s*> \[data-panel-id\]\s*\{/, ); assert.match( styles, /\[data-panel-surface\]:not\(\[hidden\]\):not\(dialog:not\(\[open\]\)\)\s*\{\s*display: flex;/, ); assert.match( styles, /dialog\[data-panel-surface\]:not\(\[open\]\)\s*\{\s*display: none !important;/, ); assert.match( styles, /body:is\(\.ui-clean-view, \.recording-mode\)[^{]+\{[^}]*visibility: hidden !important;[^}]*pointer-events: none !important;/, ); }); test('Cyber clipped expand controls retain an inset keyboard-focus outline in map and cockpit', () => { assert.match( cyberStyles, /:root\[data-ui-theme='cyber'\]\s+:is\(\s*\.panel-collapse-btn,\s*body\.cockpit-mode \.cockpit-utility-glyph\s*\):focus-visible\s*\{[^}]*outline: 2px solid var\(--cyber-red-bright\) !important;[^}]*outline-offset: -3px;[^}]*box-shadow: none;/, ); assert.match( cyberStyles, /:root\[data-ui-theme='cyber'\]\s+:is\(\.panel-collapse-btn, body\.cockpit-mode \.cockpit-utility-glyph\):is\(\s*:hover,\s*:focus-visible\s*\)\s*\{[^}]*border-color: var\(--cyber-red-bright\);/, ); }); test('Cyber Radio retains upstream nested Context ownership', () => { assert.doesNotMatch(cyberStyles, /#right-context-rail\s*>\s*#radio-panel/); }); test('Cyber cockpit launchers share Data Layers frame and expand-control treatment', () => { assert.match( cyberStyles, /:is\(\.panel-collapse-btn, body\.cockpit-mode \.cockpit-utility-glyph\)/, ); assert.match( cyberStyles, /body\.cockpit-mode \.cockpit-utility-launcher \{[^}]*width: var\(--cyber-panel-width\);[^}]*min-height: 60px;[^}]*clip-path: var\(--cyber-panel-frame\);/, ); assert.match( cyberStyles, /\.cockpit-utility-glyph::after \{\s*content: '\+';/, ); assert.match( cyberStyles, /\.cockpit-utility-glyph\[aria-expanded='true'\]::after \{\s*content: '−';/, ); assert.match( cyberStyles, /\.cockpit-utility-control\.is-expanded\s+\.cockpit-utility-launcher \{[^}]*width: 100%;[^}]*clip-path: none;/, ); }); test('Cyber side panels share one width and one framed surface material', () => { assert.match(cyberStyles, /--cyber-panel-width: 272px;/); assert.match(cyberStyles, /--cyber-right-panel-width: 312px;/); assert.match(cyberStyles, /--cyber-panel-collapsed-width: 200px;/); assert.match(cyberStyles, /--cyber-panel-control-size: 30px;/); assert.match( cyberStyles, /--cyber-panel-surface: rgba\(18, 24, 27, 0\.92\);/, ); assert.match( cyberStyles, /\.cockpit-utility-control\.is-expanded \{[\s\S]*?width: var\(--cyber-right-panel-width\);[\s\S]*?background-color: var\(--cyber-panel-surface\);[\s\S]*?background-image: var\(--cyber-panel-surface-detail\);[\s\S]*?clip-path: var\(--cyber-panel-frame\);/, ); assert.match( cyberStyles, /\.cockpit-utility-control\.is-expanded::after \{[\s\S]*?width: 36px;[\s\S]*?border-top: 1px solid rgba\(225, 102, 102, 0\.84\);/, ); assert.match( cyberStyles, /#global-context-panel #radio-panel\.collapsed \{\s*width: 100%;/, ); assert.match( cyberStyles, /#left-panel-stack\s*> :is\(#data-panel, #scene-panel\)\.collapsed \{\s*width: var\(--cyber-panel-collapsed-width\);/, ); assert.match( cyberStyles, /:is\(#data-panel, #scene-panel\)\.collapsed\s+:is\(\.data-panel-inner, \.scene-panel-inner\) \{[\s\S]*?height: 50px;[\s\S]*?min-height: 50px;/, ); assert.match( cyberStyles, /\.cockpit-utility-control:not\(\.is-expanded\)\s+\.cockpit-utility-launcher \{\s*width: var\(--cyber-panel-collapsed-width\);/, ); assert.match( cyberStyles, /#pp-toggles\s*> \.pp-header-row\s+\.pp-collapse-btn \{[\s\S]*?width: var\(--cyber-panel-control-size\);[\s\S]*?height: var\(--cyber-panel-control-size\);/, ); assert.match( cyberStyles, /#right-context-rail\s+:is\(\s*\.cctv-panel-inner,\s*\.weather-panel-inner,\s*\.recent-imagery-panel-inner,\s*\.street-level-panel-inner,\s*\.global-context-panel-inner\s*\) \{\s*padding-right: 8px;/, ); assert.match( cyberStyles, /#right-context-rail[\s\S]*?:is\(\s*#cctv-panel,\s*#weather-panel,\s*#recent-imagery-panel,\s*#street-level-panel,\s*#global-context-panel\s*\)\.collapsed[\s\S]*?> \.panel-header \{[\s\S]*?height: 48px;[\s\S]*?padding: 0 10px 0 14px;/, ); assert.doesNotMatch( cyberStyles, /\.cockpit-utility-control\.is-expanded\s*\{[^}]*order:/, 'expanded Cockpit utilities must retain Display-then-Radio DOM order', ); }); test('Cyber is a canonical HUD layout with a safe Tactical fallback', () => { assert.deepEqual(HUD_LAYOUTS, ['tactical', 'minimal', 'cyber']); assert.equal(normalizeHudLayout('CYBER'), 'cyber'); assert.equal(normalizeHudLayout('operator'), 'tactical'); assert.equal(normalizeHudLayout('unknown'), 'tactical'); }); test('the Cyber HUD owns the global shell skin and leaving it restores defaults', () => { const root = { dataset: { unrelated: 'preserved' } }; assert.equal(applyHudUiTheme(root, 'cyber'), 'cyber'); assert.equal(root.dataset.uiTheme, 'cyber'); assert.equal(applyHudUiTheme(root, 'minimal'), null); assert.equal(root.dataset.uiTheme, undefined); assert.equal(root.dataset.unrelated, 'preserved'); }); test('an explicit transition into Cyber selects FLIR with Ironbow 0.42 once', () => { assert.deepEqual(CYBER_VISUAL_DEFAULTS, { style: 'thermal', ironbow: 0.42, }); assert.equal( cyberVisualDefaultsForHudTransition('tactical', 'cyber', { explicit: true, }), CYBER_VISUAL_DEFAULTS, ); assert.equal( cyberVisualDefaultsForHudTransition('cyber', 'cyber', { explicit: true, }), null, ); assert.equal(cyberVisualDefaultsForHudTransition('tactical', 'cyber'), null); assert.equal( cyberVisualDefaultsForHudTransition('cyber', 'minimal', { explicit: true, }), null, ); }); test('Cyber visual defaults update the style, thermal palette and render state', () => { const calls = []; const owner = { stages: { thermal: { uniforms: { palette: 0 } } }, services: { governorRequestRender: (reason) => calls.push(['render', reason]), }, setStyle: (style, options) => calls.push(['style', style, options]), _updateSliderPanel: (style, options) => calls.push(['sliders', style, options]), }; VisualSettings.prototype._applyCyberVisualDefaults.call( owner, CYBER_VISUAL_DEFAULTS, ); assert.equal(owner.stages.thermal.uniforms.palette, 0.42); assert.deepEqual(calls, [ [ 'style', 'thermal', { applyPreset: false, revealParameters: false, preserveCyberRestore: true, }, ], ['sliders', 'thermal', { reveal: false }], ['render', 'cyber-visual-defaults'], ]); }); test('the Display selector, voice schema and final stylesheet expose Cyber', () => { const display = read('./views/panels/DisplayPanel.vue'); const stylesheet = read('../../style.css'); const rightRail = read('./rightPanelRail.js'); const hud = read('../hud.js'); const detection = read('../data/detection.js'); const civilRendering = read('../layers/flights/rendering.js'); const militaryRendering = read('../layers/military/rendering.js'); const setHud = GEV_ACTION_SCHEMAS.find((schema) => schema.name === 'set_hud'); assert.match(display, /