Files
worldmonitor/tests/variant-layer-guardrail.test.mjs
Elie Habib f2c83e59b1 fix(map): prevent ghost layers that render without a toggle (#1264)
* fix(map): prevent ghost layers that render without a toggle

Layers enabled in variant defaults but missing from VARIANT_LAYER_ORDER
rendered on the map with no UI toggle to turn them off. Commodity variant
had 6 ghost layers including undersea cables.

Add sanitizeLayersForVariant() guardrail that forces any layer not in
the variant's allowed list to false — applied at both fresh init and
localStorage load. Replace the fragile happy-only hardcoded blocklist
with this generic mechanism.

Add regression test covering all 10 variant×platform combinations.

* fix(map): sanctions renderers + sanitize URL-derived layers

- Fix sanctions layer having empty renderers[] — getLayersForVariant
  filtered it out so it had no toggle despite being in VARIANT_LAYER_ORDER
- Apply sanitizeLayersForVariant() after URL state merge, replacing
  fragile hardcoded tech/happy blocklists — deeplinks can no longer
  enable layers outside the variant's allowed set
- Add renderer coverage to guardrail test (11 cases)

* fix(map): remove no-op sanctions toggle + guard search-manager layer mutations

- sanctions has no DeckGL/Globe renderer (only SVG map country fills),
  so revert renderers to [] and remove from VARIANT_LAYER_ORDER — no
  more no-op toggle in desktop mode
- Set sanctions defaults to false across all variants (SVG map has its
  own toggle list independent of VARIANT_LAYER_ORDER)
- Guard search-manager layer commands (layers:all, presets, layer:*)
  against enabling off-variant layers via getAllowedLayerKeys()
- Add renderer-coverage assertion to guardrail test

* fix(map): make layer sanitizer renderer-aware for SVG-only layers

sanctions is implemented in SVG map (country fills) but not DeckGL/Globe.
Previous commit removed it from VARIANT_LAYER_ORDER, causing the sanitizer
to strip it on reload — breaking SVG map state persistence.

Add SVG_ONLY_LAYERS concept: layers allowed by the sanitizer but excluded
from DeckGL/Globe toggle UI. sanctions restored to defaults for full,
finance, and commodity variants where the SVG map exposes it.

- getAllowedLayerKeys() now includes SVG_ONLY_LAYERS
- VARIANT_LAYER_ORDER remains DeckGL/Globe-only (renderer test enforced)
- Guardrail test updated to check both sources
2026-03-08 14:14:16 +04:00

125 lines
4.7 KiB
JavaScript

/**
* Guardrail: every layer enabled by default in a variant's MapLayers
* MUST be in VARIANT_LAYER_ORDER (DeckGL/Globe toggle) or SVG_ONLY_LAYERS
* (SVG fallback toggle). Layers in VARIANT_LAYER_ORDER must have at least
* one DeckGL/Globe renderer so getLayersForVariant() returns them.
*
* Without this, layers render but have no UI toggle → users can't turn them off.
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
const SRC = new URL('../src/config/', import.meta.url);
const layerDefsSource = readFileSync(new URL('map-layer-definitions.ts', SRC), 'utf8');
const panelsSource = readFileSync(new URL('panels.ts', SRC), 'utf8');
function extractRecordBlock(source, name) {
const re = new RegExp(`(?:const|export const)\\s+${name}[^=]*=\\s*\\{([\\s\\S]*?)\\n\\};`);
const match = source.match(re);
if (!match) return null;
const body = match[1];
const result = {};
const variantRe = /(\w+):\s*\[([\s\S]*?)\]/g;
let m;
while ((m = variantRe.exec(body)) !== null) {
const keys = m[2].match(/'(\w+)'/g)?.map(s => s.replace(/'/g, '')) ?? [];
result[m[1]] = new Set(keys);
}
return result;
}
function extractLayerRenderers(source) {
const result = {};
const registryMatch = source.match(/LAYER_REGISTRY[^=]*=\s*\{([\s\S]*?)\n\};/);
if (!registryMatch) throw new Error('Could not find LAYER_REGISTRY');
const body = registryMatch[1];
const defRe = /def\(\s*'(\w+)'[^)]*\)/g;
let m;
while ((m = defRe.exec(body)) !== null) {
const key = m[1];
const fullCall = m[0];
const renderersMatch = fullCall.match(/\[([^\]]*)\]\s*(?:,\s*(?:'[^']*'|undefined))?\s*\)/);
if (renderersMatch) {
const renderers = renderersMatch[1].match(/'(\w+)'/g)?.map(s => s.replace(/'/g, '')) ?? [];
result[key] = renderers;
} else {
result[key] = ['flat', 'globe'];
}
}
return result;
}
function extractEnabledLayers(source, constName) {
const re = new RegExp(`const ${constName}[^=]*=\\s*\\{([\\s\\S]*?)\\};`);
const match = source.match(re);
if (!match) throw new Error(`Could not find ${constName}`);
const enabled = [];
const lineRe = /(\w+):\s*true/g;
let m;
while ((m = lineRe.exec(match[1])) !== null) {
enabled.push(m[1]);
}
return enabled;
}
const variantOrder = extractRecordBlock(layerDefsSource, 'VARIANT_LAYER_ORDER');
const svgOnlyLayers = extractRecordBlock(layerDefsSource, 'SVG_ONLY_LAYERS') ?? {};
const layerRenderers = extractLayerRenderers(layerDefsSource);
function getAllowedForVariant(variant) {
const allowed = new Set(variantOrder[variant] ?? []);
for (const k of svgOnlyLayers[variant] ?? []) allowed.add(k);
return allowed;
}
const VARIANT_DEFAULTS = {
full: { desktop: 'FULL_MAP_LAYERS', mobile: 'FULL_MOBILE_MAP_LAYERS' },
tech: { desktop: 'TECH_MAP_LAYERS', mobile: 'TECH_MOBILE_MAP_LAYERS' },
finance: { desktop: 'FINANCE_MAP_LAYERS', mobile: 'FINANCE_MOBILE_MAP_LAYERS' },
happy: { desktop: 'HAPPY_MAP_LAYERS', mobile: 'HAPPY_MOBILE_MAP_LAYERS' },
commodity: { desktop: 'COMMODITY_MAP_LAYERS', mobile: 'COMMODITY_MOBILE_MAP_LAYERS' },
};
describe('variant layer guardrail', () => {
for (const [variant, { desktop, mobile }] of Object.entries(VARIANT_DEFAULTS)) {
const allowed = getAllowedForVariant(variant);
if (allowed.size === 0) continue;
it(`${variant} desktop: no enabled layer without a toggle`, () => {
const enabled = extractEnabledLayers(panelsSource, desktop);
const orphans = enabled.filter(k => !allowed.has(k));
assert.deepStrictEqual(
orphans, [],
`${variant} desktop has layers enabled but NOT in VARIANT_LAYER_ORDER or SVG_ONLY_LAYERS (no toggle): ${orphans.join(', ')}`,
);
});
it(`${variant} mobile: no enabled layer without a toggle`, () => {
const enabled = extractEnabledLayers(panelsSource, mobile);
const orphans = enabled.filter(k => !allowed.has(k));
assert.deepStrictEqual(
orphans, [],
`${variant} mobile has layers enabled but NOT in VARIANT_LAYER_ORDER or SVG_ONLY_LAYERS (no toggle): ${orphans.join(', ')}`,
);
});
}
it('every layer in VARIANT_LAYER_ORDER has at least one DeckGL/Globe renderer', () => {
const noRenderer = [];
for (const [variant, keys] of Object.entries(variantOrder)) {
for (const key of keys) {
const renderers = layerRenderers[key];
if (!renderers || renderers.length === 0) {
noRenderer.push(`${variant}:${key}`);
}
}
}
assert.deepStrictEqual(
noRenderer, [],
`Layers in VARIANT_LAYER_ORDER with empty renderers (getLayersForVariant filters them out → no toggle): ${noRenderer.join(', ')}`,
);
});
});