refactor(map): split foundry workspace and cesium runtime
This commit is contained in:
@@ -48,14 +48,14 @@ test("Inspector hard-forces a raw integrated Select to the split presentation",
|
||||
});
|
||||
|
||||
test("Foundry Inspector screens consume the semantic selection field", async () => {
|
||||
const [mapPreview, catalog, coreStyles] = await Promise.all([
|
||||
readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8"),
|
||||
const [mapInspector, catalog, coreStyles] = await Promise.all([
|
||||
readFile(new URL("../apps/catalog/src/mapInspectorSections.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../apps/catalog/src/CatalogApp.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../packages/ui-core/styles.css", import.meta.url), "utf8"),
|
||||
]);
|
||||
|
||||
assert.match(mapPreview, /<InspectorSelectField\s+label="Профиль покрытия"/);
|
||||
assert.doesNotMatch(mapPreview, /<ControlRow label="Профиль покрытия">/);
|
||||
assert.match(mapInspector, /<InspectorSelectField\s+label="Профиль покрытия"/);
|
||||
assert.doesNotMatch(mapInspector, /<ControlRow label="Профиль покрытия">/);
|
||||
assert.equal((catalog.match(/<InspectorSelectField/g) ?? []).length, 2);
|
||||
assert.match(coreStyles, /\.nodedc-inspector \.nodedc-control-row:has\(\.nodedc-select-anchor\)/);
|
||||
});
|
||||
|
||||
@@ -315,29 +315,32 @@ test("grid center is fixed and camera-independent, including for legacy camera-m
|
||||
});
|
||||
|
||||
test("renderer uses fixed ENU sectors, angular graticules and a non-blank double-buffer swap", async () => {
|
||||
const source = await readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8");
|
||||
assert.match(source, /class GridLayerController/);
|
||||
assert.match(source, /dataSourceDisplay\.ready/);
|
||||
assert.match(source, /fixedGridOrigin\(presentation\)/);
|
||||
assert.match(source, /Transforms\.eastNorthUpToFixedFrame\(anchor\)/);
|
||||
assert.match(source, /localSectorAt\(/);
|
||||
assert.match(source, /gridController\?\.pick\(worldPosition\)/);
|
||||
assert.match(source, /lod\.stepKm \* 1_000/);
|
||||
assert.match(source, /lod\.graticuleStepDegrees/);
|
||||
assert.match(source, /lod\.graticuleLineWidthPx/);
|
||||
assert.match(source, /arcType: ArcType\.RHUMB/);
|
||||
assert.match(source, /clampToGround: false/);
|
||||
assert.match(source, /majorLineWidthMultiplier/);
|
||||
assert.match(source, /isLocalMajorLineIndex/);
|
||||
assert.match(source, /isGraticuleMajorLineValue/);
|
||||
assert.match(source, /new LabelCollection\(\)/);
|
||||
assert.match(source, /const maximumLabels = 48/);
|
||||
assert.match(source, /materializeLocalSelection/);
|
||||
assert.match(source, /localVolumeAt\(/);
|
||||
assert.match(source, /focusGridSector/);
|
||||
assert.match(source, /selectedGridSector/);
|
||||
assert.match(source, /mountResources\(resources\)[\s\S]*?const previous = this\.current;[\s\S]*?removeResources\(previous\.resources\)/);
|
||||
assert.doesNotMatch(source, /function rebuildElevatedGrid[\s\S]*?entities\.removeAll\(\)/);
|
||||
assert.doesNotMatch(source, /Math\.min\(40,[\s\S]*?safeRadiusKm \/ safeStepKm/);
|
||||
assert.doesNotMatch(source, /snapGridCenter\(/);
|
||||
const [renderer, gridLayer] = await Promise.all([
|
||||
readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../apps/catalog/src/mapCesiumGridLayer.ts", import.meta.url), "utf8"),
|
||||
]);
|
||||
assert.match(gridLayer, /class GridLayerController/);
|
||||
assert.match(gridLayer, /dataSourceDisplay\.ready/);
|
||||
assert.match(gridLayer, /fixedGridOrigin\(presentation\)/);
|
||||
assert.match(gridLayer, /Transforms\.eastNorthUpToFixedFrame\(anchor\)/);
|
||||
assert.match(gridLayer, /localSectorAt\(/);
|
||||
assert.match(renderer, /gridController\?\.pick\(worldPosition\)/);
|
||||
assert.match(gridLayer, /lod\.stepKm \* 1_000/);
|
||||
assert.match(gridLayer, /lod\.graticuleStepDegrees/);
|
||||
assert.match(gridLayer, /lod\.graticuleLineWidthPx/);
|
||||
assert.match(gridLayer, /arcType: ArcType\.RHUMB/);
|
||||
assert.match(gridLayer, /clampToGround: false/);
|
||||
assert.match(gridLayer, /majorLineWidthMultiplier/);
|
||||
assert.match(gridLayer, /isLocalMajorLineIndex/);
|
||||
assert.match(gridLayer, /isGraticuleMajorLineValue/);
|
||||
assert.match(gridLayer, /new LabelCollection\(\)/);
|
||||
assert.match(gridLayer, /const maximumLabels = 48/);
|
||||
assert.match(gridLayer, /materializeLocalSelection/);
|
||||
assert.match(gridLayer, /localVolumeAt\(/);
|
||||
assert.match(renderer, /focusGridSector/);
|
||||
assert.match(renderer, /selectedGridSector/);
|
||||
assert.match(gridLayer, /mountResources\(resources\)[\s\S]*?const previous = this\.current;[\s\S]*?removeResources\(previous\.resources\)/);
|
||||
assert.doesNotMatch(gridLayer, /function rebuildElevatedGrid[\s\S]*?entities\.removeAll\(\)/);
|
||||
assert.doesNotMatch(gridLayer, /Math\.min\(40,[\s\S]*?safeRadiusKm \/ safeStepKm/);
|
||||
assert.doesNotMatch(gridLayer, /snapGridCenter\(/);
|
||||
});
|
||||
|
||||
@@ -3,17 +3,17 @@ import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
test("Cesium elevated targets keep their profile border while labels occlude them inside the scene overlay", async () => {
|
||||
const [renderer, cesiumDisplay] = await Promise.all([
|
||||
readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8"),
|
||||
const [runtimeLayers, cesiumDisplay] = await Promise.all([
|
||||
readFile(new URL("../apps/catalog/src/mapCesiumRuntimeLayers.ts", import.meta.url), "utf8"),
|
||||
readFile(new URL("../node_modules/@cesium/engine/Source/DataSources/DataSourceDisplay.js", import.meta.url), "utf8"),
|
||||
]);
|
||||
|
||||
assert.match(renderer, /BillboardGraphics/);
|
||||
assert.match(renderer, /elevatedTargetImage\(\s*color,\s*Color\.fromCssColorString\(target\.outlineColor\)\.withAlpha\(target\.outlineOpacity\),\s*target\.outlineWidthPx,\s*target\.headSizePx/);
|
||||
assert.match(renderer, /entity\.point = undefined;\s*entity\.billboard = new BillboardGraphics/);
|
||||
assert.match(renderer, /disableDepthTestDistance: Number\.POSITIVE_INFINITY/);
|
||||
assert.match(renderer, /outlineWidth: 0,\s*style: LabelStyle\.FILL/);
|
||||
assert.doesNotMatch(renderer, /style: 2/);
|
||||
assert.match(runtimeLayers, /BillboardGraphics/);
|
||||
assert.match(runtimeLayers, /elevatedTargetImage\(\s*color,\s*Color\.fromCssColorString\(target\.outlineColor\)\.withAlpha\(target\.outlineOpacity\),\s*target\.outlineWidthPx,\s*target\.headSizePx/);
|
||||
assert.match(runtimeLayers, /entity\.point = undefined;\s*entity\.billboard = new BillboardGraphics/);
|
||||
assert.match(runtimeLayers, /disableDepthTestDistance: Number\.POSITIVE_INFINITY/);
|
||||
assert.match(runtimeLayers, /outlineWidth: 0,\s*style: LabelStyle\.FILL/);
|
||||
assert.doesNotMatch(runtimeLayers, /style: 2/);
|
||||
assert.ok(cesiumDisplay.indexOf("new BillboardVisualizer") < cesiumDisplay.indexOf("new LabelVisualizer"));
|
||||
});
|
||||
|
||||
|
||||
@@ -2,71 +2,92 @@ import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
|
||||
test("Objects menu opens controllable groups from the row and keeps plain bindings as visibility layers", async () => {
|
||||
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
|
||||
const source = (path) => readFile(new URL(path, import.meta.url), "utf8");
|
||||
|
||||
assert.match(preview, /const toggleSubjectVisibility = \(bindingId: string\)/);
|
||||
assert.match(preview, /role=\{hasControls \? "menuitem" : "menuitemcheckbox"\}/);
|
||||
assert.match(preview, /if \(hasControls\) \{\s*openSubjectWindow\(summary\.bindingId\);\s*close\(\)/);
|
||||
assert.match(preview, /toggleSubjectVisibility\(summary\.bindingId\)/);
|
||||
assert.match(preview, /visible: !state\.visible/);
|
||||
assert.match(preview, /слой скрыт · \$\{summary\.total\} объектов/);
|
||||
assert.doesNotMatch(preview, /Фильтры и счётчики: \$\{summary\.displayName\}/);
|
||||
test("Objects menu delegates controlled layers to the workspace reducer", async () => {
|
||||
const [preview, toolbar, state] = await Promise.all([
|
||||
source("../apps/catalog/src/MapFixturePreview.tsx"),
|
||||
source("../apps/catalog/src/MapWorkspaceToolbar.tsx"),
|
||||
source("../apps/catalog/src/mapWorkspaceState.mjs"),
|
||||
]);
|
||||
|
||||
assert.match(toolbar, /role=\{hasControls \? "menuitem" : "menuitemcheckbox"\}/);
|
||||
assert.match(toolbar, /onOpenSubjectWindow\(summary\.bindingId\)/);
|
||||
assert.match(toolbar, /onToggleSubjectVisibility\(summary\.bindingId\)/);
|
||||
assert.match(toolbar, /слой скрыт · \$\{summary\.total\} объектов/);
|
||||
assert.match(preview, /type: "toggle-subject-visibility", bindingId/);
|
||||
assert.match(state, /visible: !subject\.visible/);
|
||||
});
|
||||
|
||||
test("a subject window exists only when the presentation profile exposes controls", async () => {
|
||||
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
|
||||
test("a subject window exists only when the MCP presentation profile exposes controls", async () => {
|
||||
const [windows, model, state] = await Promise.all([
|
||||
source("../apps/catalog/src/MapSubjectWorkspaceWindows.tsx"),
|
||||
source("../apps/catalog/src/mapWorkspaceModel.mjs"),
|
||||
source("../apps/catalog/src/mapWorkspaceState.mjs"),
|
||||
]);
|
||||
|
||||
assert.match(preview, /function hasSubjectWindowControls\(profile: MapPresentationProfile\)/);
|
||||
assert.match(preview, /if \(!state\?\.window\.open \|\| !hasSubjectWindowControls\(summary\.profile\)\) return null/);
|
||||
assert.match(preview, /window: \{ \.\.\.state\.window, open: false \}/);
|
||||
assert.match(model, /export function mapProfileHasSubjectWindowControls/);
|
||||
assert.match(windows, /!state\?\.window\.open \|\| !mapProfileHasSubjectWindowControls\(summary\.profile\)/);
|
||||
assert.match(windows, /type: "close-subject-window"/);
|
||||
assert.match(state, /window: \{ \.\.\.subject\.window, open: false \}/);
|
||||
});
|
||||
|
||||
test("hidden and visible layers have an explicit visual state", async () => {
|
||||
const styles = await readFile(new URL("../apps/catalog/src/styles.css", import.meta.url), "utf8");
|
||||
const styles = await source("../apps/catalog/src/styles.css");
|
||||
|
||||
assert.match(styles, /\.catalog-map-fixture__objects-menu-item\[data-visible\]/);
|
||||
assert.match(styles, /\.catalog-map-fixture__objects-menu-item:not\(\[data-visible\]\)/);
|
||||
});
|
||||
|
||||
test("facet controls and renderer consume the same canonical enabled-value state", async () => {
|
||||
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
|
||||
test("facet controls and renderer consume one canonical enabled-value state", async () => {
|
||||
const [preview, windows, state] = await Promise.all([
|
||||
source("../apps/catalog/src/MapFixturePreview.tsx"),
|
||||
source("../apps/catalog/src/MapSubjectWorkspaceWindows.tsx"),
|
||||
source("../apps/catalog/src/mapWorkspaceState.mjs"),
|
||||
]);
|
||||
|
||||
assert.match(preview, /initialSubjectState\(initialLayout\?\.dataProductBindings[\s\S]*?presentationProfiles\)/);
|
||||
assert.match(preview, /facets: profile \? normalizeMapPresentationFacetSelections\(state\.filters, profile\) : state\.filters/);
|
||||
assert.match(preview, /filters: normalizeMapPresentationFacetSelections\(state\.filters, summary\.profile\)/);
|
||||
assert.match(preview, /const active = mapPresentationFacetValueIsEnabled\(state\.filters, facet\.field, item\.value\)/);
|
||||
assert.match(preview, /toggleMapPresentationFacetSelection\(filters, field, value, availableValues\)/);
|
||||
assert.match(preview, /facet\.values\.map\(\(value\) => value\.value\)/);
|
||||
assert.match(preview, /normalizeMapPresentationFacetSelections\(state\.filters, profile\)/);
|
||||
assert.match(preview, /presentationFilters=\{rendererPresentationFilters\}/);
|
||||
assert.match(windows, /mapPresentationFacetValueIsEnabled\(state\.filters, facet\.field, item\.value\)/);
|
||||
assert.match(windows, /facet\.values\.map\(\(value\) => value\.value\)/);
|
||||
assert.match(state, /toggleMapPresentationFacetSelection\(/);
|
||||
});
|
||||
|
||||
test("joined detail aspects do not become independent map layers", async () => {
|
||||
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
|
||||
test("joined detail aspects enrich subjects but never become independent renderer layers", async () => {
|
||||
const [preview, model] = await Promise.all([
|
||||
source("../apps/catalog/src/MapFixturePreview.tsx"),
|
||||
source("../apps/catalog/src/mapWorkspaceModel.mjs"),
|
||||
]);
|
||||
|
||||
assert.match(preview, /dataProductBindings\.filter\(\(binding\) => !binding\.joinToBindingId\)/);
|
||||
assert.match(preview, /runtimeBindings\.filter\(\(binding\) => primaryBindingIds\.has\(binding\.bindingId\)\)/);
|
||||
assert.match(model, /filter\(\(binding\) => !binding\.joinToBindingId\)/);
|
||||
assert.match(model, /primaryMapBindingConfigs\(bindingConfigs\)/);
|
||||
assert.match(preview, /runtimeBindings=\{\[\.\.\.sectorScopedPrimaryRuntimeBindings, \.\.\.referenceRuntimeBindings\]\}/);
|
||||
});
|
||||
|
||||
test("bounded map windows keep one stack while layers and settings use their canonical surfaces", async () => {
|
||||
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
|
||||
test("bounded map windows share one reducer-owned z-stack while settings and layers keep canonical surfaces", async () => {
|
||||
const [preview, toolbar, windows, sectorWindow, state, contract] = await Promise.all([
|
||||
source("../apps/catalog/src/MapFixturePreview.tsx"),
|
||||
source("../apps/catalog/src/MapWorkspaceToolbar.tsx"),
|
||||
source("../apps/catalog/src/MapSubjectWorkspaceWindows.tsx"),
|
||||
source("../apps/catalog/src/MapSectorWorkspaceWindow.tsx"),
|
||||
source("../apps/catalog/src/mapWorkspaceState.mjs"),
|
||||
source("../apps/catalog/src/mapPageContract.ts"),
|
||||
]);
|
||||
|
||||
assert.match(preview, /type MapWorkspaceWindowId = "sector" \| "subject-card" \| `binding:\$\{string\}`/);
|
||||
assert.match(preview, /const activateWorkspaceWindow = useCallback/);
|
||||
assert.match(preview, /onActivate=\{\(\) => activateWorkspaceWindow\(`binding:\$\{summary\.bindingId\}`\)\}/);
|
||||
assert.doesNotMatch(preview, /onActivate=\{\(\) => openSubjectWindow\(summary\.bindingId\)\}/);
|
||||
assert.match(preview, /sectorWindowZIndex[\s\S]*subjectCardZIndex/);
|
||||
assert.match(contract, /type MapWorkspaceWindowId = "sector" \| "subject-card" \| `binding:\$\{string\}`/);
|
||||
assert.match(state, /function activateWindow\(state, windowId\)/);
|
||||
assert.match(state, /Math\.max\([\s\S]*state\.sector\.window\.zIndex[\s\S]*state\.subjectCard\.zIndex/);
|
||||
assert.match(windows, /type: "activate-window", windowId: `binding:\$\{summary\.bindingId\}`/);
|
||||
assert.match(sectorWindow, /onActivate=\{onActivate\}/);
|
||||
assert.match(preview, /<ApplicationSidePanel[\s\S]*title="Настройки карты"[\s\S]*onClose=\{closeSettingsPanel\}/);
|
||||
assert.match(preview, /createPortal\(headerActions, headerActionsHost\)/);
|
||||
assert.doesNotMatch(preview, /settingsPanelPinned|onPinnedChange/);
|
||||
assert.match(preview, /surfaceClassName="catalog-map-fixture__objects-menu catalog-map-fixture__layers-menu nodedc-map-glass"/);
|
||||
assert.match(preview, /onOpenChange=\{setLayersOpen\}/);
|
||||
assert.doesNotMatch(preview, /settingsWindowZIndex|layersWindowZIndex/);
|
||||
assert.doesNotMatch(preview, /<Window\b/);
|
||||
assert.match(toolbar, /surfaceClassName="catalog-map-fixture__objects-menu catalog-map-fixture__layers-menu nodedc-map-glass"/);
|
||||
assert.match(toolbar, /onOpenChange=\{onOpenChange\}/);
|
||||
assert.doesNotMatch(preview, /settingsPanelPinned|settingsWindowZIndex|layersWindowZIndex/);
|
||||
});
|
||||
|
||||
test("selecting a point or HGeoZone preserves the active grid sector", async () => {
|
||||
const renderer = await readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8");
|
||||
const renderer = await source("../apps/catalog/src/CesiumMapRenderer.tsx");
|
||||
const pointBranch = renderer.match(/if \(pickedId instanceof Entity[\s\S]*?return;\s*\}/)?.[0] ?? "";
|
||||
const zoneBranch = renderer.match(/if \(\s*pickedId[\s\S]*?HGeoZonePickId\)\.entityId\);\s*return;\s*\}/)?.[0] ?? "";
|
||||
|
||||
@@ -77,59 +98,65 @@ test("selecting a point or HGeoZone preserves the active grid sector", async ()
|
||||
assert.match(renderer, /gridController\?\.setSelection\(gridSelection\);\s*onGridSectorSelectRef\.current\?\.\(gridSelection\)/);
|
||||
});
|
||||
|
||||
test("an active sector scopes point facts, exposes data facets and deactivates on close", async () => {
|
||||
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
|
||||
|
||||
assert.match(preview, /function mapFactInsideGridSector/);
|
||||
assert.match(preview, /localSectorAtGeodetic/);
|
||||
assert.match(preview, /label="Скрыть объекты за сектором"/);
|
||||
assert.match(preview, />Домены данных</);
|
||||
assert.match(preview, />Провайдеры</);
|
||||
assert.match(preview, />Типы объектов</);
|
||||
assert.match(preview, />Объекты сектора</);
|
||||
assert.match(preview, /onClose=\{deactivateGridSector\}/);
|
||||
assert.match(preview, />\s*Деактивировать сектор\s*</);
|
||||
assert.match(preview, /setSelectedGridSector\(null\)/);
|
||||
});
|
||||
|
||||
test("reference stations are first-class Objects menu layers without provider settings actions", async () => {
|
||||
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
|
||||
|
||||
assert.match(preview, /const referenceObjectSummaries = useMemo/);
|
||||
assert.match(preview, /const objectLayerCount = presentationSummaries\.length \+ referenceObjectSummaries\.length/);
|
||||
assert.match(preview, /referenceObjectSummaries\.map\(\(\{ layer, displayName, total \}\)/);
|
||||
assert.match(preview, /candidate\.id === layer\.id \? \{ \.\.\.candidate, visible: !candidate\.visible \} : candidate/);
|
||||
assert.match(preview, /role="menuitemcheckbox"/);
|
||||
});
|
||||
|
||||
test("facet window auto-fits expanded and collapsed content inside map workspace", async () => {
|
||||
const [preview, workspace] = await Promise.all([
|
||||
readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../packages/ui-react/src/WorkspaceWindow.tsx", import.meta.url), "utf8"),
|
||||
test("active sector scope is geometric, filterable and reducer-deactivated", async () => {
|
||||
const [runtime, sectorWindow, state] = await Promise.all([
|
||||
source("../apps/catalog/src/mapSectorRuntime.mjs"),
|
||||
source("../apps/catalog/src/MapSectorWorkspaceWindow.tsx"),
|
||||
source("../apps/catalog/src/mapWorkspaceState.mjs"),
|
||||
]);
|
||||
|
||||
assert.match(preview, /minHeight=\{220\}\s*autoHeight/);
|
||||
assert.match(runtime, /export function mapFactInsideGridSector/);
|
||||
assert.match(runtime, /localSectorAtGeodetic/);
|
||||
assert.match(sectorWindow, /label="Скрыть объекты за сектором"/);
|
||||
assert.match(sectorWindow, /title="Домены данных"/);
|
||||
assert.match(sectorWindow, /title="Провайдеры"/);
|
||||
assert.match(sectorWindow, /title="Типы объектов"/);
|
||||
assert.match(sectorWindow, /id="map-sector-objects-title">Объекты сектора/);
|
||||
assert.match(sectorWindow, /onClose=\{onDeactivate\}/);
|
||||
assert.match(sectorWindow, />\s*Деактивировать сектор\s*</);
|
||||
assert.match(state, /case "deactivate-sector"/);
|
||||
assert.match(state, /selectedSector: null/);
|
||||
});
|
||||
|
||||
test("reference stations remain first-class visibility layers without provider settings actions", async () => {
|
||||
const [preview, toolbar] = await Promise.all([
|
||||
source("../apps/catalog/src/MapFixturePreview.tsx"),
|
||||
source("../apps/catalog/src/MapWorkspaceToolbar.tsx"),
|
||||
]);
|
||||
|
||||
assert.match(preview, /const referenceObjectSummaries = useMemo/);
|
||||
assert.match(toolbar, /const objectLayerCount = summaries\.length \+ referenceSummaries\.length/);
|
||||
assert.match(toolbar, /referenceSummaries\.map\(\(\{ layer, displayName, total \}\)/);
|
||||
assert.match(toolbar, /onToggleReferenceLayer\(layer\.id\)/);
|
||||
assert.match(toolbar, /role="menuitemcheckbox"/);
|
||||
});
|
||||
|
||||
test("facet windows auto-fit expanded and collapsed content inside the map workspace", async () => {
|
||||
const [windows, workspace] = await Promise.all([
|
||||
source("../apps/catalog/src/MapSubjectWorkspaceWindows.tsx"),
|
||||
source("../packages/ui-react/src/WorkspaceWindow.tsx"),
|
||||
]);
|
||||
|
||||
assert.match(windows, /minHeight=\{220\}\s*autoHeight/);
|
||||
assert.match(workspace, /autoHeight\?: boolean/);
|
||||
assert.match(workspace, /content\.scrollHeight/);
|
||||
assert.match(workspace, /availableHeight = Math\.max\(0, nextBounds\.height - normalized\.y\)/);
|
||||
assert.match(workspace, /new ResizeObserver\(scheduleFit\)/);
|
||||
});
|
||||
|
||||
test("facet tree selection focuses the subject without resetting a compatible detail tab", async () => {
|
||||
const preview = await readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8");
|
||||
const renderer = await readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8");
|
||||
test("facet and search selection focus the subject while the reducer preserves a valid detail tab", async () => {
|
||||
const [preview, renderer, state] = await Promise.all([
|
||||
source("../apps/catalog/src/MapFixturePreview.tsx"),
|
||||
source("../apps/catalog/src/CesiumMapRenderer.tsx"),
|
||||
source("../apps/catalog/src/mapWorkspaceState.mjs"),
|
||||
]);
|
||||
|
||||
assert.match(preview, /const \[expandedFacetRows, setExpandedFacetRows\]/);
|
||||
assert.match(preview, /const handleSelectAndFocus = useCallback/);
|
||||
assert.match(preview, /const focusSubject = useCallback/);
|
||||
assert.match(preview, /if \(renderer\.focusRuntimeEntity\(entityId\)\) return true/);
|
||||
assert.match(preview, /mapFactPointCoordinates\(selectable\.find/);
|
||||
assert.match(preview, /renderer\.focusSubjectCoordinates\(fallbackCoordinates\[0\], fallbackCoordinates\[1\]\)/);
|
||||
assert.match(preview, /focusSubject\(entityId\)/);
|
||||
assert.match(preview, /focusSubject\(result\.entityId, result\.coordinates\)/);
|
||||
assert.match(preview, /setSubjectCardTabId\(\(current\) =>/);
|
||||
assert.match(preview, /profile\?\.tabs\.some\(\(tab\) => tab\.id === current\)/);
|
||||
assert.match(preview, /\(profile\?\.defaultTabId \?\? "overview"\)/);
|
||||
assert.match(preview, /validTabIds: profile\?\.tabs\.map\(\(tab\) => tab\.id\) \?\? \["overview"\]/);
|
||||
assert.match(state, /action\.validTabIds\.includes\(state\.subjectCard\.tabId\)/);
|
||||
assert.match(renderer, /void viewer\.flyTo\(entity, \{/);
|
||||
assert.match(renderer, /const focusSubjectCoordinates = useCallback/);
|
||||
assert.match(renderer, /viewer\.camera\.flyToBoundingSphere\(new BoundingSphere\(target, 1\), \{/);
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import test from "node:test";
|
||||
import ts from "typescript";
|
||||
|
||||
const source = await readFile(new URL("../apps/catalog/src/mapPresentationProfile.ts", import.meta.url), "utf8");
|
||||
const transpiled = ts.transpileModule(source, {
|
||||
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
|
||||
}).outputText;
|
||||
const moduleUrl = `data:text/javascript;base64,${Buffer.from(transpiled).toString("base64")}`;
|
||||
const {
|
||||
mapFactMatchesFilters,
|
||||
mapPresentationBindingIsAll,
|
||||
@@ -15,7 +7,7 @@ const {
|
||||
mapPresentationFacetValueIsEnabled,
|
||||
normalizeMapPresentationFacetSelections,
|
||||
toggleMapPresentationFacetSelection,
|
||||
} = await import(moduleUrl);
|
||||
} = await import("../apps/catalog/src/mapPresentationProfile.mjs");
|
||||
|
||||
const profile = {
|
||||
id: "map.moving-object.operational.default",
|
||||
|
||||
@@ -28,12 +28,12 @@ test("Map Page registers a provider-neutral reference-points slot and three stat
|
||||
|
||||
test("existing layouts upgrade reference layers on read and renderer accepts their slot", async () => {
|
||||
const server = await readFile(new URL("server/catalog-server.mjs", root), "utf8");
|
||||
const renderer = await readFile(new URL("apps/catalog/src/CesiumMapRenderer.tsx", root), "utf8");
|
||||
const runtimeLayers = await readFile(new URL("apps/catalog/src/mapCesiumRuntimeLayers.ts", root), "utf8");
|
||||
const runtime = await readFile(new URL("apps/catalog/src/useMapReferenceRuntime.ts", root), "utf8");
|
||||
|
||||
assert.match(server, /pageLayoutMatch\[1\] === "map" \? validateMapPageLayout\(stored\) : stored/);
|
||||
assert.match(server, /referenceLayers: structuredClone\(canonicalMapReferenceLayers\)/);
|
||||
assert.match(renderer, /binding\.slotId === "reference-points"/);
|
||||
assert.match(runtimeLayers, /binding\.slotId === "reference-points"/);
|
||||
assert.match(runtime, /\/api\/map-gateway\/api\/map\/reference-sources\/v1\/profiles\//);
|
||||
assert.match(runtime, /allowedAttributes = new Set\(\["name", "category", "network", "operator", "official_name", "local_name", "uic_ref", "wheelchair"\]\)/);
|
||||
assert.doesNotMatch(runtime, /credential|accessToken|providerEndpoint|rawPayload/);
|
||||
|
||||
@@ -275,17 +275,20 @@ test("restricted identity aspects render full identifiers and bounded string lis
|
||||
});
|
||||
|
||||
test("Cesium pin and label share one entity selection that opens the subject card", async () => {
|
||||
const [renderer, preview] = await Promise.all([
|
||||
const [renderer, preview, windows, state] = await Promise.all([
|
||||
readFile(new URL("../apps/catalog/src/CesiumMapRenderer.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../apps/catalog/src/MapFixturePreview.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../apps/catalog/src/MapSubjectWorkspaceWindows.tsx", import.meta.url), "utf8"),
|
||||
readFile(new URL("../apps/catalog/src/mapWorkspaceState.mjs", import.meta.url), "utf8"),
|
||||
]);
|
||||
|
||||
assert.match(renderer, /viewer\?\.scene\.pick/);
|
||||
assert.match(renderer, /pickedId instanceof Entity/);
|
||||
assert.match(renderer, /onSelectRef\.current\?\.\(pickedId\.id\)/);
|
||||
assert.match(preview, /const handleSelect = useCallback/);
|
||||
assert.match(preview, /setSubjectCardOpen\(true\)/);
|
||||
assert.match(preview, /title=\{selectedSubjectCard\.title\}/);
|
||||
assert.match(preview, /Карточка объекта:/);
|
||||
assert.match(preview, /SegmentedControl/);
|
||||
assert.match(preview, /type: "select-entity"/);
|
||||
assert.match(state, /subjectCard: \{ \.\.\.state\.subjectCard, open: true, tabId \}/);
|
||||
assert.match(windows, /title=\{selectedSubjectCard\.title\}/);
|
||||
assert.match(windows, /Карточка объекта:/);
|
||||
assert.match(windows, /SegmentedControl/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
buildFilteredMapTargets,
|
||||
buildMapPresentationFilters,
|
||||
buildSelectableMapEntities,
|
||||
planMapSubjectReveal,
|
||||
primaryMapRuntimeBindings,
|
||||
} from "../apps/catalog/src/mapWorkspaceModel.mjs";
|
||||
import { mapRuntimeEntityId, mapRuntimeFactKey } from "../apps/catalog/src/mapRuntimeIdentity.mjs";
|
||||
import { localSectorAtGeodetic, localSectorSummary } from "../apps/catalog/src/mapSectorGrid.mjs";
|
||||
|
||||
const origin = { latitude: 55.7558, longitude: 37.6173 };
|
||||
const gridProfile = {
|
||||
mode: "3d",
|
||||
stepKm: 1,
|
||||
tileSizeKm: 5,
|
||||
graticuleStepDegrees: 1,
|
||||
};
|
||||
const primaryBinding = {
|
||||
id: "positions",
|
||||
order: 1,
|
||||
dataProductId: "fleet.positions.current.v5",
|
||||
slotId: "points",
|
||||
delivery: "snapshot+patch",
|
||||
semanticTypes: ["map.moving_object"],
|
||||
fieldProjection: ["display_name", "signal_state", "position_source", "object_kind"],
|
||||
presentationProfileId: "moving",
|
||||
};
|
||||
const joinedBinding = {
|
||||
...primaryBinding,
|
||||
id: "identity",
|
||||
order: 2,
|
||||
dataProductId: "fleet.units.identity.current.v1",
|
||||
joinToBindingId: "positions",
|
||||
};
|
||||
const profile = {
|
||||
id: "moving",
|
||||
title: "Current positions",
|
||||
semanticTypes: ["map.moving_object"],
|
||||
label: { mode: "attributes", fields: ["display_name"], maxLength: 80 },
|
||||
target: { variant: "elevated-spike" },
|
||||
facets: [{
|
||||
id: "signal",
|
||||
field: "signal_state",
|
||||
filterable: true,
|
||||
counter: true,
|
||||
values: [
|
||||
{ value: "active", label: "Online", order: 1 },
|
||||
{ value: "inactive", label: "Offline", order: 2 },
|
||||
],
|
||||
}],
|
||||
styles: [{ id: "active", color: "#fff", opacity: 1 }],
|
||||
classes: [{ id: "active", label: "Online", priority: 1, match: [], styleId: "active", renderable: true }],
|
||||
defaultClassId: "active",
|
||||
sort: [{ field: "signal_state", order: ["active", "inactive"] }],
|
||||
};
|
||||
|
||||
function pointFact(sourceId, signalState, coordinates, displayName = sourceId) {
|
||||
return {
|
||||
sourceId,
|
||||
semanticType: "map.moving_object",
|
||||
observedAt: "2026-08-09T12:00:00.000Z",
|
||||
receivedAt: "2026-08-09T12:00:01.000Z",
|
||||
attributes: {
|
||||
display_name: displayName,
|
||||
signal_state: signalState,
|
||||
position_source: "gelios",
|
||||
object_kind: "tracked_unit",
|
||||
},
|
||||
geometry: { type: "Point", coordinates },
|
||||
presentationStatus: signalState,
|
||||
};
|
||||
}
|
||||
|
||||
const inactive = pointFact("unit-offline", "inactive", [37.6173, 55.7558], "Альфа");
|
||||
const active = pointFact("unit-online", "active", [37.618, 55.756], "Янтарь");
|
||||
const runtimeBindings = [{
|
||||
bindingId: "positions",
|
||||
dataProductId: primaryBinding.dataProductId,
|
||||
slotId: "points",
|
||||
presentationProfileId: "moving",
|
||||
facts: [inactive, active],
|
||||
cursor: "1",
|
||||
state: "ready",
|
||||
}, {
|
||||
bindingId: "identity",
|
||||
dataProductId: joinedBinding.dataProductId,
|
||||
slotId: "subject-aspect",
|
||||
presentationProfileId: "moving",
|
||||
facts: [active],
|
||||
cursor: "1",
|
||||
state: "ready",
|
||||
}];
|
||||
|
||||
test("stable runtime identity uses binding, semantic type and source id only", () => {
|
||||
const moved = { ...active, geometry: { type: "Point", coordinates: [40, 60] } };
|
||||
assert.equal(mapRuntimeFactKey(active), "map.moving_object\u0000unit-online");
|
||||
assert.equal(mapRuntimeEntityId("positions", active), mapRuntimeEntityId("positions", moved));
|
||||
assert.equal(mapRuntimeEntityId("positions", active), "nodedc-runtime:positions:map.moving_object:unit-online");
|
||||
});
|
||||
|
||||
test("joined aspects enrich a primary subject and never become map entities", () => {
|
||||
assert.deepEqual(
|
||||
primaryMapRuntimeBindings(runtimeBindings, [joinedBinding, primaryBinding]).map((binding) => binding.bindingId),
|
||||
["positions"],
|
||||
);
|
||||
assert.deepEqual(
|
||||
buildSelectableMapEntities(runtimeBindings, [joinedBinding, primaryBinding], [profile]).map((entity) => entity.fact.sourceId),
|
||||
["unit-online", "unit-offline"],
|
||||
);
|
||||
});
|
||||
|
||||
test("object overview preserves the same MCP profile sort as facets and renderer", () => {
|
||||
const filters = { positions: { visible: true, facets: {} } };
|
||||
const selectable = buildSelectableMapEntities(runtimeBindings, [primaryBinding, joinedBinding], [profile]);
|
||||
const targets = buildFilteredMapTargets(runtimeBindings, [primaryBinding, joinedBinding], [profile], filters);
|
||||
|
||||
// Alphabetical title order would be Альфа, Янтарь. Declared state order is
|
||||
// active, inactive and must remain authoritative everywhere.
|
||||
assert.deepEqual(selectable.map((entity) => entity.title), ["Янтарь", "Альфа"]);
|
||||
assert.deepEqual(targets.map((target) => target.title), ["Янтарь", "Альфа"]);
|
||||
});
|
||||
|
||||
test("explicit empty facets remain match-nothing through workspace projection", () => {
|
||||
const subjectStates = {
|
||||
positions: {
|
||||
bindingId: "positions",
|
||||
visible: true,
|
||||
filters: { signal_state: [] },
|
||||
window: { open: false, rect: { x: 0, y: 0, width: 280, height: 260 }, maximized: false, zIndex: 20 },
|
||||
},
|
||||
};
|
||||
const filters = buildMapPresentationFilters(subjectStates, [primaryBinding], [profile]);
|
||||
assert.deepEqual(filters.positions.facets, { signal_state: [] });
|
||||
assert.deepEqual(buildFilteredMapTargets(runtimeBindings, [primaryBinding], [profile], filters), []);
|
||||
});
|
||||
|
||||
test("search reveal removes only constraints hiding the selected subject", () => {
|
||||
const address = localSectorAtGeodetic(origin, {
|
||||
lod: 1,
|
||||
originLatitude: origin.latitude,
|
||||
originLongitude: origin.longitude,
|
||||
stepMeters: 1_000,
|
||||
});
|
||||
const selectedSector = {
|
||||
...localSectorSummary(address, {
|
||||
lod: 1,
|
||||
originLatitude: origin.latitude,
|
||||
originLongitude: origin.longitude,
|
||||
stepMeters: 1_000,
|
||||
tileSizeMeters: 5_000,
|
||||
}),
|
||||
mode: "3d",
|
||||
units: "meters-enu",
|
||||
volume: null,
|
||||
};
|
||||
const entity = buildSelectableMapEntities(runtimeBindings, [primaryBinding], [profile])
|
||||
.find((candidate) => candidate.fact.sourceId === "unit-offline");
|
||||
const subjectState = {
|
||||
bindingId: "positions",
|
||||
visible: false,
|
||||
filters: { signal_state: ["active"] },
|
||||
window: { open: true, rect: { x: 10, y: 20, width: 280, height: 260 }, maximized: false, zIndex: 42 },
|
||||
};
|
||||
const reveal = planMapSubjectReveal({
|
||||
entity,
|
||||
subjectState,
|
||||
profile,
|
||||
selectedSector,
|
||||
gridProfiles: [gridProfile],
|
||||
origin,
|
||||
hideOutsideSector: true,
|
||||
scope: {
|
||||
excludedBindingIds: ["positions", "other"],
|
||||
excludedProviders: ["gelios", "other"],
|
||||
excludedObjectKinds: ["tracked_unit", "other"],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(reveal.subjectState.visible, true);
|
||||
assert.deepEqual(reveal.subjectState.filters, {});
|
||||
assert.deepEqual(reveal.subjectState.window, subjectState.window);
|
||||
assert.equal(reveal.hideOutsideSector, true);
|
||||
assert.deepEqual(reveal.scope, {
|
||||
excludedBindingIds: ["other"],
|
||||
excludedProviders: ["other"],
|
||||
excludedObjectKinds: ["other"],
|
||||
});
|
||||
});
|
||||
|
||||
test("search keeps the active sector but relaxes outside clipping for an outside subject", () => {
|
||||
const address = localSectorAtGeodetic(origin, {
|
||||
lod: 1,
|
||||
originLatitude: origin.latitude,
|
||||
originLongitude: origin.longitude,
|
||||
stepMeters: 1_000,
|
||||
});
|
||||
const selectedSector = {
|
||||
...localSectorSummary(address, {
|
||||
lod: 1,
|
||||
originLatitude: origin.latitude,
|
||||
originLongitude: origin.longitude,
|
||||
stepMeters: 1_000,
|
||||
tileSizeMeters: 5_000,
|
||||
}),
|
||||
mode: "3d",
|
||||
units: "meters-enu",
|
||||
volume: null,
|
||||
};
|
||||
const outsideEntity = {
|
||||
...buildSelectableMapEntities(runtimeBindings, [primaryBinding], [profile])[0],
|
||||
fact: pointFact("far-away", "active", [50, 60]),
|
||||
};
|
||||
const subjectState = {
|
||||
bindingId: "positions",
|
||||
visible: true,
|
||||
filters: {},
|
||||
window: { open: false, rect: { x: 0, y: 0, width: 280, height: 260 }, maximized: false, zIndex: 20 },
|
||||
};
|
||||
const reveal = planMapSubjectReveal({
|
||||
entity: outsideEntity,
|
||||
subjectState,
|
||||
profile,
|
||||
selectedSector,
|
||||
gridProfiles: [gridProfile],
|
||||
origin,
|
||||
hideOutsideSector: true,
|
||||
scope: { excludedBindingIds: [], excludedProviders: [], excludedObjectKinds: [] },
|
||||
});
|
||||
|
||||
assert.equal(reveal.hideOutsideSector, false);
|
||||
assert.equal(selectedSector.id, reveal ? selectedSector.id : null);
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { createMapWorkspaceState, mapWorkspaceReducer } from "../apps/catalog/src/mapWorkspaceState.mjs";
|
||||
|
||||
const rect = (x, y, width = 280, height = 260) => ({ x, y, width, height });
|
||||
const subjectStates = {
|
||||
positions: {
|
||||
bindingId: "positions",
|
||||
visible: true,
|
||||
filters: {},
|
||||
window: { open: false, rect: rect(24, 56), maximized: false, zIndex: 20 },
|
||||
},
|
||||
zones: {
|
||||
bindingId: "zones",
|
||||
visible: true,
|
||||
filters: {},
|
||||
window: { open: false, rect: rect(52, 84), maximized: false, zIndex: 21 },
|
||||
},
|
||||
};
|
||||
|
||||
function initialState() {
|
||||
return createMapWorkspaceState({
|
||||
subjectStates: structuredClone(subjectStates),
|
||||
sectorWindowRect: rect(24, 72, 380, 530),
|
||||
subjectCardRect: rect(940, 72, 390, 520),
|
||||
});
|
||||
}
|
||||
|
||||
test("one reducer owns the complete floating-window stack", () => {
|
||||
const opened = mapWorkspaceReducer(initialState(), { type: "open-subject-window", bindingId: "positions" });
|
||||
assert.equal(opened.subjectStates.positions.window.open, true);
|
||||
assert.equal(opened.activeWindowId, "binding:positions");
|
||||
assert.ok(opened.subjectStates.positions.window.zIndex > opened.sector.window.zIndex);
|
||||
|
||||
const activatedCard = mapWorkspaceReducer(opened, {
|
||||
type: "select-entity",
|
||||
entityId: "nodedc-runtime:positions:map.moving_object:unit-1",
|
||||
validTabIds: ["overview", "position"],
|
||||
defaultTabId: "overview",
|
||||
});
|
||||
assert.equal(activatedCard.subjectCard.open, true);
|
||||
assert.equal(activatedCard.activeWindowId, "subject-card");
|
||||
assert.ok(activatedCard.subjectCard.zIndex > opened.subjectStates.positions.window.zIndex);
|
||||
|
||||
// Re-activating the active window is stable and does not inflate z-index.
|
||||
assert.equal(
|
||||
mapWorkspaceReducer(activatedCard, { type: "activate-window", windowId: "subject-card" }),
|
||||
activatedCard,
|
||||
);
|
||||
});
|
||||
|
||||
test("selecting a domain subject never clears the active grid sector", () => {
|
||||
const sector = { id: "grid/local/sector", lod: 2, mode: "3d" };
|
||||
const withSector = mapWorkspaceReducer(initialState(), { type: "select-sector", selection: sector });
|
||||
const withSubject = mapWorkspaceReducer(withSector, {
|
||||
type: "select-entity",
|
||||
entityId: "nodedc-runtime:positions:map.moving_object:unit-1",
|
||||
validTabIds: ["overview"],
|
||||
defaultTabId: "overview",
|
||||
});
|
||||
|
||||
assert.equal(withSubject.selectedSector, sector);
|
||||
assert.equal(withSubject.selectedEntityId, "nodedc-runtime:positions:map.moving_object:unit-1");
|
||||
});
|
||||
|
||||
test("sector deactivation clears only sector scope and keeps subject UI state", () => {
|
||||
let state = mapWorkspaceReducer(initialState(), { type: "select-sector", selection: { id: "sector", lod: 1, mode: "3d" } });
|
||||
state = mapWorkspaceReducer(state, { type: "set-sector-scope-enabled", dimension: "binding", value: "positions", enabled: false });
|
||||
state = mapWorkspaceReducer(state, { type: "open-subject-window", bindingId: "positions" });
|
||||
state = mapWorkspaceReducer(state, { type: "deactivate-sector" });
|
||||
|
||||
assert.equal(state.selectedSector, null);
|
||||
assert.deepEqual(state.sector.scope, {
|
||||
excludedBindingIds: [],
|
||||
excludedProviders: [],
|
||||
excludedObjectKinds: [],
|
||||
});
|
||||
assert.equal(state.subjectStates.positions.window.open, true);
|
||||
assert.equal(state.subjectStates.positions.visible, true);
|
||||
});
|
||||
|
||||
test("search reveal state is applied atomically without replacing window geometry", () => {
|
||||
const state = initialState();
|
||||
const originalWindow = state.subjectStates.positions.window;
|
||||
const revealedSubject = {
|
||||
...state.subjectStates.positions,
|
||||
visible: true,
|
||||
filters: { signal_state: ["inactive"] },
|
||||
};
|
||||
const next = mapWorkspaceReducer(state, {
|
||||
type: "reveal-subject",
|
||||
bindingId: "positions",
|
||||
subjectState: revealedSubject,
|
||||
hideOutsideSector: false,
|
||||
scope: { excludedBindingIds: [], excludedProviders: ["other"], excludedObjectKinds: [] },
|
||||
});
|
||||
|
||||
assert.deepEqual(next.subjectStates.positions.window, originalWindow);
|
||||
assert.deepEqual(next.subjectStates.positions.filters, { signal_state: ["inactive"] });
|
||||
assert.deepEqual(next.sector.scope.excludedProviders, ["other"]);
|
||||
});
|
||||
Reference in New Issue
Block a user