mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
main moved the panels to Vue views and a one-open-panel rail, and removed
the two templates this branch edited. This merge carries the branch onto
that architecture; it adds no behaviour of its own beyond the port.
- The Utilities panel is a RailPanel in LayerPanelsView with its own rail
item (handyman). LayerPanel renders the Directions rows into its list with
LayerRows, and falls back to Data Layers without it.
- Draw leaves DisplayPanel for DrawControls, DrawMarkList and DrawSwatches,
which render what the draw tool publishes to the display store and call
its actions, as GeoImport does. drawMarks.js words the rows; the DOM-built
drawMarkList.js is gone. A row's fly-to is an immediate navigation like
Import's ('drawing' gets its Cockpit refusal), the name field keeps a
draft through v-field-value, and the colours are one radio group with a
single tab stop.
- UiSegmented draws an option's icon, named by its label, for the shape
buttons.
- The catalogs take main's languages with this branch's strings, Arabic
included, and the tests follow the views to their new homes.
322 lines
14 KiB
JavaScript
322 lines
14 KiB
JavaScript
import { readShellSource } from './testSupport/readShellSource.mjs';
|
|
import { renderApplicationHtml } from './testSupport/applicationHtml.mjs';
|
|
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, /onCollapse\(panel, \{ collapsed: true, auto: 'layout' \}\)/);
|
|
assert.match(readFileSync(new URL('./ui/panelLayoutController.js', import.meta.url), 'utf8'), /_presentPanel\(panel, \{ collapsed: false, auto: null \}\)/);
|
|
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*\)/);
|
|
// Collapse buttons render aria-expanded from the state the chrome presents
|
|
// (panelChromeButtons.test.mjs covers the views).
|
|
assert.match(ui, /this\._presentPanel\(panelEl, \{ collapsed: nextCollapsed, auto: null \}\)/);
|
|
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, /if \(exclusive && panel\.classList\.contains\('collapsed'\)\)\s*panel\.setAttribute\('aria-hidden', 'true'\)/);
|
|
});
|
|
|
|
test('share-panel state excludes responsive collapse and preserves recipient preferences', () => {
|
|
const ui = readShellSource();
|
|
// The long form spells the share format's panel table (one source of truth).
|
|
const panels = readFileSync(new URL('./share/viewCodec.js', import.meta.url), 'utf8');
|
|
|
|
assert.match(
|
|
ui,
|
|
/const shown = this\._shownPanel\(panelEl\);\s*const collapsed = prefersCollapsed\(shown\);/,
|
|
'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(panels, /id: 'param-slider-panel',\s*token: 'm',\s*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\);/);
|
|
// The panel's parameters change, and the rail lays out again.
|
|
assert.match(
|
|
ui,
|
|
/this\._publishParameters\(null\);\s*this\._scheduleRightPanelLayout\(\);/,
|
|
);
|
|
assert.match(
|
|
ui,
|
|
/this\._publishParameters\(\{[\s\S]*?\}\);\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', async () => {
|
|
const html = await renderApplicationHtml(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', async () => {
|
|
const html = await renderApplicationHtml(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*\);/,
|
|
);
|
|
});
|
|
|
|
test('Utilities sits between Data Layers and Scenes and follows the left panels through every mode', () => {
|
|
const css = readStylesheet(new URL('../style.css', import.meta.url));
|
|
const order = (id) => Number(new RegExp(`#left-panel-stack > #${id} \\{\\s*order: (\\d+);`).exec(css)?.[1]);
|
|
assert.ok(order('data-panel') < order('utilities-panel'));
|
|
assert.ok(order('utilities-panel') < order('scene-panel'));
|
|
assert.match(css, /body\.ui-clean-view #utilities-panel,/);
|
|
assert.match(css, /body\.recording-mode #utilities-panel,/);
|
|
// Directions stays reachable in Cockpit; only Draw and Import step aside.
|
|
assert.match(css, /body\.cockpit-mode #utilities-panel \.utilities-tools \{\s*display: none !important;/);
|
|
assert.doesNotMatch(css, /body\.cockpit-mode #left-panel-stack > #utilities-panel[,\s{]/);
|
|
assert.match(css, /body\.mobile-shell\[data-mobile-sheet='layers'\]\s+#left-panel-stack\s+> #utilities-panel,/);
|
|
});
|
|
|
|
test('every left panel has its rail item, Utilities with its own icon', async () => {
|
|
const { PANEL_RAIL_ITEMS } = await import('./ui/panelRailCatalog.js');
|
|
const view = readFileSync(new URL('./ui/views/LayerPanelsView.vue', import.meta.url), 'utf8');
|
|
const ids = [...view.matchAll(/<RailPanel id="([\w-]+)"/g)].map(([, id]) => id);
|
|
assert.ok(ids.includes('utilities-panel'));
|
|
for (const id of ids) assert.ok(PANEL_RAIL_ITEMS[id], `${id} has a rail item`);
|
|
assert.equal(PANEL_RAIL_ITEMS['utilities-panel'].icon, 'handyman');
|
|
});
|