mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
The scene chrome, cockpit, Display controls, command dock, layer panels, Context rail, welcome card and Provider Settings move verbatim into src/ui/views and mount into their hosts before the page is translated and wired. Each view renders the same elements, attributes and text as its template; Vue keeps whitespace between elements as one space, and no view markup sits under a white-space: pre rule. Template asset URLs stay literal, view files are left out of Prettier so the markup is not reflowed, and the Node component loader moves to build/ and is exported as build/register-vue for host test runners. Hosts that do not initialize Core's locale pass localePicker: false instead of editing the markup.
298 lines
16 KiB
JavaScript
298 lines
16 KiB
JavaScript
import { readShellSource } from './testSupport/readShellSource.mjs';
|
|
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
|
|
// src/reasonableDefaults.test.mjs
|
|
//
|
|
// What the console looks like the FIRST time it opens — before any share link,
|
|
// before any stored state. The "reasonable defaults" batch (owner directive,
|
|
// 2026-08-22) moved three of them together:
|
|
//
|
|
// 1. 3D aircraft models ON, mode `proximity`.
|
|
// Pinned in `data/layerState.test.mjs`, next to the coordinator that
|
|
// actually decides fresh-boot layer state — including the early return that
|
|
// makes each layer's own initializer the operative default.
|
|
// 2. Scope feather. Owner: "I like to hide feather" set it to 0% on
|
|
// 2026-08-22; later revisions were 8%, then 11%, then 100% on
|
|
// 2026-10-25. Scope stays ON on a fresh boot. The hard crop remains available.
|
|
// 3. Detection ON (Dense @ 75%) for EVERY style, Normal included.
|
|
// Owner: "detect should also be on by default."
|
|
// 4. Detection OUTSIDE opacity 1% (owner final lock, 2026-08-24; 3% on 08-23, 5% before), with
|
|
// the slider's `step` at 1 so the range around it is reachable at all.
|
|
//
|
|
// Each pin below has the same three parts, because a default is never one
|
|
// literal:
|
|
//
|
|
// • the first-run VALUE, at every surface that independently decides it — a
|
|
// fresh boot runs no restore, so these literals ARE the startup state and
|
|
// changing one alone ships a UI that disagrees with its own engine;
|
|
// • explicit state still WINS over it — a link, or the operator's own hand,
|
|
// because "default" means "what you get when you said nothing";
|
|
// • the surrounding override machinery is INTACT, so a default flip cannot
|
|
// quietly take a separate landed behaviour with it.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
|
|
import { KEYHOLE_OUTER_RADIUS, KEYHOLE_OUTSIDE_OPACITY_DEFAULT, KEYHOLE_LABEL_FEATHER_RATIO } from './celestialRing.js';
|
|
import { AIRCRAFT_BRACKET_FLOOR_ANCHOR } from './data/detectionPolicy.js';
|
|
import {
|
|
SCOPE_FEATHER_RATIO_DEFAULT,
|
|
getScopeMaskFeather,
|
|
isScopeMaskEnabled,
|
|
scopeMaskGeometry,
|
|
setScopeMaskFeather,
|
|
} from './scopeMask.js';
|
|
import { ShareLinkManager } from './sharelink.js';
|
|
|
|
// Follow the UI wiring and its extracted preset definitions.
|
|
const uiSource = readShellSource()
|
|
+ '\n' + fs.readFileSync(new URL('./ui/visualPresets.js', import.meta.url), 'utf8');
|
|
const indexHtml = await renderApplicationHtml(fs.readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
|
const shareSource = fs.readFileSync(new URL('./sharelink.js', import.meta.url), 'utf8');
|
|
|
|
/** Slice ui.js between two literal anchors, so a pin reads one method, not the file. */
|
|
function uiBlock(start, end) {
|
|
const startIndex = uiSource.indexOf(start);
|
|
assert.ok(startIndex >= 0, `missing source anchor: ${start}`);
|
|
const endIndex = uiSource.indexOf(end, startIndex + start.length);
|
|
assert.ok(endIndex > startIndex, `missing source anchor: ${end}`);
|
|
return uiSource.slice(startIndex, endIndex);
|
|
}
|
|
|
|
/** A ShareLinkManager over a synthetic hash — enough surface for parseInitialHash. */
|
|
function managerForHash(hash) {
|
|
globalThis.window = { location: { hash, href: `http://localhost/${hash}` } };
|
|
globalThis.history = { replaceState(_s, _t, 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);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 2. Scope ON with 100% feather on a first run
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('first run opens with Scope ON and 100% feather at every default surface', () => {
|
|
assert.equal(isScopeMaskEnabled(), true, 'the live mask starts enabled');
|
|
assert.equal(SCOPE_FEATHER_RATIO_DEFAULT, 1, 'owner preference 2026-10-25');
|
|
assert.equal(getScopeMaskFeather(), 1,
|
|
'and the live module starts there, not merely documents it');
|
|
|
|
// The slider and its readout are the same default rendered as markup — a
|
|
// fresh boot applies no restore, so a stale value here would show one number
|
|
// over a mask drawn at another.
|
|
assert.match(indexHtml, /id="scope-feather-slider"[^>]*\svalue="100"/,
|
|
'index.html: the feather slider ships at 100');
|
|
assert.match(indexHtml, /id="scope-feather-value"[^>]*>100%</,
|
|
'index.html: and its readout agrees with the handle');
|
|
|
|
assert.match(indexHtml, /<input type="checkbox" id="scope-toggle"[^>]*\schecked\s*\/?>/,
|
|
'the native Scope checkbox starts checked');
|
|
|
|
// The link this session generates must describe the mask this session draws,
|
|
// for the window before the first _syncShareState.
|
|
assert.match(shareSource, /this\._scopeFeatherPct = 100;/,
|
|
'sharelink.js: the generator starts from the same value the mask starts at');
|
|
const params = managerForHash('')._buildHashParams();
|
|
assert.equal(params.get('sc'), '1');
|
|
assert.equal(params.get('scf'), '100');
|
|
});
|
|
|
|
test('an explicit feather still wins over the 100% default', () => {
|
|
// A link is authored state. The new default governs a session that said
|
|
// nothing; it must never overwrite one that said something.
|
|
assert.equal(managerForHash('#lat=10&lon=20&scf=11').parseInitialHash().scopeFeatherPct, 11,
|
|
'links carrying the previous default preserve their authored feather');
|
|
assert.equal(managerForHash('#lat=10&lon=20&scf=35').parseInitialHash().scopeFeatherPct, 35);
|
|
assert.equal(managerForHash('#lat=10&lon=20&scf=64').parseInitialHash().scopeFeatherPct, 64);
|
|
assert.equal(managerForHash('#lat=10&lon=20&scf=0').parseInitialHash().scopeFeatherPct, 0,
|
|
'an explicit 0 is a choice too, not an absent field');
|
|
|
|
// A link from before `scf` existed still restores what ITS author saw, which
|
|
// is the retired 35 — parsing an archive is not the same question as booting
|
|
// fresh, and this deliberately did NOT move with either later default.
|
|
assert.equal(managerForHash('#lat=10&lon=20&style=normal').parseInitialHash().scopeFeatherPct, 35,
|
|
'a pre-scf link restores the author\'s view, not the new default');
|
|
});
|
|
|
|
test('share links preserve explicit Scope OFF and feather choices over the defaults', () => {
|
|
const shared = managerForHash('#lat=10&lon=20&sc=0&scf=11').parseInitialHash();
|
|
assert.equal(shared.scopeEnabled, false);
|
|
assert.equal(shared.scopeFeatherPct, 11);
|
|
});
|
|
|
|
test('the 100% default retains the expanded feather range and hard edge', () => {
|
|
// The cheap way to move a default would be to nerf the control. Prove the
|
|
// slider still spans its full range and the geometry is still DERIVED from
|
|
// the ratio — a check that would pass vacuously if it only ever saw one value.
|
|
assert.match(indexHtml, /id="scope-feather-slider"[^>]*\smin="0"[^>]*\smax="200"/,
|
|
'the slider still offers the whole range');
|
|
const previous = getScopeMaskFeather();
|
|
try {
|
|
for (const ratio of [0.35, 0.7, 1, 1.75, 2]) {
|
|
setScopeMaskFeather(ratio);
|
|
const geo = scopeMaskGeometry(1200, 900);
|
|
const keyholeR = 900 * 0.5 * KEYHOLE_OUTER_RADIUS;
|
|
assert.ok(Math.abs((geo.outerR - geo.innerR) - keyholeR * ratio) < 1e-9,
|
|
`feather ${ratio} must still widen the band to that fraction of the keyhole`);
|
|
}
|
|
// The first-run value draws a band derived from the same keyhole radius.
|
|
setScopeMaskFeather(SCOPE_FEATHER_RATIO_DEFAULT);
|
|
const soft = scopeMaskGeometry(1200, 900);
|
|
const keyholeR = 900 * 0.5 * KEYHOLE_OUTER_RADIUS;
|
|
assert.ok(Math.abs((soft.outerR - soft.innerR) - keyholeR * SCOPE_FEATHER_RATIO_DEFAULT) < 1e-9,
|
|
'the default really draws its own band, derived from the ratio');
|
|
assert.ok(soft.outerR > soft.innerR, 'and it is a band, not a hard edge');
|
|
// The hard crop the previous default shipped is still one drag away.
|
|
setScopeMaskFeather(0);
|
|
const hard = scopeMaskGeometry(1200, 900);
|
|
assert.equal(hard.outerR, hard.innerR,
|
|
'an explicit 0 is still the hard crop — the path was not removed with the default');
|
|
} finally {
|
|
setScopeMaskFeather(previous);
|
|
}
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 2c. Detection Fade — 7% on a first run, at every surface that decides it
|
|
// (owner final lock 2026-08-24; 16% before). Fade is the label/card fading
|
|
// band around the keyhole — a different control from the scope-mask feather.
|
|
test('first run opens at 7% detection fade, at every surface that decides it', () => {
|
|
assert.equal(KEYHOLE_LABEL_FEATHER_RATIO, 0.07,
|
|
'celestialRing.js: the engine fade band opens at 7%');
|
|
assert.match(uiSource, /detectionFadePct: 7,/,
|
|
'ui.js: the global post defaults apply the same value on first load');
|
|
assert.match(indexHtml, /id="detection-fade-slider"[^>]*\svalue="7"/,
|
|
'index.html: the fade slider ships at 7');
|
|
assert.match(indexHtml, /id="detection-fade-value"[^>]*>7%</,
|
|
'index.html: the fade readout agrees with the slider');
|
|
assert.match(shareSource, /this\._detectionFadePct = 7;/,
|
|
'sharelink.js: the generator starts from the same value the overlay draws');
|
|
});
|
|
|
|
// 2b. Detection OUTSIDE opacity — 1% on a first run
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('first run opens at 1% OUTSIDE opacity, at every surface that decides it', () => {
|
|
assert.equal(KEYHOLE_OUTSIDE_OPACITY_DEFAULT, 0.01,
|
|
'owner final lock 2026-08-24: the world overlay reads quieter beyond the keyhole');
|
|
|
|
// Four independent literals decide this on a fresh boot: the engine constant
|
|
// above, the markup and its readout, ui.js's global post defaults, and the
|
|
// share generator's starting state. Changing one alone ships a UI that
|
|
// disagrees with its own engine.
|
|
assert.match(indexHtml, /id="detection-opacity-slider"[^>]*\svalue="1"/,
|
|
'index.html: the OUTSIDE slider ships at 1');
|
|
assert.match(indexHtml, /id="detection-opacity-value"[^>]*>1%</,
|
|
'index.html: and its readout agrees with the handle');
|
|
assert.match(uiSource, /detectionOutsideOpacityPct: 1,/,
|
|
'ui.js: the global post defaults apply the same value on first load');
|
|
assert.match(shareSource, /this\._detectionOutsideOpacityPct = 1;/,
|
|
'sharelink.js: the generator starts from the same value the overlay draws');
|
|
|
|
// The bracket floor is calibrated AT the default, so it moves with it — the
|
|
// approval attaches to the bracket brightness, not to the slider position.
|
|
assert.equal(AIRCRAFT_BRACKET_FLOOR_ANCHOR, KEYHOLE_OUTSIDE_OPACITY_DEFAULT,
|
|
'the AIR bracket floor anchor tracks the default it calibrates against');
|
|
|
|
// Reachability: the mapping was always continuous, but at the previous step
|
|
// of 5 the whole sub-default range was one stop wide.
|
|
assert.match(indexHtml, /id="detection-opacity-slider"[^>]*\sstep="1"/,
|
|
'index.html: every integer percent is reachable from the handle');
|
|
});
|
|
|
|
test('an explicit OUTSIDE opacity still wins over the new default', () => {
|
|
assert.equal(managerForHash('#lat=10&lon=20&ko=5').parseInitialHash().detectionOutsideOpacityPct, 5);
|
|
assert.equal(managerForHash('#lat=10&lon=20&ko=40').parseInitialHash().detectionOutsideOpacityPct, 40);
|
|
assert.equal(managerForHash('#lat=10&lon=20&ko=0').parseInitialHash().detectionOutsideOpacityPct, 0,
|
|
'an explicit 0 is a choice too, not an absent field');
|
|
|
|
// A link from before `ko` existed restores what ITS author saw. Every link
|
|
// since carries the field explicitly, so the 5% era is unaffected either way.
|
|
assert.equal(managerForHash('#lat=10&lon=20&style=normal').parseInitialHash().detectionOutsideOpacityPct, 5,
|
|
'a pre-ko link restores the author\'s view, not the new default');
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 3. Detection — on for every style on a first run, Normal included
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test('first run opens with detection on, in every style, using the one tactical preset', () => {
|
|
// Normal used to start OFF while only CRT/NVG/FLIR auto-applied the preset.
|
|
// It is now the baseline for all of them, reusing the SAME frozen object, so
|
|
// "the tactical look" cannot fork into two definitions.
|
|
assert.match(uiSource, /const MILITARY_DETECTION_PRESET = Object\.freeze\(\{\s*mode: 'dense',\s*densityPct: 75,?\s*\}\);/,
|
|
'the tactical look is still Dense @ 75%');
|
|
const baseline = uiBlock('const GLOBAL_POST_DEFAULTS = {', '\n};');
|
|
assert.match(baseline, /detectionMode: MILITARY_DETECTION_PRESET\.mode\.toUpperCase\(\),/,
|
|
'the first-load baseline reads the preset rather than restating it');
|
|
assert.match(baseline, /detectionDensity: MILITARY_DETECTION_PRESET\.densityPct,/,
|
|
'density comes from the same object, so the two cannot drift');
|
|
assert.doesNotMatch(baseline, /detectionMode: 'OFF'/,
|
|
'the retired OFF baseline is gone, not shadowed');
|
|
|
|
// `const` has no hoisted value: the baseline can only READ the preset if the
|
|
// preset is declared first. Getting this backwards is a startup TDZ crash,
|
|
// which no other test in the suite would reach.
|
|
assert.ok(
|
|
uiSource.indexOf('const MILITARY_DETECTION_PRESET =')
|
|
< uiSource.indexOf('const GLOBAL_POST_DEFAULTS ='),
|
|
'MILITARY_DETECTION_PRESET must be declared before the baseline that reads it',
|
|
);
|
|
});
|
|
|
|
test('detection-on-by-default is a default, not an operator override', () => {
|
|
// `_detectionUserOverridden` means the OPERATOR hand-edited detection, and it
|
|
// suppresses the military-style auto-enable for the rest of the session.
|
|
// 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');
|
|
assert.match(applyDefaults, /this\._setDetectionMode\(defaults\.detectionMode\)/,
|
|
'the baseline still goes through the real detection path');
|
|
assert.doesNotMatch(applyDefaults, /_detectionUserOverridden/,
|
|
'applying a factory default must not impersonate an operator edit');
|
|
|
|
// And the two halves of the override machinery are still wired: the style
|
|
// preset consults the flag, and the detection button sets it.
|
|
assert.match(uiSource, /if \(preset\.detection && !this\._detectionUserOverridden\) \{/,
|
|
'a style preset still yields to an operator who changed detection by hand');
|
|
const displayActions = uiSource.slice(uiSource.indexOf('this._displayControls ='));
|
|
const detectionButton = displayActions.slice(displayActions.indexOf('cycleDetection:'), displayActions.indexOf('toggleModels:'));
|
|
assert.match(detectionButton, /cycleDetectionMode\(\)/, 'the button still invokes the detection action');
|
|
assert.match(detectionButton, /this\.claimDetection\(\);/,
|
|
'and the detection control still claims the override when the operator uses it');
|
|
assert.match(uiSource, /claimDetection: \(\) => \{\s*this\._visualSettings\._detectionUserOverridden = true;/);
|
|
|
|
// Style-switch semantics are unchanged: Normal is still not a preset owner,
|
|
// so switching TO Normal does not re-apply or clear anything.
|
|
const stylePresets = uiBlock('const STYLE_PRESET_DEFAULTS = {', '\n};');
|
|
for (const style of ['retro', 'surveillance', 'thermal']) {
|
|
assert.match(stylePresets, new RegExp(`\\n ${style}: \\{`),
|
|
`${style} still carries its own preset`);
|
|
}
|
|
assert.doesNotMatch(stylePresets, /\n normal: \{/,
|
|
'Normal gained a default, not a style preset — switching to it still touches nothing');
|
|
});
|
|
|
|
test('a share link that carries detection OFF still restores OFF', () => {
|
|
// Same rule as the feather: the default governs a session that said nothing.
|
|
const off = managerForHash('#lat=10&lon=20&dm=OFF&dd=50').parseInitialHash();
|
|
assert.equal(off.detectionMode, 'OFF', 'an explicit OFF survives the default flip');
|
|
|
|
const sparse = managerForHash('#lat=10&lon=20&dm=SPARSE&dd=25').parseInitialHash();
|
|
assert.equal(sparse.detectionMode, 'SPARSE');
|
|
assert.equal(sparse.detectionDensity, 25,
|
|
'and a quieter explicit profile is not promoted to the new default');
|
|
|
|
const dense = managerForHash('#lat=10&lon=20&dm=DENSE&dd=75').parseInitialHash();
|
|
assert.equal(dense.detectionMode, 'DENSE');
|
|
assert.equal(dense.detectionDensity, 75);
|
|
});
|