mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
* feat(deep-dive): clickable sector rows with route visualization and bypass corridors
- Trade Exposure sector rows expand on click to show route detail
- Route path: origin > chokepoints > destination from trade-routes config
- Top 3 bypass corridors load lazily on expand (PRO-gated)
- Map highlights selected route (bright arc, others dim to alpha 40)
- Map zooms to fit highlighted route segments
- Auto-minimizes panel if maximized (so map is visible)
- Click again to collapse and restore map
* fix(deep-dive): show all matching routes in detail, fire gate hit on click not render
- buildRouteDetail now iterates all matching routes for a chokepoint,
rendering each as a labeled path (route name + waypoint chain)
- Replaced single matchingRoutes[0] path with full list under a
"Routes via <chokepoint>:" heading
- Moved trackGateHit('sector-bypass-corridors') from render-time to
a { once: true } click handler on the PRO gate placeholder
* fix(deep-dive): clear route highlight before switching sectors
Move clearHighlightedRoute() to the top of handleSectorRowClick()
so it runs unconditionally before any branching. Previously it only
fired when collapsing the current selection, leaving a stale highlight
when switching to a new sector with no matching routes.
* test(deep-dive): add sector route explorer integration tests
Static analysis tests verifying the route highlighting pipeline across
DeckGLMap, MapContainer, and CountryDeepDivePanel. Covers method existence,
dispatch wiring, cleanup in reset, XSS sanitization, and data consistency.
* fix(tests): add missing stubs for sector route explorer imports
The country-deep-dive-panel harness was missing stubs for escapeHtml,
createCircuitBreaker, loadFromStorage, saveToStorage, and new modules
added by sector route explorer (trade-routes, geo, analytics, supply-chain).
263 lines
9.0 KiB
JavaScript
263 lines
9.0 KiB
JavaScript
import { build } from 'esbuild';
|
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { dirname, join, resolve } from 'node:path';
|
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
import { createBrowserEnvironment } from './runtime-config-panel-harness.mjs';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const root = resolve(__dirname, '..', '..');
|
|
const entry = resolve(root, 'src/components/CountryDeepDivePanel.ts');
|
|
|
|
function snapshotGlobal(name) {
|
|
return {
|
|
exists: Object.prototype.hasOwnProperty.call(globalThis, name),
|
|
value: globalThis[name],
|
|
};
|
|
}
|
|
|
|
function restoreGlobal(name, snapshot) {
|
|
if (snapshot.exists) {
|
|
Object.defineProperty(globalThis, name, {
|
|
configurable: true,
|
|
writable: true,
|
|
value: snapshot.value,
|
|
});
|
|
return;
|
|
}
|
|
delete globalThis[name];
|
|
}
|
|
|
|
function defineGlobal(name, value) {
|
|
Object.defineProperty(globalThis, name, {
|
|
configurable: true,
|
|
writable: true,
|
|
value,
|
|
});
|
|
}
|
|
|
|
async function loadCountryDeepDivePanel() {
|
|
const tempDir = mkdtempSync(join(tmpdir(), 'wm-country-deep-dive-'));
|
|
const outfile = join(tempDir, 'CountryDeepDivePanel.bundle.mjs');
|
|
|
|
const stubModules = new Map([
|
|
['feeds-stub', `
|
|
export function getSourcePropagandaRisk() {
|
|
return { stateAffiliated: '' };
|
|
}
|
|
export function getSourceTier() {
|
|
return 2;
|
|
}
|
|
`],
|
|
['country-geometry-stub', `
|
|
export function getCountryCentroid() {
|
|
return null;
|
|
}
|
|
export const ME_STRIKE_BOUNDS = [];
|
|
`],
|
|
['i18n-stub', `
|
|
export function t(key, params) {
|
|
if (params && typeof params.count === 'number') {
|
|
return key + ':' + params.count;
|
|
}
|
|
return key;
|
|
}
|
|
`],
|
|
['related-assets-stub', `
|
|
export function getNearbyInfrastructure() {
|
|
return [];
|
|
}
|
|
export function getCountryInfrastructure() {
|
|
return [];
|
|
}
|
|
export function haversineDistanceKm() {
|
|
return 0;
|
|
}
|
|
`],
|
|
['sanitize-stub', `
|
|
export function sanitizeUrl(value) { return value ?? ''; }
|
|
export function escapeHtml(value) { return value ?? ''; }
|
|
`],
|
|
['intel-brief-stub', `export function formatIntelBrief(value) { return value; }`],
|
|
['utils-stub', `
|
|
export function getCSSColor() { return '#44ff88'; }
|
|
export function createCircuitBreaker() { return { execute: (fn) => fn() }; }
|
|
export function loadFromStorage() { return null; }
|
|
export function saveToStorage() {}
|
|
`],
|
|
['country-flag-stub', `export function toFlagEmoji(code, fallback = '🌍') { return code ? ':' + code + ':' : fallback; }`],
|
|
['ports-stub', `export const PORTS = [];`],
|
|
['trade-routes-stub', `export function getChokepointRoutes() { return []; }`],
|
|
['geo-stub', `export const STRATEGIC_WATERWAYS = [];`],
|
|
['analytics-stub', `export function trackGateHit() {}`],
|
|
['supply-chain-stub', `
|
|
export function fetchBypassOptions() { return Promise.resolve({ corridors: [] }); }
|
|
export function getCountryChokepointIndex() { return null; }
|
|
`],
|
|
['runtime-stub', `
|
|
export function toApiUrl(path) { return path; }
|
|
export function isDesktopRuntime() { return false; }
|
|
export function getConfiguredWebApiBaseUrl() { return ''; }
|
|
`],
|
|
['intelligence-client-stub', `
|
|
export class IntelligenceServiceClient {}
|
|
`],
|
|
['panel-gating-stub', `
|
|
export function hasPremiumAccess() { return false; }
|
|
export function getPanelGateReason() { return 'none'; }
|
|
`],
|
|
['auth-state-stub', `
|
|
export function getAuthState() { return { user: null }; }
|
|
`],
|
|
['resilience-widget-stub', `
|
|
const state = globalThis.__wmCountryDeepDiveTestState;
|
|
export class ResilienceWidget {
|
|
constructor(code) {
|
|
this.code = code;
|
|
this.destroyCount = 0;
|
|
this.element = document.createElement('section');
|
|
this.element.className = 'resilience-widget-stub';
|
|
this.element.setAttribute('data-country-code', code);
|
|
this.element.textContent = 'Resilience ' + code;
|
|
state.widgets.push(this);
|
|
}
|
|
getElement() {
|
|
return this.element;
|
|
}
|
|
destroy() {
|
|
this.destroyCount += 1;
|
|
}
|
|
}
|
|
`],
|
|
]);
|
|
|
|
const aliasMap = new Map([
|
|
['@/config/feeds', 'feeds-stub'],
|
|
['@/services/country-geometry', 'country-geometry-stub'],
|
|
['@/services/i18n', 'i18n-stub'],
|
|
['@/services/related-assets', 'related-assets-stub'],
|
|
['@/utils/sanitize', 'sanitize-stub'],
|
|
['@/utils/format-intel-brief', 'intel-brief-stub'],
|
|
['@/utils', 'utils-stub'],
|
|
['@/utils/country-flag', 'country-flag-stub'],
|
|
['@/config/ports', 'ports-stub'],
|
|
['@/config/trade-routes', 'trade-routes-stub'],
|
|
['@/config/geo', 'geo-stub'],
|
|
['@/services/analytics', 'analytics-stub'],
|
|
['@/services/supply-chain', 'supply-chain-stub'],
|
|
['./ResilienceWidget', 'resilience-widget-stub'],
|
|
['@/services/runtime', 'runtime-stub'],
|
|
['@/generated/client/worldmonitor/intelligence/v1/service_client', 'intelligence-client-stub'],
|
|
['@/services/panel-gating', 'panel-gating-stub'],
|
|
['@/services/auth-state', 'auth-state-stub'],
|
|
]);
|
|
|
|
const plugin = {
|
|
name: 'country-deep-dive-test-stubs',
|
|
setup(buildApi) {
|
|
buildApi.onResolve({ filter: /.*/ }, (args) => {
|
|
const target = aliasMap.get(args.path);
|
|
return target ? { path: target, namespace: 'stub' } : null;
|
|
});
|
|
|
|
buildApi.onLoad({ filter: /.*/, namespace: 'stub' }, (args) => ({
|
|
contents: stubModules.get(args.path),
|
|
loader: 'js',
|
|
}));
|
|
},
|
|
};
|
|
|
|
const result = await build({
|
|
entryPoints: [entry],
|
|
bundle: true,
|
|
format: 'esm',
|
|
platform: 'browser',
|
|
target: 'es2020',
|
|
write: false,
|
|
plugins: [plugin],
|
|
});
|
|
|
|
writeFileSync(outfile, result.outputFiles[0].text, 'utf8');
|
|
|
|
const mod = await import(`${pathToFileURL(outfile).href}?t=${Date.now()}`);
|
|
return {
|
|
CountryDeepDivePanel: mod.CountryDeepDivePanel,
|
|
cleanupBundle() {
|
|
rmSync(tempDir, { recursive: true, force: true });
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function createCountryDeepDivePanelHarness() {
|
|
const originalGlobals = {
|
|
document: snapshotGlobal('document'),
|
|
window: snapshotGlobal('window'),
|
|
localStorage: snapshotGlobal('localStorage'),
|
|
requestAnimationFrame: snapshotGlobal('requestAnimationFrame'),
|
|
cancelAnimationFrame: snapshotGlobal('cancelAnimationFrame'),
|
|
navigator: snapshotGlobal('navigator'),
|
|
HTMLElement: snapshotGlobal('HTMLElement'),
|
|
HTMLButtonElement: snapshotGlobal('HTMLButtonElement'),
|
|
};
|
|
const browserEnvironment = createBrowserEnvironment();
|
|
const state = { widgets: [] };
|
|
|
|
defineGlobal('document', browserEnvironment.document);
|
|
defineGlobal('window', browserEnvironment.window);
|
|
defineGlobal('localStorage', browserEnvironment.localStorage);
|
|
defineGlobal('requestAnimationFrame', browserEnvironment.requestAnimationFrame);
|
|
defineGlobal('cancelAnimationFrame', browserEnvironment.cancelAnimationFrame);
|
|
defineGlobal('navigator', browserEnvironment.window.navigator);
|
|
defineGlobal('HTMLElement', browserEnvironment.HTMLElement);
|
|
defineGlobal('HTMLButtonElement', browserEnvironment.HTMLButtonElement);
|
|
globalThis.__wmCountryDeepDiveTestState = state;
|
|
|
|
let CountryDeepDivePanel;
|
|
let cleanupBundle;
|
|
try {
|
|
({ CountryDeepDivePanel, cleanupBundle } = await loadCountryDeepDivePanel());
|
|
} catch (error) {
|
|
delete globalThis.__wmCountryDeepDiveTestState;
|
|
restoreGlobal('document', originalGlobals.document);
|
|
restoreGlobal('window', originalGlobals.window);
|
|
restoreGlobal('localStorage', originalGlobals.localStorage);
|
|
restoreGlobal('requestAnimationFrame', originalGlobals.requestAnimationFrame);
|
|
restoreGlobal('cancelAnimationFrame', originalGlobals.cancelAnimationFrame);
|
|
restoreGlobal('navigator', originalGlobals.navigator);
|
|
restoreGlobal('HTMLElement', originalGlobals.HTMLElement);
|
|
restoreGlobal('HTMLButtonElement', originalGlobals.HTMLButtonElement);
|
|
throw error;
|
|
}
|
|
|
|
function createPanel() {
|
|
return new CountryDeepDivePanel(null);
|
|
}
|
|
|
|
function getPanelRoot() {
|
|
return browserEnvironment.document.getElementById('country-deep-dive-panel');
|
|
}
|
|
|
|
function cleanup() {
|
|
cleanupBundle();
|
|
delete globalThis.__wmCountryDeepDiveTestState;
|
|
restoreGlobal('document', originalGlobals.document);
|
|
restoreGlobal('window', originalGlobals.window);
|
|
restoreGlobal('localStorage', originalGlobals.localStorage);
|
|
restoreGlobal('requestAnimationFrame', originalGlobals.requestAnimationFrame);
|
|
restoreGlobal('cancelAnimationFrame', originalGlobals.cancelAnimationFrame);
|
|
restoreGlobal('navigator', originalGlobals.navigator);
|
|
restoreGlobal('HTMLElement', originalGlobals.HTMLElement);
|
|
restoreGlobal('HTMLButtonElement', originalGlobals.HTMLButtonElement);
|
|
}
|
|
|
|
return {
|
|
createPanel,
|
|
document: browserEnvironment.document,
|
|
getPanelRoot,
|
|
getWidgets() {
|
|
return state.widgets;
|
|
},
|
|
cleanup,
|
|
};
|
|
}
|