mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 07:42:23 +00:00
Test scope opacity, the Visual reset and the panel's presets
The share codec carries field 30 and still encodes a link made before it to the same code; the long form writes sco only below full opacity; the mask fades without repainting; the panel shows and sets the opacity, and the reset and the presets call their actions. Co-authored-by: Manjunath Paparaju <[email protected]>
This commit is contained in:
1 parent
471151910d
commit
6c1744e55b
6 files changed
+294
-1
No files matched your search
@@ -19,6 +19,8 @@ import {
|
||||
SCOPE_TERMINUS_MAX_PCT,
|
||||
SCOPE_FEATHER_RATIO_DEFAULT,
|
||||
getScopeMaskFeather,
|
||||
setScopeMaskOpacity,
|
||||
getScopeMaskOpacity,
|
||||
SCOPE_OUTSIDE_ALPHA,
|
||||
SCOPE_TERMINUS_ALPHA_NEAR,
|
||||
SCOPE_TERMINUS_FAR_M,
|
||||
@@ -205,6 +207,37 @@ test('scope appearance follows direct state changes and install/destroy lifecycl
|
||||
}
|
||||
});
|
||||
|
||||
test('scope opacity fades the painted mask without repainting it', () => {
|
||||
const dom = stubScopeMaskDom();
|
||||
try {
|
||||
assert.equal(getScopeMaskOpacity(), 1, 'full by default');
|
||||
setScopeMaskOpacity(0.4);
|
||||
installScopeMask({ container: dom.container });
|
||||
assert.equal(dom.canvas.style.opacity, '0.4', 'a choice made before install applies');
|
||||
const paints = { ...dom.ops };
|
||||
const terminus = getScopeTerminusAlpha();
|
||||
setScopeMaskOpacity(0.25);
|
||||
assert.equal(dom.canvas.style.opacity, '0.25');
|
||||
assert.deepEqual(dom.ops, paints, 'the mask is not repainted');
|
||||
assert.equal(getScopeTerminusAlpha(), terminus, 'the terminus is its own setting');
|
||||
setScopeMaskOpacity(3);
|
||||
assert.equal(getScopeMaskOpacity(), 1);
|
||||
setScopeMaskOpacity(-1);
|
||||
assert.equal(getScopeMaskOpacity(), 0);
|
||||
setScopeMaskOpacity('soft');
|
||||
assert.equal(getScopeMaskOpacity(), 1, 'not a number is full');
|
||||
setScopeMaskOpacity(0.5);
|
||||
destroyScopeMask();
|
||||
installScopeMask({ container: dom.container });
|
||||
assert.equal(dom.canvas.style.opacity, '0.5', 'reinstall keeps it');
|
||||
_resetScopeMaskForTest();
|
||||
assert.equal(getScopeMaskOpacity(), 1, 'test reset restores full');
|
||||
} finally {
|
||||
destroyScopeMask();
|
||||
dom.restore();
|
||||
}
|
||||
});
|
||||
|
||||
test('a DPR change with no resize still repaints the backing store', () => {
|
||||
const dom = stubScopeMaskDom({ width: 1000, height: 800, dpr: 1 });
|
||||
try {
|
||||
|
||||
@@ -119,6 +119,7 @@ function randomState(random) {
|
||||
detectionFadePct: maybe(0.5, int(0, 40)),
|
||||
detectionOutsideOpacityPct: maybe(0.5, int(0, 100)),
|
||||
scopeFeatherPct: maybe(0.5, int(0, 200)),
|
||||
scopeOpacityPct: maybe(0.5, int(0, 100)),
|
||||
scopeTerminusPct: maybe(
|
||||
0.3,
|
||||
int(SCOPE_TERMINUS_MIN_PCT, SCOPE_TERMINUS_MAX_PCT),
|
||||
@@ -210,6 +211,7 @@ test('each setting costs bytes only when it differs from its default', () => {
|
||||
detectionFadePct: 8,
|
||||
detectionOutsideOpacityPct: 2,
|
||||
scopeFeatherPct: 99,
|
||||
scopeOpacityPct: 40,
|
||||
scopeTerminusPct: 95,
|
||||
};
|
||||
const defaults = createDefaultShareViewState();
|
||||
@@ -355,6 +357,7 @@ test('every registered field round-trips at once', () => {
|
||||
celestialRing: true,
|
||||
scopeEnabled: false,
|
||||
scopeFeatherPct: 35,
|
||||
scopeOpacityPct: 60,
|
||||
scopeTerminusPct: 97,
|
||||
mapStack: 'esri-imagery',
|
||||
layerState,
|
||||
@@ -1587,3 +1590,50 @@ test('Data Library tokens round-trip and malformed ones are skipped', () => {
|
||||
assert.equal(normalizeLibraryTokens(null), null);
|
||||
assert.deepEqual(normalizeLibraryTokens('x'.repeat(500)), []);
|
||||
});
|
||||
|
||||
test('scope opacity is field 30; a link made before it opens at full opacity', () => {
|
||||
// Made before field 30 existed: thermal, Cyber, Scope off at 35% feather
|
||||
// and a pinned terminus.
|
||||
const before = 'ARD-7j4YuxMguBcoUEgQcAOAAQPIASPQAWGa';
|
||||
const old = decodeOk(before).state;
|
||||
assert.equal(old.scopeOpacityPct, 100);
|
||||
assert.equal(old.scopeFeatherPct, 35);
|
||||
assert.equal(encodeShareView(old), before, 'it still encodes to itself');
|
||||
const state = {
|
||||
lat: 51.5007,
|
||||
lon: -0.1246,
|
||||
alt: 1500,
|
||||
heading: 40,
|
||||
style: 'thermal',
|
||||
scopeEnabled: false,
|
||||
scopeFeatherPct: 35,
|
||||
scopeTerminusPct: 97,
|
||||
hudVariant: 'cyber',
|
||||
};
|
||||
assert.equal(encodeShareView(state), before);
|
||||
assert.equal(encodeShareView({ ...state, scopeOpacityPct: 100 }), before);
|
||||
const faded = assertRoundTrip({ ...state, scopeOpacityPct: 45 });
|
||||
assert.notEqual(faded, before);
|
||||
assert.equal(decodeOk(faded).state.scopeOpacityPct, 45);
|
||||
assert.equal(
|
||||
decodeOk(assertRoundTrip({ scopeOpacityPct: 0 })).state.scopeOpacityPct,
|
||||
0,
|
||||
);
|
||||
// Out of range is clamped; anything not a number is the default.
|
||||
assert.equal(
|
||||
normalizeShareViewState({ scopeOpacityPct: 150 }).scopeOpacityPct,
|
||||
100,
|
||||
);
|
||||
assert.equal(
|
||||
normalizeShareViewState({ scopeOpacityPct: -5 }).scopeOpacityPct,
|
||||
0,
|
||||
);
|
||||
assert.equal(
|
||||
normalizeShareViewState({ scopeOpacityPct: 33.6 }).scopeOpacityPct,
|
||||
34,
|
||||
);
|
||||
assert.equal(
|
||||
normalizeShareViewState({ scopeOpacityPct: 'x' }).scopeOpacityPct,
|
||||
100,
|
||||
);
|
||||
});
|
||||
@@ -121,6 +121,29 @@ test('scope feather above 100% survives share parsing and serialization', () =>
|
||||
clearTimeout(manager._debounceTimer);
|
||||
});
|
||||
|
||||
test('scope opacity rides as sco only below full, and through the compact link', () => {
|
||||
// Links without `sco`, every link made before it, open at full opacity.
|
||||
assert.equal(makeManager('#v=2&lat=10&lon=20&scf=35').parseInitialHash().scopeOpacityPct, 100);
|
||||
assert.equal(makeManager('#v=2&lat=10&lon=20&sco=40').parseInitialHash().scopeOpacityPct, 40);
|
||||
assert.equal(makeManager('#v=2&lat=10&lon=20&sco=250').parseInitialHash().scopeOpacityPct, 100);
|
||||
assert.equal(makeManager('#v=2&lat=10&lon=20&sco=-3').parseInitialHash().scopeOpacityPct, 0);
|
||||
assert.equal(makeManager('#v=2&lat=10&lon=20&sco=x').parseInitialHash().scopeOpacityPct, 100);
|
||||
|
||||
const manager = makeManager();
|
||||
manager.onToggleChange(false, true, {});
|
||||
const before = manager._buildHashParams().toString();
|
||||
assert.equal(manager._buildHashParams().has('sco'), false, 'full opacity writes nothing');
|
||||
manager.onToggleChange(false, true, { scopeOpacityPct: 100 });
|
||||
assert.equal(manager._buildHashParams().toString(), before);
|
||||
manager.onToggleChange(false, true, { scopeOpacityPct: 45.4 });
|
||||
assert.equal(manager._buildHashParams().get('sco'), '45');
|
||||
assert.equal(sharedState(manager.buildLink()).scopeOpacityPct, 45);
|
||||
manager.onToggleChange(false, true, { scopeOpacityPct: 100 });
|
||||
assert.equal(manager._buildHashParams().has('sco'), false);
|
||||
assert.equal(sharedState(manager.buildLink()).scopeOpacityPct, 100);
|
||||
clearTimeout(manager._debounceTimer);
|
||||
});
|
||||
|
||||
test('share-link serialization emits the current celestial state', () => {
|
||||
const manager = makeManager();
|
||||
manager.onToggleChange(false, false, { celestialRingEnabled: false });
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
import { getScopeMaskFeather, isScopeMaskEnabled, setScopeMaskEnabled, setScopeMaskFeather } from '../scopeMask.js';
|
||||
import {
|
||||
getScopeMaskFeather,
|
||||
getScopeMaskOpacity,
|
||||
isScopeMaskEnabled,
|
||||
setScopeMaskEnabled,
|
||||
setScopeMaskFeather,
|
||||
setScopeMaskOpacity,
|
||||
} from '../scopeMask.js';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
@@ -298,6 +305,36 @@ test('scope choice and expanded feather survive every HUD layout and both restor
|
||||
assert.deepEqual(shown, { enabled: true, featherPct: 200, opacityPct: 100 });
|
||||
});
|
||||
|
||||
test('a shared scope opacity is restored and rides in the next share state', async (t) => {
|
||||
const { owner } = styleOwner(t, 'tactical');
|
||||
const previous = getScopeMaskOpacity();
|
||||
t.after(() => setScopeMaskOpacity(previous));
|
||||
Object.assign(owner.services, {
|
||||
setScopeMaskEnabled() {},
|
||||
setScopeMaskFeather() {},
|
||||
setScopeMaskOpacity,
|
||||
});
|
||||
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);
|
||||
// A state without it leaves the opacity alone.
|
||||
await owner.restoreShareState({ scopeFeatherPct: 80 });
|
||||
assert.equal(getScopeMaskOpacity(), 0.4);
|
||||
assert.equal(shown.opacityPct, 40);
|
||||
Object.assign(owner.services, {
|
||||
getDetectionTuning: () => ({ allocationStrategy: 'ELASTIC' }),
|
||||
getDetectionMode: () => 'OFF',
|
||||
isScopeMaskEnabled: () => true,
|
||||
getScopeMaskFeather: () => 0.8,
|
||||
getScopeMaskOpacity,
|
||||
getScopeTerminusOverride: () => null,
|
||||
});
|
||||
owner.readCelestialRing = () => null;
|
||||
assert.equal(owner._readShareState().options.scopeOpacityPct, 40);
|
||||
});
|
||||
|
||||
test('a scene shot saved with the retired Snow style plays on Normal', async (t) => {
|
||||
const { owner } = styleOwner(t, 'tactical');
|
||||
owner.setStyle('noir');
|
||||
|
||||
@@ -314,6 +314,59 @@ test('Scope and Celestial render what VisualSettings shows', async (t) => {
|
||||
assert.equal(celestial.title, 'Celestial ring — reveal the full globe');
|
||||
});
|
||||
|
||||
test('scope opacity renders what VisualSettings shows and sets the mask', async (t) => {
|
||||
const { store, $ } = fixture(t);
|
||||
const { owner } = settings(store);
|
||||
const mask = { opacity: 1 };
|
||||
owner.services = {
|
||||
...owner.services,
|
||||
setScopeMaskEnabled() {},
|
||||
setScopeMaskOpacity: (value) => (mask.opacity = value),
|
||||
};
|
||||
assert.equal($('scope-opacity-slider').value, '100');
|
||||
assert.equal($('scope-opacity-value').textContent, '100%');
|
||||
|
||||
owner._setScope({ opacityPct: 42.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);
|
||||
|
||||
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 });
|
||||
await nextTick();
|
||||
assert.equal(mask.opacity, 1, 'only shown');
|
||||
assert.equal($('scope-opacity-value').textContent, '30%');
|
||||
|
||||
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');
|
||||
});
|
||||
|
||||
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(),
|
||||
),
|
||||
['Look', 'MAP SOURCE 3D', 'Overlay', 'Effects'],
|
||||
);
|
||||
assert.equal(panel.querySelector('.pp-header-label').textContent, 'VISUAL');
|
||||
const body = panel.querySelector('.pp-panel-body');
|
||||
assert.equal(body.style.getPropertyValue('--pp-pill-on'), '"ON"');
|
||||
assert.equal(body.style.getPropertyValue('--pp-pill-off'), '"OFF"');
|
||||
const reset = panel.querySelector('.pp-reset-btn');
|
||||
assert.equal(reset, body.lastElementChild, 'the reset closes the panel');
|
||||
assert.equal(reset.textContent.trim(), 'restart_altReset to default');
|
||||
});
|
||||
|
||||
test('the 3D aircraft group renders what AircraftDisplay shows', async (t) => {
|
||||
const { store, $ } = fixture(t);
|
||||
const params = [];
|
||||
|
||||
@@ -231,3 +231,100 @@ test('several owners answer their own actions, the newest first', (t) => {
|
||||
['display', 'toggleBloom'],
|
||||
]);
|
||||
});
|
||||
|
||||
test('the Visual panel’s scope opacity, reset and presets call their actions', async (t) => {
|
||||
const store = mount(t);
|
||||
const { calls } = recorder(store);
|
||||
const selected = [];
|
||||
store.connectMapSource({ select: async (id) => selected.push(id) });
|
||||
store.publishMapSource({
|
||||
status: { text: 'OSM', warn: false },
|
||||
chips: [
|
||||
{ id: 'photoreal', label: 'Google 3D', available: false },
|
||||
{ id: 'osm', label: 'OSM', available: true, active: true },
|
||||
],
|
||||
});
|
||||
await nextTick();
|
||||
input($('scope-opacity-slider'), 40);
|
||||
document.querySelector('#pp-toggles .pp-reset-btn').click();
|
||||
const presets = document.querySelector('#pp-toggles .visual-presets');
|
||||
presets.querySelector('[data-style="noir"]').click();
|
||||
const chips = presets.querySelectorAll('.visual-presets__chip');
|
||||
chips[0].click();
|
||||
chips[1].click();
|
||||
assert.deepEqual(calls, [
|
||||
['setScopeOpacity', 40],
|
||||
['resetVisual'],
|
||||
['setStyle', 'noir'],
|
||||
]);
|
||||
assert.deepEqual(selected, ['osm'], 'an unavailable source is not selected');
|
||||
// The presets follow the published style, as the dock's do.
|
||||
assert.equal(
|
||||
presets.querySelector('[data-style="normal"]').getAttribute('aria-pressed'),
|
||||
'true',
|
||||
);
|
||||
store.publishStyle({ name: 'noir', label: 'NOIR' });
|
||||
await nextTick();
|
||||
assert.equal(
|
||||
presets.querySelector('[data-style="noir"]').getAttribute('aria-pressed'),
|
||||
'true',
|
||||
);
|
||||
assert.equal(
|
||||
presets.querySelector('[data-style="normal"]').getAttribute('aria-pressed'),
|
||||
'false',
|
||||
);
|
||||
// The dock keeps its own presets and map source beside them.
|
||||
assert.ok(
|
||||
document.querySelector('#control-panel .style-btn[data-style="noir"]'),
|
||||
);
|
||||
assert.ok($('map-stack-chips'));
|
||||
});
|
||||
|
||||
test('DisplayBindings sets the scope opacity and resets Visual to its defaults', (t) => {
|
||||
const store = mount(t);
|
||||
const calls = [];
|
||||
const bindings = new DisplayBindings({
|
||||
viewer: null,
|
||||
services: { isScopeMaskEnabled: () => true, cycleDetectionMode: () => {} },
|
||||
elements: { _locationSearch: document.createElement('input') },
|
||||
operations: {
|
||||
setStyle: (style, options) => calls.push(['style', style, options]),
|
||||
_applyGlobalPostDefaults: () => calls.push(['postDefaults']),
|
||||
_setDetectionAllocation: (value) => calls.push(['allocation', value]),
|
||||
_setScope: (scope) => calls.push(['scope', scope]),
|
||||
_setModels3dEnabled: (on) => calls.push(['models3d', on]),
|
||||
_setModels3dMode: (mode) => calls.push(['models3dMode', mode]),
|
||||
_syncModels3dModeRow: () => calls.push(['models3dRow']),
|
||||
_syncShareState: () => calls.push(['share']),
|
||||
},
|
||||
readState: () => ({
|
||||
shareLinkManager: {
|
||||
claimRestoreLane: (lane) => calls.push(['claim', lane]),
|
||||
},
|
||||
}),
|
||||
claimDetection: () => {},
|
||||
displayStore: store,
|
||||
});
|
||||
bindings._initUI();
|
||||
t.after(() => bindings.destroy());
|
||||
calls.length = 0;
|
||||
input($('scope-opacity-slider'), 35);
|
||||
assert.deepEqual(calls, [
|
||||
['claim', 'visual'],
|
||||
['scope', { opacityPct: 35 }],
|
||||
['share'],
|
||||
]);
|
||||
calls.length = 0;
|
||||
document.querySelector('#pp-toggles .pp-reset-btn').click();
|
||||
assert.deepEqual(calls, [
|
||||
['claim', 'visual'],
|
||||
['style', 'normal', { userInitiated: true }],
|
||||
['postDefaults'],
|
||||
['allocation', 'ELASTIC'],
|
||||
['scope', { enabled: true, featherPct: 100, opacityPct: 100 }],
|
||||
['models3d', true],
|
||||
['models3dMode', 'proximity'],
|
||||
['models3dRow'],
|
||||
['share'],
|
||||
]);
|
||||
});
|
||||
Reference in new issue
Block a user