diff --git a/src/reasonableDefaults.test.mjs b/src/reasonableDefaults.test.mjs index 9f90ba1be..bc9f125fd 100644 --- a/src/reasonableDefaults.test.mjs +++ b/src/reasonableDefaults.test.mjs @@ -253,7 +253,7 @@ test('detection-on-by-default is a default, not an operator override', () => { // A factory default is not that. If applying the baseline set the flag, a // fresh session would silently lose the style auto-enable behaviour — a // separate landed feature, taken out by an unrelated change. - const applyDefaults = uiBlock(' _applyGlobalPostDefaults() {', '\n }\n'); + const applyDefaults = uiBlock(' _applyGlobalPostDefaults({ keepHudVariant = false } = {}) {', '\n }\n'); assert.match(applyDefaults, /this\._setDetectionMode\(defaults\.detectionMode\)/, 'the baseline still goes through the real detection path'); assert.doesNotMatch(applyDefaults, /_detectionUserOverridden/, diff --git a/src/ui/cyberTheme.test.mjs b/src/ui/cyberTheme.test.mjs index 4c8b40dff..193680eed 100644 --- a/src/ui/cyberTheme.test.mjs +++ b/src/ui/cyberTheme.test.mjs @@ -316,13 +316,13 @@ test('a shared scope opacity is restored and rides in the next share state', asy }); let shown = null; owner.store = { publish: (view) => (shown = view.scope) }; - await owner.restoreShareState({ scopeOpacityPct: 40 }); - assert.equal(getScopeMaskOpacity(), 0.4); - assert.equal(shown.opacityPct, 40); + await owner.restoreShareState({ scopeOpacityPct: 60 }); + assert.equal(getScopeMaskOpacity(), 0.6); + assert.equal(shown.opacityPct, 60); // A state without it leaves the opacity alone. await owner.restoreShareState({ scopeFeatherPct: 80 }); - assert.equal(getScopeMaskOpacity(), 0.4); - assert.equal(shown.opacityPct, 40); + assert.equal(getScopeMaskOpacity(), 0.6); + assert.equal(shown.opacityPct, 60); Object.assign(owner.services, { getDetectionTuning: () => ({ allocationStrategy: 'ELASTIC' }), getDetectionMode: () => 'OFF', @@ -332,7 +332,11 @@ test('a shared scope opacity is restored and rides in the next share state', asy getScopeTerminusOverride: () => null, }); owner.readCelestialRing = () => null; - assert.equal(owner._readShareState().options.scopeOpacityPct, 40); + assert.equal(owner._readShareState().options.scopeOpacityPct, 60); + // A link asking for less than the floor opens at the floor, and says so. + await owner.restoreShareState({ scopeOpacityPct: 20 }); + assert.equal(getScopeMaskOpacity(), 0.5); + assert.equal(owner._readShareState().options.scopeOpacityPct, 50); }); test('a scene shot saved with the retired Snow style plays on Normal', async (t) => { diff --git a/src/ui/displayBindings.js b/src/ui/displayBindings.js index 827cd51cd..aae9c1e9e 100644 --- a/src/ui/displayBindings.js +++ b/src/ui/displayBindings.js @@ -179,7 +179,9 @@ export class DisplayBindings { resetVisual: () => { this.shareLinkManager?.claimRestoreLane?.('visual'); this.setStyle('normal', { userInitiated: true }); - this._applyGlobalPostDefaults(); + // The HUD layout follows the Normal style, as choosing it does: the + // Cyber HUD stays Cyber. + this._applyGlobalPostDefaults({ keepHudVariant: true }); this._setDetectionAllocation('ELASTIC'); this._setScope({ enabled: true, featherPct: 100, opacityPct: 100 }); this._setModels3dEnabled(true); diff --git a/src/ui/displayPanel.test.mjs b/src/ui/displayPanel.test.mjs index 5b9489f39..c9550a5d2 100644 --- a/src/ui/displayPanel.test.mjs +++ b/src/ui/displayPanel.test.mjs @@ -108,7 +108,11 @@ test('bloom and sharpen render their state, slider and readout', async (t) => { await nextTick(); assert.equal($('bloom-toggle').classList.contains('active'), false); assert.equal($('bloom-slider-row').classList.contains('visible'), false); - assert.equal($('bloom-intensity-value').textContent, '140%', 'the amount stays'); + assert.equal( + $('bloom-intensity-value').textContent, + '140%', + 'the amount stays', + ); }); test('a publish never rewrites a slider the user is dragging', async (t) => { @@ -326,36 +330,50 @@ test('scope opacity renders what VisualSettings shows and sets the mask', async assert.equal($('scope-opacity-slider').value, '100'); assert.equal($('scope-opacity-value').textContent, '100%'); - owner._setScope({ opacityPct: 42.4 }); + owner._setScope({ opacityPct: 62.4 }); await nextTick(); - assert.equal(mask.opacity, 0.42); - assert.equal($('scope-opacity-slider').value, '42'); - assert.equal($('scope-opacity-value').textContent, '42%'); - assert.equal(store.view.scope.opacityPct, 42); + assert.equal(mask.opacity, 0.62); + assert.equal($('scope-opacity-slider').value, '62'); + assert.equal($('scope-opacity-value').textContent, '62%'); + assert.equal(store.view.scope.opacityPct, 62); + + // No fainter than the floor, where the view would read as Scope off. + owner._setScope({ opacityPct: 10 }); + await nextTick(); + assert.equal(mask.opacity, 0.5, 'floored'); + assert.equal($('scope-opacity-value').textContent, '50%'); + assert.equal($('scope-opacity-slider').min, '50'); + + // A value that is not a number changes nothing. + owner._setScope({ opacityPct: Number.NaN }); + await nextTick(); + assert.equal($('scope-opacity-value').textContent, '50%'); owner._setScope({ opacityPct: 180 }); await nextTick(); assert.equal(mask.opacity, 1, 'clamped'); assert.equal($('scope-opacity-value').textContent, '100%'); - owner._setScope({ opacityPct: 30 }, { apply: false }); + owner._setScope({ opacityPct: 70 }, { apply: false }); await nextTick(); assert.equal(mask.opacity, 1, 'only shown'); - assert.equal($('scope-opacity-value').textContent, '30%'); + assert.equal($('scope-opacity-value').textContent, '70%'); owner._setScope({ enabled: false }); await nextTick(); assert.equal($('scope-opacity-slider').disabled, true, 'off with the scope'); - assert.equal(store.view.scope.opacityPct, 30, 'kept while off'); + assert.equal(store.view.scope.opacityPct, 70, 'kept while off'); }); test('the Visual panel groups its controls and words its on/off pills', (t) => { const { $ } = fixture(t); const panel = $('pp-toggles'); assert.deepEqual( - [...panel.querySelectorAll('.pp-section-heading, .visual-presets__heading')].map( - (el) => el.textContent.trim(), - ), + [ + ...panel.querySelectorAll( + '.pp-section-heading, .visual-presets__heading', + ), + ].map((el) => el.textContent.trim()), ['Look', 'MAP SOURCE 3D', 'Overlay', 'Effects'], ); assert.equal(panel.querySelector('.pp-header-label').textContent, 'VISUAL'); @@ -384,7 +402,10 @@ test('the 3D aircraft group renders what AircraftDisplay shows', async (t) => { assert.equal(coreClasses(button), 'pp-toggle-btn active'); assert.equal(button.getAttribute('aria-pressed'), 'true'); assert.equal(row.classList.contains('visible'), true); - assert.equal($('models3d-mode-proximity').getAttribute('aria-checked'), 'true'); + assert.equal( + $('models3d-mode-proximity').getAttribute('aria-checked'), + 'true', + ); // The button leaves the row to its own sync. owner._setModels3dEnabled(false); @@ -400,7 +421,10 @@ test('the 3D aircraft group renders what AircraftDisplay shows', async (t) => { await nextTick(); assert.equal(all.classList.contains('active'), true); assert.equal(all.getAttribute('aria-checked'), 'true'); - assert.equal($('models3d-mode-proximity').classList.contains('active'), false); + assert.equal( + $('models3d-mode-proximity').classList.contains('active'), + false, + ); assert.deepEqual(params.at(-1), ['military', { models3dMode: 'all' }]); owner._syncModels3dFromLayerState({ @@ -429,12 +453,18 @@ test('the Draw controls render what the draw tool publishes', async (t) => { assert.equal(segment('area').getAttribute('aria-label'), 'Area'); assert.equal(segment('area').title, 'Area'); assert.equal(segment('area').textContent, 'pentagon'); - const swatch = (color) => document.querySelector(`#draw-colors [data-color="${color}"]`); + const swatch = (color) => + document.querySelector(`#draw-colors [data-color="${color}"]`); assert.equal(swatch('primary').getAttribute('aria-checked'), 'true'); assert.equal(swatch('amber').getAttribute('aria-label'), 'Amber'); assert.equal($('draw-hint').textContent, ''); - store.publishDraw({ active: true, shape: 'line', color: 'amber', hint: 'Click to add points.' }); + store.publishDraw({ + active: true, + shape: 'line', + color: 'amber', + hint: 'Click to add points.', + }); await nextTick(); assert.equal(swatch('amber').getAttribute('aria-checked'), 'true'); assert.equal(swatch('primary').classList.contains('active'), false); @@ -530,7 +560,10 @@ test('the imported files render what the file import publishes and keep focus', assert.equal(document.activeElement === button(first, 'toggle'), true); assert.equal(button(first, 'toggle').getAttribute('aria-pressed'), 'true'); assert.equal(button(first, 'fly').title, 'Fly to a.geojson'); - assert.equal(button(first, 'remove').getAttribute('aria-label'), 'Remove a.geojson'); + assert.equal( + button(first, 'remove').getAttribute('aria-label'), + 'Remove a.geojson', + ); await press(button(first, 'remove')); assert.equal(rowsNow().length, 1); @@ -646,7 +679,11 @@ test('VisualSettings publishes a style’s parameters and writes a moved slider assert.equal(store.parameters, null, 'the sliders go with their owner'); assert.equal($('param-slider-panel').classList.contains('active'), false); assert.equal(store.act('setStyleParameter', 'grainAmt', 0.2), undefined); - assert.equal(owner.stages.noir.uniforms.grainAmt, 0.3, 'a stopped owner no longer answers'); + assert.equal( + owner.stages.noir.uniforms.grainAmt, + 0.3, + 'a stopped owner no longer answers', + ); // An older owner's stop leaves a newer owner's rows. const newer = { style: 'noir', rows: [] }; owner._parameters = { style: 'noir', rows: [] }; @@ -658,10 +695,16 @@ test('VisualSettings publishes a style’s parameters and writes a moved slider test('the four Cockpit groups teleport to the published slots and back to their places', async (t) => { const { store, $ } = fixture(t); const groupOf = (id) => $(id).closest('.pp-toggle-group') ?? $(id); - const ids = ['hud-toggle', 'detection-toggle', 'param-slider-panel', 'models3d-toggle']; + const ids = [ + 'hud-toggle', + 'detection-toggle', + 'param-slider-panel', + 'models3d-toggle', + ]; const body = $('pp-toggles').querySelector('.pp-panel-body'); const order = () => - [...body.children].filter((el) => el.matches('.pp-toggle-group, #param-slider-panel')) + [...body.children] + .filter((el) => el.matches('.pp-toggle-group, #param-slider-panel')) .map((el) => el.id || el.querySelector('[id]')?.id); const home = order(); const popover = document.body.appendChild(document.createElement('div')); diff --git a/src/ui/displayPanelActions.test.mjs b/src/ui/displayPanelActions.test.mjs index 59cb064dd..feb123cba 100644 --- a/src/ui/displayPanelActions.test.mjs +++ b/src/ui/displayPanelActions.test.mjs @@ -246,7 +246,7 @@ test('the Visual panel’s scope opacity, reset and presets call their actions', ], }); await nextTick(); - input($('scope-opacity-slider'), 40); + input($('scope-opacity-slider'), 60); document.querySelector('#pp-toggles .pp-reset-btn').click(); const presets = document.querySelector('#pp-toggles .visual-presets'); presets.querySelector('[data-style="noir"]').click(); @@ -254,7 +254,7 @@ test('the Visual panel’s scope opacity, reset and presets call their actions', chips[0].click(); chips[1].click(); assert.deepEqual(calls, [ - ['setScopeOpacity', 40], + ['setScopeOpacity', 60], ['resetVisual'], ['setStyle', 'noir'], ]); @@ -290,7 +290,8 @@ test('DisplayBindings sets the scope opacity and resets Visual to its defaults', elements: { _locationSearch: document.createElement('input') }, operations: { setStyle: (style, options) => calls.push(['style', style, options]), - _applyGlobalPostDefaults: () => calls.push(['postDefaults']), + _applyGlobalPostDefaults: (options) => + calls.push(['postDefaults', options]), _setDetectionAllocation: (value) => calls.push(['allocation', value]), _setScope: (scope) => calls.push(['scope', scope]), _setModels3dEnabled: (on) => calls.push(['models3d', on]), @@ -310,10 +311,10 @@ test('DisplayBindings sets the scope opacity and resets Visual to its defaults', bindings._initUI(); t.after(() => bindings.destroy()); calls.length = 0; - input($('scope-opacity-slider'), 35); + input($('scope-opacity-slider'), 65); assert.deepEqual(calls, [ ['claim', 'visual'], - ['scope', { opacityPct: 35 }], + ['scope', { opacityPct: 65 }], ['share'], ]); calls.length = 0; @@ -321,7 +322,7 @@ test('DisplayBindings sets the scope opacity and resets Visual to its defaults', assert.deepEqual(calls, [ ['claim', 'visual'], ['style', 'normal', { userInitiated: true }], - ['postDefaults'], + ['postDefaults', { keepHudVariant: true }], ['allocation', 'ELASTIC'], ['scope', { enabled: true, featherPct: 100, opacityPct: 100 }], ['models3d', true], diff --git a/src/ui/views/panels/DisplayPanel.vue b/src/ui/views/panels/DisplayPanel.vue index 876982e86..b3adbb6f1 100644 --- a/src/ui/views/panels/DisplayPanel.vue +++ b/src/ui/views/panels/DisplayPanel.vue @@ -71,7 +71,7 @@ const allocationOptions = computed(() => [
{{ t('Overlay') }}