mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
* fix(resilience): sanitize resilienceScore on non-DeckGL renderers Strip resilienceScore from layer state when DeckGL is not active (mobile/SVG/globe). Prevents bookmark/URL state leaking an invisible active layer on renderers that have no resilience choropleth path. Applied at two levels: - Constructor: strip from initialState before init - setLayers: strip on every layer update for renderer switches * fix(resilience): sync sanitized layer state to app context and storage MapContainer sanitization was local-only. App state (ctx.mapLayers) and localStorage still had resilienceScore=true on non-DeckGL renderers, causing data-loader to schedule unnecessary fetches. Fix: strip resilienceScore from ctx.mapLayers and storage at two points: - After MapContainer construction (initial hydration sync-back) - In panel-layout setLayers (runtime layer updates) * fix(resilience): guard search-manager toggle and enableLayer for non-DeckGL - search-manager.ts: prevent resilienceScore from being set to true in ctx.mapLayers when DeckGL is not active (generic layer toggle path) - MapContainer.enableLayer: early-return for resilienceScore on non-DeckGL renderers to prevent SVG/globe onLayerChange from reinforcing stale state * fix(resilience): sync ctx.mapLayers after renderer mode switch switchToGlobe/switchToFlat bypasses panel-layout, so ctx.mapLayers was not synced after MapContainer internally sanitized the layer state. Add post-switch sync in event-handlers to strip resilienceScore from app state and storage when switching to a non-DeckGL renderer. * test(resilience): add regression coverage for non-DeckGL sanitization 5 new tests covering the resilienceScore sanitization invariant: - strips on non-DeckGL renderer - preserves on DeckGL renderer - doesn't affect other layers - URL restore + normalize + sanitize chain - mode switch from DeckGL to globe
237 lines
8.2 KiB
TypeScript
237 lines
8.2 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { describe, it } from 'node:test';
|
|
|
|
import { PREMIUM_RPC_PATHS } from '../src/shared/premium-paths';
|
|
import {
|
|
LAYER_REGISTRY,
|
|
getAllowedLayerKeys,
|
|
} from '../src/config/map-layer-definitions';
|
|
import {
|
|
RESILIENCE_CHOROPLETH_COLORS,
|
|
buildResilienceChoroplethMap,
|
|
formatResilienceChoroplethLevel,
|
|
getResilienceChoroplethLevel,
|
|
normalizeExclusiveChoropleths,
|
|
} from '../src/components/resilience-choropleth-utils';
|
|
|
|
describe('resilience map layer contracts', () => {
|
|
it('registers resilience RPCs as premium paths', () => {
|
|
assert.ok(PREMIUM_RPC_PATHS.has('/api/resilience/v1/get-resilience-score'));
|
|
assert.ok(PREMIUM_RPC_PATHS.has('/api/resilience/v1/get-resilience-ranking'));
|
|
});
|
|
|
|
it('registers resilienceScore as a locked flat layer in every variant', () => {
|
|
assert.equal(LAYER_REGISTRY.resilienceScore.renderers.join(','), 'flat');
|
|
assert.equal(LAYER_REGISTRY.resilienceScore.premium, 'locked');
|
|
|
|
for (const variant of ['full', 'tech', 'finance', 'happy', 'commodity'] as const) {
|
|
assert.ok(getAllowedLayerKeys(variant).has('resilienceScore'));
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('resilience choropleth thresholds', () => {
|
|
it('maps scores to the expected five-level scale', () => {
|
|
assert.equal(getResilienceChoroplethLevel(10), 'very_low');
|
|
assert.equal(getResilienceChoroplethLevel(25), 'low');
|
|
assert.equal(getResilienceChoroplethLevel(45), 'moderate');
|
|
assert.equal(getResilienceChoroplethLevel(65), 'high');
|
|
assert.equal(getResilienceChoroplethLevel(85), 'very_high');
|
|
});
|
|
|
|
it('formats labels and keeps stable fill colors', () => {
|
|
assert.equal(formatResilienceChoroplethLevel('very_high'), 'very high');
|
|
assert.deepEqual(RESILIENCE_CHOROPLETH_COLORS.very_low, [239, 68, 68, 160]);
|
|
assert.deepEqual(RESILIENCE_CHOROPLETH_COLORS.very_high, [34, 197, 94, 160]);
|
|
});
|
|
|
|
it('filters placeholder ranking rows and normalizes valid items', () => {
|
|
const scores = buildResilienceChoroplethMap([
|
|
{ countryCode: 'NO', overallScore: 82, level: 'high', lowConfidence: false },
|
|
{ countryCode: 'US', overallScore: 61.234, level: 'medium', lowConfidence: true },
|
|
{ countryCode: 'YE', overallScore: -1, level: 'unknown', lowConfidence: true },
|
|
]);
|
|
|
|
assert.equal(scores.size, 2);
|
|
assert.deepEqual(scores.get('NO'), {
|
|
overallScore: 82,
|
|
level: 'very_high',
|
|
serverLevel: 'high',
|
|
lowConfidence: false,
|
|
});
|
|
assert.deepEqual(scores.get('US'), {
|
|
overallScore: 61.2,
|
|
level: 'high',
|
|
serverLevel: 'medium',
|
|
lowConfidence: true,
|
|
});
|
|
assert.equal(scores.has('YE'), false);
|
|
});
|
|
});
|
|
|
|
describe('resilience non-DeckGL sanitization', () => {
|
|
function simulateSanitize(layers: Record<string, boolean>, isDeckGLActive: boolean) {
|
|
if (layers.resilienceScore && !isDeckGLActive) {
|
|
return { ...layers, resilienceScore: false };
|
|
}
|
|
return { ...layers };
|
|
}
|
|
|
|
it('strips resilienceScore from layer state when DeckGL is not active', () => {
|
|
const layers = { ...baseLayers(), resilienceScore: true };
|
|
const result = simulateSanitize(layers, false);
|
|
assert.equal(result.resilienceScore, false);
|
|
});
|
|
|
|
it('preserves resilienceScore when DeckGL is active', () => {
|
|
const layers = { ...baseLayers(), resilienceScore: true };
|
|
const result = simulateSanitize(layers, true);
|
|
assert.equal(result.resilienceScore, true);
|
|
});
|
|
|
|
it('does not affect other layers when stripping resilienceScore', () => {
|
|
const layers = { ...baseLayers(), resilienceScore: true, ciiChoropleth: true, flights: true };
|
|
const result = simulateSanitize(layers, false);
|
|
assert.equal(result.resilienceScore, false);
|
|
assert.equal(result.ciiChoropleth, true);
|
|
assert.equal(result.flights, true);
|
|
});
|
|
|
|
it('URL restore with resilienceScore=true on non-DeckGL produces false in sanitized state', () => {
|
|
const urlLayers = { ...baseLayers(), resilienceScore: true };
|
|
const normalized = normalizeExclusiveChoropleths(urlLayers, null);
|
|
const sanitized = simulateSanitize(normalized, false);
|
|
assert.equal(sanitized.resilienceScore, false);
|
|
});
|
|
|
|
it('mode switch from DeckGL to globe strips resilienceScore', () => {
|
|
const deckGlState = { ...baseLayers(), resilienceScore: true };
|
|
const afterSwitch = simulateSanitize(deckGlState, false);
|
|
assert.equal(afterSwitch.resilienceScore, false);
|
|
});
|
|
|
|
function baseLayers() {
|
|
return {
|
|
conflicts: false, bases: false, cables: false, pipelines: false,
|
|
hotspots: false, ais: false, nuclear: false, irradiators: false,
|
|
radiationWatch: false, sanctions: false, weather: false, economic: false,
|
|
waterways: false, outages: false, cyberThreats: false, datacenters: false,
|
|
protests: false, flights: false, military: false, natural: false,
|
|
spaceports: false, minerals: false, fires: false, ucdpEvents: false,
|
|
displacement: false, climate: false, startupHubs: false, cloudRegions: false,
|
|
accelerators: false, techHQs: false, techEvents: false, stockExchanges: false,
|
|
financialCenters: false, centralBanks: false, commodityHubs: false,
|
|
gulfInvestments: false, positiveEvents: false, kindness: false,
|
|
happiness: false, speciesRecovery: false, renewableInstallations: false,
|
|
tradeRoutes: false, iranAttacks: false, gpsJamming: false, satellites: false,
|
|
ciiChoropleth: false, resilienceScore: false, dayNight: false,
|
|
miningSites: false, processingPlants: false, commodityPorts: false,
|
|
webcams: false, weatherRadar: false, diseaseOutbreaks: false,
|
|
};
|
|
}
|
|
});
|
|
|
|
describe('resilience choropleth exclusivity', () => {
|
|
function baseLayers() {
|
|
return {
|
|
conflicts: false,
|
|
bases: false,
|
|
cables: false,
|
|
pipelines: false,
|
|
hotspots: false,
|
|
ais: false,
|
|
nuclear: false,
|
|
irradiators: false,
|
|
radiationWatch: false,
|
|
sanctions: false,
|
|
weather: false,
|
|
economic: false,
|
|
waterways: false,
|
|
outages: false,
|
|
cyberThreats: false,
|
|
datacenters: false,
|
|
protests: false,
|
|
flights: false,
|
|
military: false,
|
|
natural: false,
|
|
spaceports: false,
|
|
minerals: false,
|
|
fires: false,
|
|
ucdpEvents: false,
|
|
displacement: false,
|
|
climate: false,
|
|
startupHubs: false,
|
|
cloudRegions: false,
|
|
accelerators: false,
|
|
techHQs: false,
|
|
techEvents: false,
|
|
stockExchanges: false,
|
|
financialCenters: false,
|
|
centralBanks: false,
|
|
commodityHubs: false,
|
|
gulfInvestments: false,
|
|
positiveEvents: false,
|
|
kindness: false,
|
|
happiness: false,
|
|
speciesRecovery: false,
|
|
renewableInstallations: false,
|
|
tradeRoutes: false,
|
|
iranAttacks: false,
|
|
gpsJamming: false,
|
|
satellites: false,
|
|
ciiChoropleth: false,
|
|
resilienceScore: false,
|
|
dayNight: false,
|
|
miningSites: false,
|
|
processingPlants: false,
|
|
commodityPorts: false,
|
|
webcams: false,
|
|
weatherRadar: false,
|
|
diseaseOutbreaks: false,
|
|
};
|
|
}
|
|
|
|
it('keeps ciiChoropleth as the fallback when both choropleths arrive enabled without previous state', () => {
|
|
const layers = normalizeExclusiveChoropleths({
|
|
...baseLayers(),
|
|
ciiChoropleth: true,
|
|
resilienceScore: true,
|
|
});
|
|
|
|
assert.equal(layers.resilienceScore, false);
|
|
assert.equal(layers.ciiChoropleth, true);
|
|
});
|
|
|
|
it('preserves resilienceScore when it is the newly enabled choropleth', () => {
|
|
const previousLayers = {
|
|
...baseLayers(),
|
|
ciiChoropleth: true,
|
|
resilienceScore: false,
|
|
};
|
|
const layers = normalizeExclusiveChoropleths({
|
|
...baseLayers(),
|
|
ciiChoropleth: true,
|
|
resilienceScore: true,
|
|
}, previousLayers);
|
|
|
|
assert.equal(layers.resilienceScore, true);
|
|
assert.equal(layers.ciiChoropleth, false);
|
|
});
|
|
|
|
it('preserves ciiChoropleth when it is the newly enabled choropleth', () => {
|
|
const previousLayers = {
|
|
...baseLayers(),
|
|
ciiChoropleth: false,
|
|
resilienceScore: true,
|
|
};
|
|
const layers = normalizeExclusiveChoropleths({
|
|
...baseLayers(),
|
|
ciiChoropleth: true,
|
|
resilienceScore: true,
|
|
}, previousLayers);
|
|
|
|
assert.equal(layers.resilienceScore, false);
|
|
assert.equal(layers.ciiChoropleth, true);
|
|
});
|
|
});
|