mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Trim the doc text this PR adds (README, CHANGELOG, CURRENT-STATE, KNOWN-ISSUES, SECURITY, DATA_SOURCES, .env.example, the CI job comment), drop code comments that restate the code, cut design-note JSDoc to a line or two, and drop review tags from test names. Comments only; no code changes. Also: README key count (seven keys, two metered), CURRENT-STATE's gate paragraph (CI runs a production build with a dummy token), the stacked doc above meshBoxInRange and the orphaned inMeshRange line in coverage.js, and a duplicate ALPR row in DATA_SOURCES that a merge had brought back. Co-Authored-By: Claude Opus 5.5 <[email protected]>
781 lines
34 KiB
JavaScript
781 lines
34 KiB
JavaScript
import { DisplayBindings } from './ui/displayBindings.js';
|
|
import { LocationNavigation } from './ui/locationNavigation.js';
|
|
import { VisualSettings } from './ui/visualSettings.js';
|
|
import { readShellSource, shellMethod } from './testSupport/readShellSource.mjs';
|
|
import { StyleManager } from './ui/applicationShell.js';
|
|
import { _claimContextVisualAuthority, setContextMode } from './ui/contextActions.js';
|
|
import { _initGlobalContextPanel } from './ui/contextBindings.js';
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import { ShareLinkManager, decodeShareCreatedAtMs } from './sharelink.js';
|
|
import { createDefaultLayerState } from './data/layerState.js';
|
|
|
|
const uiSource = readShellSource();
|
|
|
|
function sourceBlock(start, end) {
|
|
const name = start.trim().match(/^(?:async )?(\w+)\(/)?.[1];
|
|
if (typeof shellMethod(name) === 'function') return shellMethod(name).toString();
|
|
const startIndex = uiSource.indexOf(start);
|
|
const endIndex = uiSource.indexOf(end, startIndex + start.length);
|
|
assert.ok(startIndex >= 0, `missing source block start: ${start}`);
|
|
assert.ok(endIndex > startIndex, `missing source block end: ${end}`);
|
|
return uiSource.slice(startIndex, endIndex);
|
|
}
|
|
|
|
function assertClaimsBefore(block, mutation, label) {
|
|
const claimIndex = block.indexOf("claimRestoreLane?.('visual')");
|
|
const mutationIndex = block.indexOf(mutation);
|
|
assert.ok(claimIndex >= 0, `${label} must claim the visual restore lane`);
|
|
assert.ok(mutationIndex >= 0, `${label} mutation marker is missing`);
|
|
assert.ok(claimIndex < mutationIndex, `${label} must claim before mutation`);
|
|
}
|
|
|
|
function makeManager(hash = '') {
|
|
globalThis.window = { location: { hash, href: `http://localhost/${hash}` } };
|
|
globalThis.history = {
|
|
replaceState(_state, _title, next) {
|
|
window.location.hash = new URL(next, window.location.href).hash;
|
|
},
|
|
};
|
|
const viewer = {
|
|
camera: {
|
|
changed: { addEventListener() {} },
|
|
positionCartographic: { latitude: 0, longitude: 0, height: 1000 },
|
|
heading: 0,
|
|
pitch: -Math.PI / 2,
|
|
roll: 0,
|
|
},
|
|
};
|
|
return new ShareLinkManager(viewer);
|
|
}
|
|
|
|
function installClipboard(writeText) {
|
|
Object.defineProperty(globalThis, 'navigator', {
|
|
configurable: true,
|
|
value: { clipboard: { writeText } },
|
|
});
|
|
}
|
|
|
|
test('share links without cr default the celestial ring off', () => {
|
|
const manager = makeManager('#lat=10&lon=20&style=normal');
|
|
assert.equal(manager.parseInitialHash().celestialRing, false);
|
|
});
|
|
|
|
test('share links parse explicit celestial on and off states', () => {
|
|
assert.equal(makeManager('#lat=10&lon=20&cr=1').parseInitialHash().celestialRing, true);
|
|
assert.equal(makeManager('#lat=10&lon=20&cr=0').parseInitialHash().celestialRing, false);
|
|
});
|
|
|
|
test('unknown-only v2 layer tokens are invalid, while historical l fields stay inert', () => {
|
|
const invalid = makeManager('#v=2&lat=10&lon=20&l=unknown').parseInitialHash();
|
|
assert.equal(invalid.layerState, null);
|
|
assert.equal(invalid.layerStateInvalid, true);
|
|
for (const hash of ['#lat=10&lon=20&l=z', '#v=1&lat=10&lon=20&l=z']) {
|
|
const legacy = makeManager(hash).parseInitialHash();
|
|
assert.equal(legacy.layerState, null);
|
|
assert.equal(legacy.layerStateInvalid, false);
|
|
}
|
|
});
|
|
|
|
test('malformed v2 layer lists mark the whole incoming share payload invalid', () => {
|
|
for (const layers of ['.f', 'f.', 'f..c', 'f.f']) {
|
|
const parsed = makeManager(
|
|
`#v=2&lat=10&lon=20&l=${layers}`,
|
|
).parseInitialHash();
|
|
assert.equal(parsed.layerState, null, `l=${layers}`);
|
|
assert.equal(parsed.layerStateInvalid, true, `l=${layers}`);
|
|
}
|
|
for (const layers of ['l=f&l=f', 'l=f&l=unknown', 'l=&l=f']) {
|
|
const parsed = makeManager(
|
|
`#v=2&lat=10&lon=20&${layers}`,
|
|
).parseInitialHash();
|
|
assert.equal(parsed.layerState, null, layers);
|
|
assert.equal(parsed.layerStateInvalid, true, layers);
|
|
}
|
|
});
|
|
|
|
test('Nepal locator token is valid in v2 share links', () => {
|
|
const parsed = makeManager('#v=2&lat=10&lon=20&l=z').parseInitialHash();
|
|
assert.deepEqual(parsed.layerState.enabledLayerIds, ['bhote-koshi-locator']);
|
|
assert.equal(parsed.layerStateInvalid, false);
|
|
});
|
|
|
|
test('share-link serialization emits the current celestial state', () => {
|
|
const manager = makeManager();
|
|
manager.onToggleChange(false, false, { celestialRingEnabled: false });
|
|
clearTimeout(manager._debounceTimer);
|
|
manager._updateHash();
|
|
assert.equal(new URLSearchParams(window.location.hash.slice(1)).get('cr'), '0');
|
|
|
|
manager.onToggleChange(false, false, { celestialRingEnabled: true });
|
|
clearTimeout(manager._debounceTimer);
|
|
manager._updateHash();
|
|
assert.equal(new URLSearchParams(window.location.hash.slice(1)).get('cr'), '1');
|
|
});
|
|
|
|
test('generated links are v2 and include deterministic layers, options, style params, and panels', () => {
|
|
const manager = makeManager();
|
|
const layers = createDefaultLayerState();
|
|
layers.enabledLayerIds = ['cctv', 'radio'];
|
|
layers.options.cctv = { coverageMode: 'viewshed', showProjection: false, autoHop: true };
|
|
layers.options.radio = { filter: 'news', volume: 0.45 };
|
|
manager.setLayerStateProvider(() => layers);
|
|
manager.setPanelStateProvider(() => ({ specs: [
|
|
{ id: 'control-panel', collapsed: false, pinned: true },
|
|
{ id: 'param-slider-panel', collapsed: true },
|
|
] }));
|
|
manager.setStyleParamStateProvider(() => ({
|
|
sensitivity: 0.82, bloom: 0.37, mode: 1, pixelation: 2.6, palette: 1,
|
|
}));
|
|
manager.onStyleChange('thermal');
|
|
clearTimeout(manager._debounceTimer);
|
|
manager._updateHash();
|
|
const params = new URLSearchParams(window.location.hash.slice(1));
|
|
assert.equal(params.get('v'), '2');
|
|
assert.equal(params.get('l'), 'c.r');
|
|
assert.equal(params.get('sp'), 's.82_b.37_m.100_p.260_a.100');
|
|
assert.equal(params.get('ui'), 'c.c.0_c.p.1_m.c.1');
|
|
});
|
|
|
|
test('visual parameters, explicit empty layers, and panel state are v2-only', () => {
|
|
const parsed = makeManager(
|
|
'#v=2&lat=10&lon=20&style=flir&l=&sp=s.82_b.37_p.260&ui=c.c.0_c.p.1_d.c.1_d.p.1',
|
|
).parseInitialHash();
|
|
assert.deepEqual(parsed.layerState.enabledLayerIds, []);
|
|
assert.deepEqual(parsed.styleParams, { sensitivity: 0.82, bloom: 0.37, pixelation: 2.6 });
|
|
assert.deepEqual(parsed.panelState, { specs: [
|
|
{ id: 'control-panel', collapsed: false, pinned: true },
|
|
{ id: 'data-panel', collapsed: true, pinned: null },
|
|
] });
|
|
const legacy = makeManager('#v=1&lat=10&lon=20&style=flir&l=&sp=s.82&ui=c.c.0')
|
|
.parseInitialHash();
|
|
assert.equal(legacy.layerState, null);
|
|
assert.equal(legacy.styleParams, null);
|
|
assert.equal(legacy.panelState, null);
|
|
});
|
|
|
|
// The panel chrome shares every rail panel, so the link registry must know
|
|
// each one: a collapsed Recent Imagery panel used to restore expanded because
|
|
// the encoder had no token for it.
|
|
test('a collapsed Recent Imagery panel survives the share-link round trip beside the older panels', () => {
|
|
const manager = makeManager();
|
|
manager.setPanelStateProvider(() => ({ specs: [
|
|
{ id: 'cctv-panel', collapsed: false },
|
|
{ id: 'recent-imagery-panel', collapsed: true },
|
|
{ id: 'global-context-panel', collapsed: true },
|
|
] }));
|
|
clearTimeout(manager._debounceTimer);
|
|
manager._updateHash();
|
|
const params = new URLSearchParams(window.location.hash.slice(1));
|
|
assert.equal(params.get('ui'), 'v.c.0_i.c.1_g.c.1');
|
|
const restored = makeManager(`#v=2&lat=10&lon=20&ui=${params.get('ui')}`)
|
|
.parseInitialHash();
|
|
assert.deepEqual(restored.panelState, { specs: [
|
|
{ id: 'cctv-panel', collapsed: false, pinned: null },
|
|
{ id: 'recent-imagery-panel', collapsed: true, pinned: null },
|
|
{ id: 'global-context-panel', collapsed: true, pinned: null },
|
|
] });
|
|
// A link written before the token existed decodes exactly as it did.
|
|
const older = makeManager('#v=2&lat=10&lon=20&ui=v.c.1_g.c.0').parseInitialHash();
|
|
assert.deepEqual(older.panelState, { specs: [
|
|
{ id: 'cctv-panel', collapsed: true, pinned: null },
|
|
{ id: 'global-context-panel', collapsed: false, pinned: null },
|
|
] });
|
|
});
|
|
|
|
test('a collapsed Street Level panel survives the share-link round trip', () => {
|
|
const manager = makeManager();
|
|
manager.setPanelStateProvider(() => ({ specs: [
|
|
{ id: 'cctv-panel', collapsed: false },
|
|
{ id: 'street-level-panel', collapsed: true },
|
|
] }));
|
|
clearTimeout(manager._debounceTimer);
|
|
manager._updateHash();
|
|
const params = new URLSearchParams(window.location.hash.slice(1));
|
|
assert.equal(params.get('ui'), 'v.c.0_t.c.1');
|
|
const restored = makeManager(`#v=2&lat=10&lon=20&ui=${params.get('ui')}`)
|
|
.parseInitialHash();
|
|
assert.deepEqual(restored.panelState, { specs: [
|
|
{ id: 'cctv-panel', collapsed: false, pinned: null },
|
|
{ id: 'street-level-panel', collapsed: true, pinned: null },
|
|
] });
|
|
});
|
|
|
|
test('camera-only, partial, and malformed panel shares remain valid incoming state', () => {
|
|
const cameraOnly = makeManager('#lat=10&lon=20').parseInitialHash();
|
|
assert.ok(cameraOnly);
|
|
assert.equal(cameraOnly.panelState, null);
|
|
|
|
const partial = makeManager('#v=2&lat=10&lon=20&ui=d.c.0').parseInitialHash();
|
|
assert.deepEqual(partial.panelState, { specs: [
|
|
{ id: 'data-panel', collapsed: false, pinned: null },
|
|
] });
|
|
|
|
for (const hash of [
|
|
'#v=2&lat=10&lon=20&ui=',
|
|
'#v=2&lat=10&lon=20&ui=unknown.c.1',
|
|
'#v=2&lat=10&lon=20&ui=d.c.maybe',
|
|
]) {
|
|
const malformed = makeManager(hash).parseInitialHash();
|
|
assert.ok(malformed);
|
|
assert.equal(malformed.panelState, null);
|
|
}
|
|
});
|
|
|
|
// Both `bing-road` and the `k` panel token belonged to the retired left Map
|
|
// Stack panel. Nothing is owed to a link that carried them — no build with
|
|
// either one ever shipped publicly — so the parser no longer knows them, and
|
|
// each takes the ordinary unknown path: an unrecognized panel token is skipped,
|
|
// and an unrecognized stack id lands on the controller's photoreal fallback
|
|
// (pinned live in `scripts/qa-map-source-tray.mjs`). The camera half of such a
|
|
// link must still restore.
|
|
test('a retired-vocabulary link degrades to the unknown paths instead of failing', () => {
|
|
const parsed = makeManager('#v=2&lat=10&lon=20&map=bing-road&ui=k.c.0').parseInitialHash();
|
|
assert.equal(parsed.lat, 10);
|
|
assert.equal(parsed.lon, 20);
|
|
assert.equal(parsed.panelState, null);
|
|
});
|
|
|
|
test('non-finite camera coordinates fail closed without reserving restoration', () => {
|
|
for (const hash of [
|
|
'#lat=Infinity&lon=20',
|
|
'#lat=10&lon=-Infinity',
|
|
'#lat=1e309&lon=20',
|
|
'#v=2&lat=%2BInfinity&lon=20',
|
|
]) {
|
|
const manager = makeManager(hash);
|
|
assert.equal(manager.parseInitialHash(), null, hash);
|
|
assert.equal(manager._initialRestorePending, false, hash);
|
|
}
|
|
|
|
assert.ok(makeManager('#lat=10&lon=20').parseInitialHash());
|
|
assert.ok(makeManager('#v=2&lat=-10.5&lon=20.25').parseInitialHash());
|
|
});
|
|
|
|
test('incoming state suppresses premature hash replacement until restoration', () => {
|
|
const manager = makeManager('#v=2&lat=10&lon=20&l=e&style=nvg');
|
|
manager.parseInitialHash();
|
|
manager._updateHash();
|
|
assert.equal(window.location.hash, '#v=2&lat=10&lon=20&l=e&style=nvg');
|
|
});
|
|
|
|
test('a shared view reserves its own camera without cancelling its saved Follow', () => {
|
|
assert.match(
|
|
uiSource,
|
|
/_beginDeferredNavigation\(\s*'shared view',\s*\{\s*cancelPendingSelection: false,?\s*\},?\s*\)/,
|
|
);
|
|
const deferred = sourceBlock(
|
|
" _beginDeferredNavigation(noun = 'location', { cancelPendingSelection = true } = {}) {",
|
|
' /** Final authority check and release immediately before a delayed flight. */',
|
|
);
|
|
// The shared view's own `cancelPendingSelection` must reach the stamp; other
|
|
// stamp options may ride alongside it.
|
|
assert.match(deferred, /_stampNavigation\(\{\s*cancelPendingSelection[^)]*\}\)/);
|
|
});
|
|
|
|
test('copy timestamp parsing is strict and rejects malformed or future values', () => {
|
|
const nowMs = 2_000_000;
|
|
assert.equal(decodeShareCreatedAtMs(new URLSearchParams('at=1999'), { nowMs }), 1_999_000);
|
|
for (const raw of ['', '0', '-1', '1.5', 'abc', '001', '9007199254740992']) {
|
|
assert.equal(
|
|
decodeShareCreatedAtMs(new URLSearchParams(`at=${encodeURIComponent(raw)}`), { nowMs }),
|
|
null,
|
|
raw,
|
|
);
|
|
}
|
|
assert.equal(decodeShareCreatedAtMs(new URLSearchParams('at=2001'), { nowMs }), null);
|
|
});
|
|
|
|
test('copy adds a fresh ephemeral timestamp without aging the live URL', async () => {
|
|
const copied = [];
|
|
installClipboard(async (url) => { copied.push(url); });
|
|
const manager = makeManager();
|
|
manager._updateHash();
|
|
const liveHash = window.location.hash;
|
|
|
|
assert.equal(await manager.copyLink({ nowMs: 2_000_000 }), true);
|
|
assert.equal(await manager.copyLink({ nowMs: 2_002_000 }), true);
|
|
assert.equal(new URL(copied[0]).hash.includes('at=2000'), true);
|
|
assert.equal(new URL(copied[1]).hash.includes('at=2002'), true);
|
|
assert.equal(window.location.hash, liveHash);
|
|
assert.equal(new URLSearchParams(window.location.hash.slice(1)).has('at'), false);
|
|
});
|
|
|
|
test('copy snapshots current state while incoming hash writes are still suppressed', async () => {
|
|
let copied = null;
|
|
installClipboard(async (url) => { copied = url; });
|
|
const manager = makeManager('#v=2&lat=10&lon=20&l=e&at=100');
|
|
manager.parseInitialHash();
|
|
assert.equal(manager._initialRestorePending, true);
|
|
assert.equal(await manager.copyLink({ nowMs: 3_000_000 }), true);
|
|
const params = new URL(copied).hash.slice(1);
|
|
assert.equal(new URLSearchParams(params).get('at'), '3000');
|
|
assert.equal(window.location.hash, '#v=2&lat=10&lon=20&l=e&at=100');
|
|
});
|
|
|
|
test('clipboard rejection leaves both live URL and restore suppression untouched', async () => {
|
|
installClipboard(async () => { throw new Error('denied'); });
|
|
const manager = makeManager('#v=2&lat=10&lon=20&l=s');
|
|
manager.parseInitialHash();
|
|
assert.equal(await manager.copyLink({ nowMs: 4_000_000 }), false);
|
|
assert.equal(window.location.hash, '#v=2&lat=10&lon=20&l=s');
|
|
assert.equal(manager._initialRestorePending, true);
|
|
});
|
|
|
|
test('legacy Panoptic and Sparse hashes migrate to canonical profiles', () => {
|
|
const panoptic = makeManager('#lat=10&lon=20&dm=PANOPTIC&dd=0').parseInitialHash();
|
|
assert.equal(panoptic.detectionMode, 'DENSE');
|
|
assert.equal(panoptic.detectionDensity, 75);
|
|
const sparse = makeManager('#lat=10&lon=20&dm=SPARSE&dd=100').parseInitialHash();
|
|
assert.equal(sparse.detectionMode, 'SPARSE');
|
|
assert.equal(sparse.detectionDensity, 25);
|
|
});
|
|
|
|
test('allocation strategy defaults to Elastic and round-trips Weighted', () => {
|
|
assert.equal(makeManager('#lat=10&lon=20').parseInitialHash().detectionAllocation, 'ELASTIC');
|
|
assert.equal(makeManager('#lat=10&lon=20&da=weighted').parseInitialHash().detectionAllocation, 'WEIGHTED');
|
|
|
|
const manager = makeManager();
|
|
manager.onToggleChange(false, false, { detectionAllocation: 'WEIGHTED' });
|
|
clearTimeout(manager._debounceTimer);
|
|
manager._updateHash();
|
|
assert.equal(new URLSearchParams(window.location.hash.slice(1)).get('da'), 'weighted');
|
|
});
|
|
|
|
test('keyhole fade controls default and round-trip as normalized percentages', () => {
|
|
const defaults = makeManager('#lat=10&lon=20').parseInitialHash();
|
|
assert.equal(defaults.detectionFadePct, 16);
|
|
assert.equal(defaults.detectionOutsideOpacityPct, 5);
|
|
|
|
const restored = makeManager('#lat=10&lon=20&kf=28&ko=35').parseInitialHash();
|
|
assert.equal(restored.detectionFadePct, 28);
|
|
assert.equal(restored.detectionOutsideOpacityPct, 35);
|
|
|
|
const manager = makeManager();
|
|
manager.onToggleChange(false, false, {
|
|
detectionFadePct: 22,
|
|
detectionOutsideOpacityPct: 30,
|
|
});
|
|
clearTimeout(manager._debounceTimer);
|
|
manager._updateHash();
|
|
const params = new URLSearchParams(window.location.hash.slice(1));
|
|
assert.equal(params.get('kf'), '22');
|
|
assert.equal(params.get('ko'), '30');
|
|
});
|
|
|
|
// ── `sce` is a BAND, not a free number (review round 2) ───────────────────────
|
|
//
|
|
// The terminus is documented and supported as 94..100. Parsing clamped to
|
|
// 0..100, so `sce=0` produced an unsupported sub-94 terminus — a hole in the
|
|
// mask, not a scope — and the next hash write serialized it straight back out.
|
|
|
|
test('sce is clamped into the supported 94..100 band on the way in', () => {
|
|
assert.equal(makeManager('#lat=10&lon=20&sce=97').parseInitialHash().scopeTerminusPct, 97);
|
|
assert.equal(makeManager('#lat=10&lon=20&sce=94').parseInitialHash().scopeTerminusPct, 94);
|
|
assert.equal(makeManager('#lat=10&lon=20&sce=100').parseInitialHash().scopeTerminusPct, 100);
|
|
assert.equal(makeManager('#lat=10&lon=20&sce=0').parseInitialHash().scopeTerminusPct, 94,
|
|
'sce=0 must not create a sub-94 terminus');
|
|
assert.equal(makeManager('#lat=10&lon=20&sce=93').parseInitialHash().scopeTerminusPct, 94);
|
|
assert.equal(makeManager('#lat=10&lon=20&sce=-40').parseInitialHash().scopeTerminusPct, 94);
|
|
assert.equal(makeManager('#lat=10&lon=20&sce=500').parseInitialHash().scopeTerminusPct, 100);
|
|
assert.equal(makeManager('#lat=10&lon=20&sce=96.6').parseInitialHash().scopeTerminusPct, 97,
|
|
'fractional percents round into the band');
|
|
});
|
|
|
|
test('an absent or non-numeric sce stays adaptive, never a pinned value', () => {
|
|
assert.equal(makeManager('#lat=10&lon=20').parseInitialHash().scopeTerminusPct, null);
|
|
assert.equal(makeManager('#lat=10&lon=20&sce=abc').parseInitialHash().scopeTerminusPct, null,
|
|
'junk is not a pin — absent semantics win');
|
|
assert.equal(makeManager('#lat=10&lon=20&sce=').parseInitialHash().scopeTerminusPct, null);
|
|
});
|
|
|
|
test('serialization writes only in-band sce values, and omits an adaptive one', () => {
|
|
const manager = makeManager();
|
|
manager.onToggleChange(false, false, { scopeTerminusPct: 0 });
|
|
clearTimeout(manager._debounceTimer);
|
|
manager._updateHash();
|
|
assert.equal(new URLSearchParams(window.location.hash.slice(1)).get('sce'), '94',
|
|
'an out-of-band value must be floored on write, not round-tripped');
|
|
|
|
manager.onToggleChange(false, false, { scopeTerminusPct: 500 });
|
|
clearTimeout(manager._debounceTimer);
|
|
manager._updateHash();
|
|
assert.equal(new URLSearchParams(window.location.hash.slice(1)).get('sce'), '100');
|
|
|
|
manager.onToggleChange(false, false, { scopeTerminusPct: null });
|
|
clearTimeout(manager._debounceTimer);
|
|
manager._updateHash();
|
|
assert.equal(new URLSearchParams(window.location.hash.slice(1)).has('sce'), false,
|
|
'adaptive stays ABSENT so a shared link never freezes the ramp');
|
|
});
|
|
|
|
test('share-link restore forces a final stationary render for Google 3D Tiles', () => {
|
|
const calls = { flyTo: null, setView: null, renders: 0 };
|
|
const viewer = {
|
|
camera: {
|
|
changed: { addEventListener() {} },
|
|
positionCartographic: { latitude: 0, longitude: 0, height: 1000 },
|
|
heading: 0,
|
|
pitch: -Math.PI / 2,
|
|
roll: 0,
|
|
flyTo(options) { calls.flyTo = options; },
|
|
setView(options) { calls.setView = options; },
|
|
},
|
|
scene: { requestRender() { calls.renders += 1; } },
|
|
};
|
|
const manager = new ShareLinkManager(viewer);
|
|
manager.applyState({
|
|
lat: 40.7669,
|
|
lon: -73.9909,
|
|
alt: 396,
|
|
heading: 206,
|
|
pitch: -22,
|
|
roll: 0,
|
|
});
|
|
|
|
assert.ok(calls.flyTo, 'restore must start a camera flight');
|
|
assert.equal(typeof calls.flyTo.complete, 'function');
|
|
calls.flyTo.complete();
|
|
assert.deepEqual(calls.setView, {
|
|
destination: calls.flyTo.destination,
|
|
orientation: calls.flyTo.orientation,
|
|
});
|
|
assert.equal(calls.renders, 1);
|
|
});
|
|
|
|
test('newer navigation suppresses delayed share camera while non-camera state still restores', async () => {
|
|
let flights = 0;
|
|
let restored = null;
|
|
const viewer = {
|
|
camera: {
|
|
changed: { addEventListener: () => () => {} },
|
|
flyTo() { flights += 1; },
|
|
},
|
|
};
|
|
const manager = new ShareLinkManager(viewer, {
|
|
onRestore: (state) => { restored = state; },
|
|
isNavigationCurrent: () => false,
|
|
});
|
|
const applied = await manager.applyState({
|
|
lat: 40, lon: -74, alt: 500, heading: 0, pitch: -30, roll: 0,
|
|
style: 'thermal', panelState: { specs: [] },
|
|
}, { navigationToken: 4 });
|
|
assert.equal(applied.succeeded, true);
|
|
assert.equal(flights, 0);
|
|
assert.equal(restored.style, 'thermal');
|
|
});
|
|
|
|
test('newer visual, map, and individual panel actions suppress only their owned restore lanes', async () => {
|
|
let restored = null;
|
|
const manager = makeManager(
|
|
'#v=2&lat=40&lon=-74&style=flir&map=osm&ui=c.c.0_d.c.0',
|
|
);
|
|
manager._onRestore = (state) => { restored = state; };
|
|
manager._isNavigationCurrent = () => false;
|
|
const state = manager.parseInitialHash();
|
|
|
|
manager.claimRestoreLane('visual');
|
|
manager.claimRestoreLane('map');
|
|
manager.claimRestoreLane('panel', 'control-panel');
|
|
const result = await manager.applyState(state, { navigationToken: 1 });
|
|
|
|
assert.equal(restored.style, undefined);
|
|
assert.equal(restored.mapStack, undefined);
|
|
assert.deepEqual(restored.panelState, {
|
|
specs: [{ id: 'data-panel', collapsed: false, pinned: null }],
|
|
});
|
|
assert.equal(result.visual, 'superseded');
|
|
assert.equal(result.map, 'superseded');
|
|
assert.equal(result.panels, 'applied');
|
|
assert.equal(manager._initialRestorePending, true);
|
|
manager.completeInitialRestore();
|
|
assert.equal(manager._initialRestorePending, false);
|
|
});
|
|
|
|
test('every explicit visual UI gesture claims restore authority before it mutates state', () => {
|
|
const initUi = sourceBlock(' _initUI() {', ' _initMapStackControl() {');
|
|
const gestureRoutes = [
|
|
['toggleHud: () => {', 'toggleOrbit: () =>', 'this.hud.toggle()', 'HUD hotkey'],
|
|
['cycleDetection: () => {', 'toggleCctv: () =>', 'cycleDetectionMode()', 'detection hotkey'],
|
|
['toggleBloom:', 'setBloomIntensity:', 'this._setBloomEnabled(', 'toggleBloom control'],
|
|
['setBloomIntensity:', 'toggleSharpen:', 'this._setBloomIntensity(', 'setBloomIntensity control'],
|
|
['toggleSharpen:', 'toggleScope:', 'this._setSharpenEnabled(', 'toggleSharpen control'],
|
|
['toggleScope:', 'setScopeFeather:', 'setScopeMaskEnabled(', 'toggleScope control'],
|
|
['setScopeFeather:', 'setSharpenIntensity:', 'setScopeMaskFeather(', 'setScopeFeather control'],
|
|
['setSharpenIntensity:', 'setHudLayout:', 'this._applySharpenIntensity(', 'setSharpenIntensity control'],
|
|
['setHudLayout:', 'toggleCleanView:', 'this._setHudVariant(', 'setHudLayout control'],
|
|
['setDensity:', 'setAllocation:', 'this._applyDetectionDensityFromUi()', 'setDensity control'],
|
|
['setAllocation:', 'setFade:', 'this._setDetectionAllocation(', 'setAllocation control'],
|
|
['setFade:', 'toggleCelestial:', 'this._applyDetectionFadeFromUi()', 'setFade control'],
|
|
];
|
|
for (const [start, end, mutation, label] of gestureRoutes) {
|
|
const startIndex = initUi.indexOf(start);
|
|
const endIndex = initUi.indexOf(end, startIndex + start.length);
|
|
assert.ok(startIndex >= 0 && endIndex > startIndex, `${label} route is missing`);
|
|
assertClaimsBefore(initUi.slice(startIndex, endIndex), mutation, label);
|
|
}
|
|
|
|
const displayActions = initUi.slice(initUi.indexOf('this._displayControls ='));
|
|
assertClaimsBefore(displayActions.slice(displayActions.indexOf('toggleHud:'), displayActions.indexOf('cycleDetection:')), 'this.hud.toggle()', 'HUD button');
|
|
assertClaimsBefore(displayActions.slice(displayActions.indexOf('cycleDetection:'), displayActions.indexOf('toggleModels:')), 'cycleDetectionMode()', 'detection button');
|
|
|
|
});
|
|
|
|
// Contacts OWNS detection while it is active (forced Dense @ 75%). That makes
|
|
// an explicit Context transition a visual-lane gesture exactly like the HUD or
|
|
// detection controls: without a claim, the share restore that lands 1.5 s into
|
|
// startup re-applies the link's `dm`/`dd` straight over the forced preset and
|
|
// Contacts silently loses its own overlay. The sweep above enumerates routes
|
|
// explicitly, so a missing Context route was simply invisible to it.
|
|
test('explicit Context transitions claim the visual restore lane before transitioning', () => {
|
|
// The named helper IS the claim; its body is pinned to the real lane call
|
|
// immediately below, so routes may use either spelling.
|
|
const assertContextClaimsBefore = (block, mutation, label) => {
|
|
const claimIndex = Math.min(
|
|
...['_claimContextVisualAuthority()', "claimRestoreLane?.('visual')"]
|
|
.map((marker) => block.indexOf(marker))
|
|
.filter((index) => index >= 0),
|
|
);
|
|
const mutationIndex = block.indexOf(mutation);
|
|
assert.ok(Number.isFinite(claimIndex), `${label} must claim the visual restore lane`);
|
|
assert.ok(mutationIndex >= 0, `${label} mutation marker is missing`);
|
|
assert.ok(claimIndex < mutationIndex, `${label} must claim before mutation`);
|
|
};
|
|
|
|
const helper = _claimContextVisualAuthority.toString();
|
|
assert.ok(
|
|
helper.includes('this.actions.claimVisualAuthority()'),
|
|
'the Context authority helper must claim the visual lane',
|
|
);
|
|
|
|
assert.match(uiSource, /claimVisualAuthority: \(\) =>\s*this\.shareLinkManager\?\.claimRestoreLane\?\.\('visual'\)/);
|
|
const contextPanel = _initGlobalContextPanel.toString();
|
|
for (const [start, end, label] of [
|
|
["this.listen(this._globalContextFlightsBtn, 'click'", "this.listen(this._globalContextMissionsBtn, 'click'", 'Contacts tab'],
|
|
["this.listen(this._globalContextMissionsBtn, 'click'", 'CONTEXT_PANEL_END', 'Space Missions tab'],
|
|
]) {
|
|
const startIndex = contextPanel.indexOf(start);
|
|
const endIndex = end === 'CONTEXT_PANEL_END'
|
|
? contextPanel.length
|
|
: contextPanel.indexOf(end, startIndex + start.length);
|
|
assert.ok(startIndex >= 0 && endIndex > startIndex, `${label} route is missing`);
|
|
assertContextClaimsBefore(contextPanel.slice(startIndex, endIndex), 'this._selectContextMode(', label);
|
|
}
|
|
|
|
// The voice/tool facade validates the mode first, then transitions.
|
|
const facade = setContextMode.toString();
|
|
assertContextClaimsBefore(facade, 'this._selectContextMode(', 'setContextMode facade');
|
|
// Authority is taken per validated branch, never ahead of validation. The
|
|
// OFF branch is validated by its own guard; the named-mode branch must claim
|
|
// only AFTER the unknown-mode rejection, so a rejected request takes nothing.
|
|
const offGuardIndex = facade.indexOf("if (!mode || mode === 'off')");
|
|
const rejectIndex = facade.indexOf('Unknown context mode');
|
|
assert.ok(offGuardIndex >= 0, 'setContextMode must keep its OFF guard');
|
|
assert.ok(rejectIndex > offGuardIndex, 'setContextMode must still reject unknown modes');
|
|
|
|
const offBranchClaim = facade.indexOf('_claimContextVisualAuthority()', offGuardIndex);
|
|
assert.ok(
|
|
offBranchClaim > offGuardIndex && offBranchClaim < rejectIndex,
|
|
'the OFF transition must claim inside its own validated branch',
|
|
);
|
|
const namedBranchClaim = facade.indexOf('_claimContextVisualAuthority()', rejectIndex);
|
|
assert.ok(
|
|
namedBranchClaim > rejectIndex,
|
|
'a named Context mode must claim only after the unknown-mode rejection',
|
|
);
|
|
assert.ok(
|
|
namedBranchClaim < facade.indexOf('this._selectContextMode(', rejectIndex),
|
|
'the named-mode claim must precede its transition',
|
|
);
|
|
});
|
|
|
|
// Claiming the lane must NOT masquerade as the operator hand-editing
|
|
// detection: `_detectionUserOverridden` is what suppresses the military-style
|
|
// auto-enable for the rest of the session. Contacts entry is not that.
|
|
test('Context lane claims never set the session detection-override flag', () => {
|
|
const contextPanel = _initGlobalContextPanel.toString();
|
|
const facade = setContextMode.toString();
|
|
for (const [block, label] of [
|
|
[contextPanel, 'Context panel'],
|
|
[facade, 'setContextMode facade'],
|
|
]) {
|
|
assert.ok(
|
|
!block.includes('_detectionUserOverridden = true'),
|
|
`${label} must not flag the operator as having overridden detection`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('every explicit visual control facade claims restore authority before mutation', () => {
|
|
const facadeRoutes = [
|
|
[' setHudVisible(mode) {', ' setHudLayout(variantName) {', 'this.hud.setMode(', 'setHudVisible'],
|
|
[' setHudLayout(variantName) {', ' getDetectionState() {', 'this._setHudVariant(', 'setHudLayout'],
|
|
[' setDetection({ enabled, mode, densityPct, allocationStrategy, fadePct, outsideOpacityPct } = {}) {', ' async setMapStack(stackId) {', 'this._setDetectionAllocation(', 'setDetection'],
|
|
[' setBloom({ enabled, intensityPct } = {}) {', ' setSharpen({ enabled, intensityPct } = {}) {', 'this._setBloomIntensity(', 'setBloom'],
|
|
[' setSharpen({ enabled, intensityPct } = {}) {', ' get celestialRingEnabled() {', 'this._applySharpenIntensity(', 'setSharpen'],
|
|
[' setCelestialRingEnabled(enabled, { syncShare = true, focus = false } = {}) {', ' setOrbit(enabled) {', 'this.celestialRing?.setEnabled(', 'setCelestialRingEnabled'],
|
|
];
|
|
for (const [start, end, mutation, label] of facadeRoutes) {
|
|
assertClaimsBefore(sourceBlock(start, end), mutation, label);
|
|
}
|
|
|
|
const style = sourceBlock(' setStyle(styleName, {', ' _startTransition(styleName, fromValue, toValue) {');
|
|
assertClaimsBefore(style, 'this.activeStyle = styleName', 'setStyle');
|
|
const sliders = sourceBlock(' _updateSliderPanel(styleName, { reveal = false } = {}) {', ' _revealStyleParameters() {');
|
|
assertClaimsBefore(sliders, 'this.stages[styleName].uniforms[uName] = val', 'style parameter slider');
|
|
});
|
|
|
|
test('public visual facades reject the complete invalid request before authority or mutation', () => {
|
|
const cases = [
|
|
{
|
|
label: 'setDetection',
|
|
block: sourceBlock(
|
|
' setDetection({ enabled, mode, densityPct, allocationStrategy, fadePct, outsideOpacityPct } = {}) {',
|
|
' async setMapStack(stackId) {',
|
|
),
|
|
validations: ['enabled !== undefined', 'Invalid outside opacity'],
|
|
},
|
|
{
|
|
label: 'setBloom',
|
|
block: sourceBlock(' setBloom({ enabled, intensityPct } = {}) {', ' setSharpen({ enabled, intensityPct } = {}) {'),
|
|
validations: ['Invalid bloom enabled value', 'Invalid bloom intensity'],
|
|
},
|
|
{
|
|
label: 'setSharpen',
|
|
block: sourceBlock(' setSharpen({ enabled, intensityPct } = {}) {', ' get celestialRingEnabled() {'),
|
|
validations: ['Invalid sharpen enabled value', 'Invalid sharpen intensity'],
|
|
},
|
|
{
|
|
label: 'setCelestialRingEnabled',
|
|
block: sourceBlock(
|
|
' setCelestialRingEnabled(enabled, { syncShare = true, focus = false } = {}) {',
|
|
' setOrbit(enabled) {',
|
|
),
|
|
validations: [
|
|
'Invalid celestial ring enabled value',
|
|
'Celestial ring options must be boolean',
|
|
'Celestial ring is available only in Normal style',
|
|
],
|
|
},
|
|
];
|
|
for (const { label, block, validations } of cases) {
|
|
const claimIndex = block.indexOf("claimRestoreLane?.('visual')");
|
|
assert.ok(claimIndex >= 0, `${label} claim is missing`);
|
|
for (const validation of validations) {
|
|
const validationIndex = block.indexOf(validation);
|
|
assert.ok(validationIndex >= 0, `${label} validation is missing: ${validation}`);
|
|
assert.ok(validationIndex < claimIndex, `${label} must validate ${validation} before claiming`);
|
|
}
|
|
}
|
|
assert.doesNotMatch(cases.at(-1).block, /!!enabled/);
|
|
});
|
|
|
|
test('share apply completion waits for both callback work and camera settlement', async () => {
|
|
let flight = null;
|
|
let releaseRestore;
|
|
const restoreGate = new Promise((resolve) => { releaseRestore = resolve; });
|
|
const viewer = {
|
|
camera: {
|
|
changed: { addEventListener: () => () => {} },
|
|
flyTo(options) { flight = options; },
|
|
setView() {},
|
|
},
|
|
scene: { requestRender() {} },
|
|
};
|
|
const manager = new ShareLinkManager(viewer, { onRestore: () => restoreGate });
|
|
let settled = false;
|
|
const applying = manager.applyState({
|
|
lat: 40, lon: -74, alt: 500, heading: 0, pitch: -30, roll: 0,
|
|
}).then((result) => { settled = true; return result; });
|
|
|
|
await Promise.resolve();
|
|
assert.equal(settled, false);
|
|
releaseRestore();
|
|
await Promise.resolve();
|
|
assert.equal(settled, false);
|
|
flight.complete();
|
|
const result = await applying;
|
|
assert.equal(result.camera, 'applied');
|
|
assert.equal(settled, true);
|
|
});
|
|
|
|
test('a later navigation prevents share completion from resetting the final pose', () => {
|
|
let generation = 3;
|
|
let flight = null;
|
|
let setViews = 0;
|
|
const viewer = {
|
|
camera: {
|
|
changed: { addEventListener: () => () => {} },
|
|
flyTo(options) { flight = options; },
|
|
setView() { setViews += 1; },
|
|
},
|
|
scene: { requestRender() {} },
|
|
};
|
|
const manager = new ShareLinkManager(viewer, {
|
|
isNavigationCurrent: (token) => token === generation,
|
|
});
|
|
manager.applyState({
|
|
lat: 40, lon: -74, alt: 500, heading: 0, pitch: -30, roll: 0,
|
|
}, { navigationToken: 3 });
|
|
generation = 4;
|
|
flight.complete();
|
|
assert.equal(setViews, 0);
|
|
});
|
|
|
|
test('destroy cancels only a still-owned share flight and ignores delayed completion', () => {
|
|
let generation = 7;
|
|
let flight = null;
|
|
let cancellations = 0;
|
|
let setViews = 0;
|
|
const viewer = {
|
|
camera: {
|
|
changed: { addEventListener: () => () => {} },
|
|
flyTo(options) { flight = options; },
|
|
setView() { setViews += 1; },
|
|
},
|
|
scene: { requestRender() {} },
|
|
};
|
|
const manager = new ShareLinkManager(viewer, {
|
|
isNavigationCurrent: (token) => token === generation,
|
|
cancelOwnedNavigation: () => { cancellations += 1; flight?.cancel?.(); },
|
|
});
|
|
manager.applyState({
|
|
lat: 40, lon: -74, alt: 500, heading: 0, pitch: -30, roll: 0,
|
|
}, { navigationToken: 7 });
|
|
manager.destroy();
|
|
flight.complete();
|
|
assert.equal(cancellations, 1);
|
|
assert.equal(setViews, 0);
|
|
|
|
const newerManager = new ShareLinkManager(viewer, {
|
|
isNavigationCurrent: (token) => token === generation,
|
|
cancelOwnedNavigation: () => { cancellations += 1; },
|
|
});
|
|
newerManager.applyState({
|
|
lat: 40, lon: -74, alt: 500, heading: 0, pitch: -30, roll: 0,
|
|
}, { navigationToken: 7 });
|
|
generation = 8;
|
|
newerManager.destroy();
|
|
assert.equal(cancellations, 1, 'newer navigation must not be cancelled');
|
|
});
|
|
|
|
|
|
test('visual input listeners are revoked before asynchronous UI teardown', () => {
|
|
const disposal = uiSource.slice(uiSource.indexOf(' async dispose() {'));
|
|
const firstAwait = disposal.indexOf('await ');
|
|
assert.ok(firstAwait > 0);
|
|
const synchronous = disposal.slice(0, firstAwait);
|
|
assert.match(synchronous, /this\._displayBindings\.destroy\(\)/);
|
|
assert.match(synchronous, /this\._visualSettings\.stop\(\)/);
|
|
assert.match(synchronous, /this\._mapSourceControls\?\.destroy\(\)/);
|
|
assert.match(synchronous, /this\._clearLayersControl\?\.destroy\(\)/);
|
|
assert.match(synchronous, /this\._locationNavigation\.destroy\(\)/);
|
|
assert.match(LocationNavigation.prototype.destroy.toString(), /this\._locationControls\?\.destroy\(\)/);
|
|
assert.match(LocationNavigation.prototype.destroy.toString(), /this\._locationLookup\?\.destroy\(\)/);
|
|
assert.match(DisplayBindings.prototype.destroy.toString(), /this\._displayControls\?\.destroy\(\)/);
|
|
assert.match(DisplayBindings.prototype.destroy.toString(), /this\._applicationShortcuts\?\.destroy\(\)/);
|
|
assert.match(VisualSettings.prototype.stop.toString(), /this\._styleParameters\?\.destroy\(\)/);
|
|
assert.match(VisualSettings.prototype.stop.toString(), /this\._visualEffects\.stop\(\)/);
|
|
});
|