mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Add the opt-in Cyber HUD layout with coordinated map and cockpit panel styling, GPU-only contact sonar (rings, range, power, opacity, sector) with native-rendering fallback, and set_cyber_sonar voice controls. Sonar visual style inspired by kk376's tactical naval sonar (#214). Co-authored-by: Sameh Khamis <[email protected]> Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
296 lines
12 KiB
JavaScript
296 lines
12 KiB
JavaScript
import { readShellSource } from './testSupport/readShellSource.mjs';
|
|
import { expandApplicationHtml } from '../build/application-html.js';
|
|
import { readStylesheet } from './testSupport/readStylesheet.mjs';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import test from 'node:test';
|
|
const leftRail = readFileSync(new URL('./ui/leftPanelRail.js', import.meta.url), 'utf8');
|
|
const rightRail = readFileSync(new URL('./ui/rightPanelRail.js', import.meta.url), 'utf8');
|
|
const rails = leftRail + rightRail;
|
|
import {
|
|
allocatePanelStackHeights,
|
|
panelStackAutoCollapseIndices,
|
|
resolveLeftStackBottomBoundary,
|
|
resolvePanelStackCorridor,
|
|
} from './panelStackLayout.js';
|
|
|
|
// Measured in Cockpit at 1512x790: CONTACT card at y541, HUD corner at y659,
|
|
// Cesium credits at y758, viewport inset boundary at y758.4. Every surface is
|
|
// a hard boundary even when a standard map panel is reopened in Cockpit.
|
|
const cockpitLane = {
|
|
baseBottom: 758.4,
|
|
safeGap: 9.5,
|
|
obstacles: [
|
|
{ top: 541.2, cockpitOverlay: true },
|
|
{ top: 659, cockpitOverlay: true },
|
|
{ top: 758, cockpitOverlay: false },
|
|
],
|
|
};
|
|
|
|
test('every left-lane obstacle shortens the corridor', () => {
|
|
assert.equal(
|
|
resolveLeftStackBottomBoundary(cockpitLane),
|
|
531.7,
|
|
);
|
|
});
|
|
|
|
test('an expanded Cockpit panel remains above the Contact and HUD surfaces', () => {
|
|
assert.equal(
|
|
resolveLeftStackBottomBoundary(cockpitLane),
|
|
531.7,
|
|
);
|
|
});
|
|
|
|
test('the Cesium credit line limits the corridor even in Cockpit', () => {
|
|
const boundary = resolveLeftStackBottomBoundary({
|
|
baseBottom: 900,
|
|
safeGap: 9.5,
|
|
obstacles: [{ top: 758 }],
|
|
});
|
|
assert.equal(boundary, 748.5);
|
|
});
|
|
|
|
test('an empty or malformed obstacle set leaves the viewport inset intact', () => {
|
|
assert.equal(resolveLeftStackBottomBoundary({ baseBottom: 700 }), 700);
|
|
assert.equal(
|
|
resolveLeftStackBottomBoundary({
|
|
baseBottom: 700,
|
|
obstacles: [{ top: Number.NaN }, {}, null],
|
|
safeGap: 8,
|
|
}),
|
|
700,
|
|
);
|
|
});
|
|
|
|
test('multiple expanded panels retain natural heights when the corridor fits', () => {
|
|
assert.deepEqual(allocatePanelStackHeights({
|
|
naturalHeights: [280, 160],
|
|
availableHeight: 500,
|
|
}), [280, 160]);
|
|
});
|
|
|
|
test('multiple expanded panels share a constrained corridor without overflow', () => {
|
|
const allocated = allocatePanelStackHeights({
|
|
naturalHeights: [520, 300],
|
|
availableHeight: 600,
|
|
});
|
|
assert.equal(Math.round(allocated.reduce((sum, height) => sum + height, 0)), 600);
|
|
assert.ok(allocated.every((height) => height >= 96));
|
|
assert.ok(allocated[0] > allocated[1]);
|
|
});
|
|
|
|
test('very short corridors remain bounded with every panel represented', () => {
|
|
const allocated = allocatePanelStackHeights({
|
|
naturalHeights: [420, 260, 180],
|
|
availableHeight: 150,
|
|
});
|
|
assert.equal(Math.round(allocated.reduce((sum, height) => sum + height, 0)), 150);
|
|
assert.ok(allocated.every((height) => height > 0));
|
|
});
|
|
|
|
test('later panels below half their natural height auto-collapse while the first is preserved', () => {
|
|
assert.deepEqual(panelStackAutoCollapseIndices({
|
|
naturalHeights: [520, 300, 180],
|
|
allocatedHeights: [180, 149, 120],
|
|
}), [1]);
|
|
});
|
|
|
|
test('the half-height boundary remains expanded', () => {
|
|
assert.deepEqual(panelStackAutoCollapseIndices({
|
|
naturalHeights: [520, 300],
|
|
allocatedHeights: [100, 150],
|
|
}), []);
|
|
});
|
|
|
|
test('Tactical focus preserves the primary panel and collapses later competitors', () => {
|
|
assert.deepEqual(panelStackAutoCollapseIndices({
|
|
naturalHeights: [320, 240, 180],
|
|
allocatedHeights: [220, 180, 140],
|
|
collapseLaterPanels: true,
|
|
}), [1, 2]);
|
|
});
|
|
|
|
test('viewport growth retains the aligned corridor when midpoint centering would cross Cockpit panels', () => {
|
|
assert.deepEqual(resolvePanelStackCorridor({
|
|
viewportHeight: 1026,
|
|
safeTop: 266.76,
|
|
safeBottom: 515.24,
|
|
obstacleSafeTop: 41.04,
|
|
obstacleSafeBottom: 515.24,
|
|
minimumHeight: 164.16,
|
|
}), {
|
|
safeTop: 266.76,
|
|
safeBottom: 515.24,
|
|
});
|
|
});
|
|
|
|
test('minimum panel corridor expands upward without crossing the lower obstacle boundary', () => {
|
|
const corridor = resolvePanelStackCorridor({
|
|
viewportHeight: 1026,
|
|
safeTop: 510.76,
|
|
safeBottom: 515.24,
|
|
obstacleSafeTop: 41.04,
|
|
obstacleSafeBottom: 515.24,
|
|
minimumHeight: 164.16,
|
|
});
|
|
assert.equal(Number(corridor.safeTop.toFixed(2)), 351.08);
|
|
assert.equal(corridor.safeBottom, 515.24);
|
|
});
|
|
|
|
test('desktop panel lanes use per-panel allocations and presentation-only auto-collapse', () => {
|
|
const ui = readShellSource();
|
|
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
|
assert.doesNotMatch(ui, /_enforce(?:Left|Right)PanelAccordion/);
|
|
assert.match(rails, /classList\.add\('collapsed', 'layout-auto-collapsed'\)/);
|
|
assert.match(readFileSync(new URL('./ui/panelLayoutController.js', import.meta.url), 'utf8'), /classList\.remove\('collapsed', 'layout-auto-collapsed'\)/);
|
|
assert.match(ui, /reconsiderAutoCollapse/);
|
|
assert.match(
|
|
ui,
|
|
/_rightPanelStack\?\.contains\(panelEl\)[\s\S]*?_scheduleRightPanelLayout\(\{ reconsiderAutoCollapse: true \}\)/,
|
|
);
|
|
assert.match(
|
|
ui,
|
|
/_scheduleLeftPanelLayout\(\{[\s\S]*?reconsiderAutoCollapse: this\._leftPanelStack\?\.contains\(panelEl\) === true/,
|
|
);
|
|
assert.match(rails, /collapseLaterPanels: shouldFocus && hud\.variant === 'tactical'/);
|
|
assert.match(ui, /this\._panelLayout\._leftStackPreferredPanelId = leftOwnerPanel\.id;/);
|
|
assert.match(
|
|
leftRail,
|
|
/preferredExpandedPanel[\s\S]*?\[\s*preferredExpandedPanel,\s*\.\.\.expandedPanelsInDomOrder/,
|
|
'the latest explicitly opened left panel must receive primary allocation',
|
|
);
|
|
assert.match(ui, /this\._panelLayout\._rightStackPreferredPanelId = rightOwnerPanel\.id;/);
|
|
assert.match(
|
|
rightRail,
|
|
/panel\.id === preferredPanelId[\s\S]*?\[\s*preferredExpandedPanel,\s*\.\.\.expandedPanelsInDomOrder/,
|
|
'the latest explicitly opened right panel must receive primary allocation',
|
|
);
|
|
assert.match(ui, /panelId === 'radio-panel'[\s\S]*?document\.getElementById\('global-context-panel'\)/);
|
|
assert.match(rightRail, /focusedExpandedPanel = expandedPanelsInDomOrder\.find\(\s*\(panel\) =>\s*panel\.contains\(documentRef\.activeElement\),?\s*\)/);
|
|
assert.match(ui, /setAttribute\('aria-expanded', String\(!collapsed\)\)/);
|
|
assert.match(leftRail, /--left-panel-allocated-height/);
|
|
assert.match(rightRail, /--right-panel-allocated-height/);
|
|
assert.match(
|
|
leftRail,
|
|
/expandedPanels[\s\S]*?removeProperty\('--left-panel-allocated-height'\)[\s\S]*?measurePanelNaturalHeight/,
|
|
'left intrinsic measurement must clear the prior allocation first',
|
|
);
|
|
assert.match(
|
|
rightRail,
|
|
/setAttribute\('data-rail-measuring', ''\)[\s\S]*?getBoundingClientRect\(\)\.height[\s\S]*?finally[\s\S]*?removeAttribute\('data-rail-measuring'\)/,
|
|
'right intrinsic measurement must neutralize allocations and always restore presentation',
|
|
);
|
|
assert.match(css, /var\(\s*--left-panel-allocated-height/);
|
|
assert.match(css, /var\(\s*--right-panel-allocated-height/);
|
|
assert.match(
|
|
css,
|
|
/#left-panel-stack\.layout-focus > \[data-panel-id\]\.collapsed\s*\{\s*display:\s*none;/,
|
|
'focus mode must hide every collapsed sibling, including presentation-only auto-collapses',
|
|
);
|
|
assert.doesNotMatch(css, /layout-focus > \[data-panel-id\]\.collapsed:not\(\.layout-auto-collapsed\)/);
|
|
assert.match(leftRail, /const hiddenSibling =\s*shouldFocus && panel\.classList\.contains\('collapsed'\);/);
|
|
});
|
|
|
|
test('share-panel state excludes responsive collapse and preserves recipient preferences', () => {
|
|
const ui = readShellSource();
|
|
const sharelink = readFileSync(new URL('./sharelink.js', import.meta.url), 'utf8');
|
|
|
|
assert.match(
|
|
ui,
|
|
/const collapsed =[\s\S]*?panelEl\.classList\.contains\('layout-auto-collapsed'\)[\s\S]*?panelEl\.classList\.contains\('cyber-accordion-collapsed'\)[\s\S]*?\? false\s*: panelEl\.classList\.contains\('collapsed'\);/,
|
|
'responsive auto-collapse must serialize the explicit expanded preference',
|
|
);
|
|
assert.match(
|
|
ui,
|
|
/_setCommandDockPanelPinState\(spec\.id, state\.pinned, \{\s*restore: true,\s*persist: false,\s*syncShare: false,/,
|
|
'restoring a pin must not overwrite local panel preferences or emit an intermediate hash',
|
|
);
|
|
assert.match(
|
|
ui,
|
|
/_setCommandDockPanelPinState[\s\S]*?if \(syncShare\) this\.shareLinkManager\?\.onPanelStateChange\?\.\(\);/,
|
|
'pin and unpin must update the share hash even when collapse state is unchanged',
|
|
);
|
|
assert.match(ui, /\{ id: 'param-slider-panel' \}/);
|
|
assert.match(sharelink, /\{ id: 'param-slider-panel', token: 'm', pinnable: false \}/);
|
|
});
|
|
|
|
test('parameterized Display presets keep one stable scroll owner', () => {
|
|
const ui = readShellSource();
|
|
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
|
|
|
assert.match(css, /#pp-toggles:not\(\.collapsed\) > \.pp-panel-body > #param-slider-panel\.active\s*\{[\s\S]*?flex:\s*0 0 auto;[\s\S]*?max-height:\s*none;[\s\S]*?overflow-y:\s*visible;/);
|
|
assert.match(ui, /readDisplayScrollTop: \(\) =>\s*this\._displayPortalScrollRestoreOwner === 'standard'[\s\S]*?this\._standardDisplayScrollTop[\s\S]*?displayPanelScroller\(this\._ppToggles\)\?\.scrollTop \|\| 0/);
|
|
assert.match(rightRail, /scroller\.scrollTop = Math\.min\(displayScrollTop, maxScrollTop\);/);
|
|
assert.match(
|
|
ui,
|
|
/this\._sliderPanel\.classList\.remove\('active'\);\s*this\._scheduleRightPanelLayout\(\);/,
|
|
);
|
|
assert.match(
|
|
ui,
|
|
/this\._sliderPanel\.classList\.add\('active'\);\s*this\._scheduleRightPanelLayout\(\);/,
|
|
);
|
|
assert.match(
|
|
css,
|
|
/\.param-slider\s*\{[\s\S]*?flex:\s*1;[\s\S]*?min-width:\s*0;/,
|
|
'parameter sliders must shrink before their value column can overflow',
|
|
);
|
|
});
|
|
|
|
test('expanded Display uses its container shell instead of a nested header card', () => {
|
|
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
|
|
|
assert.match(
|
|
css,
|
|
/#pp-toggles:not\(\.collapsed\) > \.pp-header-row\s*\{[\s\S]*?border:\s*0;[\s\S]*?background:\s*transparent;[\s\S]*?box-shadow:\s*none;/,
|
|
);
|
|
assert.match(
|
|
css,
|
|
/#pp-toggles\.collapsed \.pp-header-row\s*\{[\s\S]*?width:\s*var\(\s*--right-collapsed-width, 132px\);/,
|
|
'collapsed Display must retain its standalone launcher sizing',
|
|
);
|
|
});
|
|
|
|
test('expanded left panels integrate their headers with the container shell', () => {
|
|
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
|
|
|
assert.match(
|
|
css,
|
|
/#left-panel-stack > \[data-panel-id\]:not\(\.collapsed\) \.panel-header\s*\{[\s\S]*?position:\s*sticky;[\s\S]*?background:\s*transparent;/,
|
|
);
|
|
assert.match(
|
|
css,
|
|
/#left-panel-stack > \[data-panel-id\]:not\(\.collapsed\) \.panel-divider\s*\{[\s\S]*?linear-gradient\(\s*90deg,\s*rgb\(0 212 255 \/ 28%\),\s*rgba\(0, 212, 255, 0\.18\) 58%,\s*transparent\s*\)[\s\S]*?box-shadow:\s*0 0 7px rgba\(0, 212, 255, 0\.22\);/,
|
|
);
|
|
});
|
|
|
|
test('Map Source uses five compact tiles in the bottom Visual Presets tray', () => {
|
|
const html = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
|
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
|
|
|
assert.doesNotMatch(html, /id="stack-panel"/);
|
|
assert.match(html, /id="control-panel"[\s\S]*?class="map-source-section"[\s\S]*?id="map-stack-chips"/);
|
|
assert.match(
|
|
css,
|
|
/\.map-stack-chip-row\s*\{[\s\S]*?grid-template-columns:\s*repeat\(5, minmax\(0, 1fr\)\);/,
|
|
'the desktop source selector keeps all five tiles on one row',
|
|
);
|
|
});
|
|
|
|
test('expanded right panels highlight the title divider without changing collapsed launchers', () => {
|
|
const html = expandApplicationHtml(readFileSync(new URL('../index.html', import.meta.url), 'utf8'));
|
|
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
|
|
|
assert.match(
|
|
html,
|
|
/class="compact pp-header-row"[\s\S]*?class="pp-header-label">DISPLAY<\/span>[\s\S]*?class="panel-divider"/,
|
|
);
|
|
assert.match(
|
|
css,
|
|
/#right-context-rail \[data-panel-id\]:not\(\.collapsed\) \.panel-divider\s*\{[\s\S]*?linear-gradient\(\s*90deg,\s*rgb\(0 212 255 \/ 28%\),\s*rgba\(0, 212, 255, 0\.18\) 58%,\s*transparent\s*\)[\s\S]*?box-shadow:\s*0 0 7px rgba\(0, 212, 255, 0\.22\);/,
|
|
);
|
|
assert.match(
|
|
css,
|
|
/#param-slider-panel:not\(\.collapsed\) \.param-panel-divider\s*\{[\s\S]*?linear-gradient\(\s*90deg,\s*rgb\(0 212 255 \/ 28%\),\s*rgba\(0, 212, 255, 0\.18\) 58%,\s*transparent\s*\);/,
|
|
);
|
|
});
|