GenerAI / worldmonitor /e2e /runtime-fetch.spec.ts
amogaddy's picture
Fix build: ripristinate blog-site/tests/e2e/pro-test/convex (referenziate dagli script di build) (part 2)
97ee7cb verified
Raw
History Blame Contribute Delete
70.9 kB
import { expect, test } from '@playwright/test';
test.describe('desktop runtime routing guardrails', () => {
test('detectDesktopRuntime covers packaged tauri hosts', async ({ page }) => {
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const runtime = await import('/src/services/runtime.ts');
return {
tauriHost: runtime.detectDesktopRuntime({
hasTauriGlobals: false,
userAgent: 'Mozilla/5.0',
locationProtocol: 'https:',
locationHost: 'tauri.localhost',
locationOrigin: 'https://tauri.localhost',
}),
tauriScheme: runtime.detectDesktopRuntime({
hasTauriGlobals: false,
userAgent: 'Mozilla/5.0',
locationProtocol: 'tauri:',
locationHost: '',
locationOrigin: 'tauri://localhost',
}),
tauriUa: runtime.detectDesktopRuntime({
hasTauriGlobals: false,
userAgent: 'Mozilla/5.0 Tauri/2.0',
locationProtocol: 'https:',
locationHost: 'example.com',
locationOrigin: 'https://example.com',
}),
tauriGlobal: runtime.detectDesktopRuntime({
hasTauriGlobals: true,
userAgent: 'Mozilla/5.0',
locationProtocol: 'https:',
locationHost: 'example.com',
locationOrigin: 'https://example.com',
}),
secureLocalhost: runtime.detectDesktopRuntime({
hasTauriGlobals: false,
userAgent: 'Mozilla/5.0',
locationProtocol: 'https:',
locationHost: 'localhost',
locationOrigin: 'https://localhost',
}),
insecureLocalhost: runtime.detectDesktopRuntime({
hasTauriGlobals: false,
userAgent: 'Mozilla/5.0',
locationProtocol: 'http:',
locationHost: 'localhost:5173',
locationOrigin: 'http://localhost:5173',
}),
webHost: runtime.detectDesktopRuntime({
hasTauriGlobals: false,
userAgent: 'Mozilla/5.0',
locationProtocol: 'https:',
locationHost: 'worldmonitor.app',
locationOrigin: 'https://worldmonitor.app',
}),
};
});
expect(result.tauriHost).toBe(true);
expect(result.tauriScheme).toBe(true);
expect(result.tauriUa).toBe(true);
expect(result.tauriGlobal).toBe(true);
expect(result.secureLocalhost).toBe(true);
expect(result.insecureLocalhost).toBe(false);
expect(result.webHost).toBe(false);
});
test('runtime fetch patch falls back to cloud for local failures', async ({ page }) => {
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const runtime = await import('/src/services/runtime.ts');
const runtimeConfig = await import('/src/services/runtime-config.ts');
const globalWindow = window as unknown as Record<string, unknown>;
const originalFetch = window.fetch.bind(window);
const calls: string[] = [];
const proxiedPaths: string[] = [];
const responseJson = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), {
status,
headers: { 'content-type': 'application/json' },
});
window.fetch = (async (input: RequestInfo | URL) => {
const url =
typeof input === 'string'
? input
: input instanceof URL
? input.toString()
: input.url;
calls.push(url);
if (url.includes('worldmonitor.app/api/fred-data')) {
return responseJson({ observations: [{ value: '321.5' }] }, 200);
}
if (url.includes('worldmonitor.app/api/stablecoin-markets')) {
return responseJson({ stablecoins: [{ symbol: 'USDT' }] }, 200);
}
return responseJson({ ok: true }, 200);
}) as typeof window.fetch;
const previousTauri = globalWindow.__TAURI__;
globalWindow.__TAURI__ = {
core: {
invoke: async (command: string, payload?: { request?: { path?: string } }) => {
if (command === 'proxy_local_api_request') {
const path = payload?.request?.path || '';
proxiedPaths.push(path);
if (path.startsWith('/api/fred-data')) {
return { status: 500, headers: { 'content-type': 'application/json' }, body: Array.from(new TextEncoder().encode('{"error":"missing local api key"}')) };
}
if (path.startsWith('/api/stablecoin-markets')) throw new Error('ECONNREFUSED');
}
return null;
},
},
};
delete globalWindow.__wmFetchPatched;
// Set a valid WM API key so cloud fallback is allowed
await runtimeConfig.setSecretValue('WORLDMONITOR_API_KEY' as import('/src/services/runtime-config.ts').RuntimeSecretKey, 'wm_test_key_1234567890abcdef');
try {
runtime.installRuntimeFetchPatch();
const fredResponse = await window.fetch('/api/fred-data?series_id=CPIAUCSL');
const fredBody = await fredResponse.json() as { observations?: Array<{ value: string }> };
const stableResponse = await window.fetch('/api/stablecoin-markets');
const stableBody = await stableResponse.json() as { stablecoins?: Array<{ symbol: string }> };
return {
fredStatus: fredResponse.status,
fredValue: fredBody.observations?.[0]?.value ?? null,
stableStatus: stableResponse.status,
stableSymbol: stableBody.stablecoins?.[0]?.symbol ?? null,
calls,
proxiedPaths,
};
} finally {
window.fetch = originalFetch;
delete globalWindow.__wmFetchPatched;
if (previousTauri === undefined) {
delete globalWindow.__TAURI__;
} else {
globalWindow.__TAURI__ = previousTauri;
}
await runtimeConfig.setSecretValue('WORLDMONITOR_API_KEY' as import('/src/services/runtime-config.ts').RuntimeSecretKey, '');
}
});
expect(result.fredStatus).toBe(200);
expect(result.fredValue).toBe('321.5');
expect(result.stableStatus).toBe(200);
expect(result.stableSymbol).toBe('USDT');
expect(result.calls.some((url) => url.includes('worldmonitor.app/api/fred-data'))).toBe(true);
expect(result.calls.some((url) => url.includes('worldmonitor.app/api/stablecoin-markets'))).toBe(true);
expect(result.proxiedPaths.some((path) => path.startsWith('/api/fred-data'))).toBe(true);
expect(result.proxiedPaths.some((path) => path.startsWith('/api/stablecoin-markets'))).toBe(true);
});
test('runtime fetch patch never sends blocked local-control endpoints to cloud', async ({ page }) => {
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const runtime = await import('/src/services/runtime.ts');
const globalWindow = window as unknown as Record<string, unknown>;
const originalFetch = window.fetch.bind(window);
const calls: string[] = [];
const proxiedPaths: string[] = [];
const previousTauri = globalWindow.__TAURI__;
globalWindow.__TAURI__ = {
core: {
invoke: async (command: string, payload?: { request?: { path?: string } }) => {
if (command === 'proxy_local_api_request') {
const path = payload?.request?.path || '';
proxiedPaths.push(path);
throw new Error(`native proxy rejected ${path}`);
}
return null;
},
},
};
delete globalWindow.__wmFetchPatched;
try {
runtime.installRuntimeFetchPatch();
let envUpdateError: string | null = null;
let validateError: string | null = null;
try {
await window.fetch('/api/local-env-update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'GROQ_API_KEY', value: 'sk-secret-value' }),
});
} catch (error) {
envUpdateError = error instanceof Error ? error.message : String(error);
}
try {
await window.fetch('/api/local-validate-secret', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'GROQ_API_KEY', value: 'sk-secret-value' }),
});
} catch (error) {
validateError = error instanceof Error ? error.message : String(error);
}
return {
envUpdateError,
validateError,
calls,
proxiedPaths,
};
} finally {
window.fetch = originalFetch;
delete globalWindow.__wmFetchPatched;
if (previousTauri === undefined) {
delete globalWindow.__TAURI__;
} else {
globalWindow.__TAURI__ = previousTauri;
}
}
});
expect(result.envUpdateError).toContain('native proxy rejected /api/local-env-update');
expect(result.validateError).toContain('native proxy rejected /api/local-validate-secret');
expect(result.proxiedPaths).toContain('/api/local-env-update');
expect(result.proxiedPaths).toContain('/api/local-validate-secret');
expect(result.calls.some((url) => url.includes('worldmonitor.app/api/local-env-update'))).toBe(false);
expect(result.calls.some((url) => url.includes('worldmonitor.app/api/local-validate-secret'))).toBe(false);
});
test('runtime fetch patch preserves Request abort signals', async ({ page }) => {
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const runtime = await import('/src/services/runtime.ts');
const globalWindow = window as unknown as Record<string, unknown>;
const previousTauri = globalWindow.__TAURI__;
const originalFetch = window.fetch.bind(window);
globalWindow.__TAURI__ = {
core: {
invoke: async () => {
await new Promise((resolve) => setTimeout(resolve, 50));
return { status: 200, headers: {}, body: [] };
},
},
};
delete globalWindow.__wmFetchPatched;
try {
const controller = new AbortController();
runtime.installRuntimeFetchPatch();
const request = window.fetch(new Request(`${window.location.origin}/api/fred-data`, { signal: controller.signal }));
controller.abort();
try {
await request;
return { name: null };
} catch (error) {
return { name: error instanceof DOMException ? error.name : null };
}
} finally {
window.fetch = originalFetch;
delete globalWindow.__wmFetchPatched;
if (previousTauri === undefined) {
delete globalWindow.__TAURI__;
} else {
globalWindow.__TAURI__ = previousTauri;
}
}
});
expect(result.name).toBe('AbortError');
});
test('chunk preload reload guard is one-shot until app boot clears it', async ({ page }) => {
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const {
buildChunkReloadStorageKey,
installChunkReloadGuard,
clearChunkReloadGuard,
} = await import('/src/bootstrap/chunk-reload.ts');
const listeners = new Map<string, Array<() => void>>();
const eventTarget = {
addEventListener: (type: string, listener: EventListenerOrEventListenerObject) => {
const list = listeners.get(type) ?? [];
list.push(() => {
if (typeof listener === 'function') {
listener(new Event(type));
} else {
listener.handleEvent(new Event(type));
}
});
listeners.set(type, list);
},
};
const storageMap = new Map<string, string>();
const storage = {
getItem: (key: string) => storageMap.get(key) ?? null,
setItem: (key: string, value: string) => {
storageMap.set(key, value);
},
removeItem: (key: string) => {
storageMap.delete(key);
},
};
const emit = (eventName: string) => {
const handlers = listeners.get(eventName) ?? [];
handlers.forEach((handler) => handler());
};
let reloadCount = 0;
const storageKey = installChunkReloadGuard('9.9.9', {
eventTarget,
storage,
eventName: 'preload-error',
reload: () => {
reloadCount += 1;
},
});
emit('preload-error');
emit('preload-error');
const reloadCountBeforeClear = reloadCount;
clearChunkReloadGuard(storageKey, storage);
emit('preload-error');
return {
storageKey,
expectedKey: buildChunkReloadStorageKey('9.9.9'),
reloadCountBeforeClear,
reloadCountAfterClear: reloadCount,
storedValue: storageMap.get(storageKey) ?? null,
};
});
expect(result.storageKey).toBe(result.expectedKey);
expect(result.reloadCountBeforeClear).toBe(1);
expect(result.reloadCountAfterClear).toBe(2);
expect(result.storedValue).toBe('1');
});
test('update badge picks architecture-correct desktop download url', async ({ page }) => {
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const { DesktopUpdater } = await import('/src/app/desktop-updater.ts');
const globalWindow = window as unknown as {
__TAURI__?: { core?: { invoke?: (command: string) => Promise<unknown> } };
};
const previousTauri = globalWindow.__TAURI__;
const releaseUrl = 'https://github.com/koala73/worldmonitor/releases/latest';
const updaterProto = DesktopUpdater.prototype as unknown as {
resolveUpdateDownloadUrl: (releaseUrl: string) => Promise<string>;
mapDesktopDownloadPlatform: (os: string, arch: string) => string | null;
getDesktopBuildVariant: () => 'full' | 'tech' | 'finance';
};
const fakeApp = {
mapDesktopDownloadPlatform: updaterProto.mapDesktopDownloadPlatform,
getDesktopBuildVariant: () => 'full' as const,
};
try {
globalWindow.__TAURI__ = {
core: {
invoke: async (command: string) => {
if (command !== 'get_desktop_runtime_info') throw new Error(`Unexpected command: ${command}`);
return { os: 'macos', arch: 'aarch64' };
},
},
};
const macArm = await updaterProto.resolveUpdateDownloadUrl.call(fakeApp, releaseUrl);
globalWindow.__TAURI__ = {
core: {
invoke: async () => ({ os: 'windows', arch: 'amd64' }),
},
};
const windowsX64 = await updaterProto.resolveUpdateDownloadUrl.call(fakeApp, releaseUrl);
globalWindow.__TAURI__ = {
core: {
invoke: async () => ({ os: 'linux', arch: 'x86_64' }),
},
};
const linuxFallback = await updaterProto.resolveUpdateDownloadUrl.call(fakeApp, releaseUrl);
return { macArm, windowsX64, linuxFallback };
} finally {
if (previousTauri === undefined) {
delete globalWindow.__TAURI__;
} else {
globalWindow.__TAURI__ = previousTauri;
}
}
});
expect(result.macArm).toBe('https://worldmonitor.app/api/download?platform=macos-arm64&variant=full');
expect(result.windowsX64).toBe('https://worldmonitor.app/api/download?platform=windows-exe&variant=full');
expect(result.linuxFallback).toBe('https://github.com/koala73/worldmonitor/releases/latest');
});
test('MapContainer paints a mobile shell before heavy map renderer resources', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const { DEFAULT_MAP_LAYERS } = await import('/src/config/index.ts');
const { initI18n } = await import('/src/services/i18n.ts');
await initI18n();
const isHeavyMapResource = (name: string): boolean =>
/DeckGLMap|GlobeMap|maplibre|deck-stack|globe\.gl|pmtiles|\.pmtiles|@deck\.gl|@luma\.gl|@loaders\.gl/i.test(name);
const heavyResourceNames = (): string[] =>
performance.getEntriesByType('resource')
.map((entry) => entry.name)
.filter(isHeavyMapResource);
const nextFrame = (): Promise<void> => new Promise((resolve) => requestAnimationFrame(() => resolve()));
const waitForRenderer = async (host: HTMLElement): Promise<void> => {
const deadline = performance.now() + 5_000;
while (!(
host.classList.contains('svg-mode')
|| host.classList.contains('deckgl-mode')
|| host.classList.contains('globe-mode')
)) {
if (performance.now() > deadline) throw new Error('Map renderer did not settle');
await nextFrame();
}
};
performance.clearResourceTimings();
const { MapContainer } = await import('/src/components/MapContainer.ts');
const heavyAfterMapContainerImport = heavyResourceNames();
const mapHost = document.createElement('div');
mapHost.className = 'map-container';
mapHost.style.width = '390px';
mapHost.style.height = '260px';
mapHost.style.position = 'relative';
document.body.appendChild(mapHost);
let map: InstanceType<typeof MapContainer> | null = null;
const originalGetContext = HTMLCanvasElement.prototype.getContext;
let webglProbeCalls = 0;
try {
HTMLCanvasElement.prototype.getContext = (function (
this: HTMLCanvasElement,
contextId: string,
options?: unknown
) {
if (contextId.toLowerCase().includes('webgl')) webglProbeCalls += 1;
return originalGetContext.call(this, contextId, options as never);
}) as typeof HTMLCanvasElement.prototype.getContext;
map = new MapContainer(mapHost, {
zoom: 2.5,
pan: { x: 0, y: 0 },
view: 'global',
layers: { ...DEFAULT_MAP_LAYERS },
timeRange: '7d',
});
const webglProbeCallsAtShell = webglProbeCalls;
const shellRect = mapHost.getBoundingClientRect();
const shellState = {
hasShellClass: mapHost.classList.contains('map-renderer-shell'),
ariaBusy: mapHost.getAttribute('aria-busy'),
pendingRenderer: mapHost.dataset.mapRendererPending ?? null,
shellChildCount: mapHost.querySelectorAll('.map-renderer-shell-surface').length,
width: shellRect.width,
height: shellRect.height,
};
await nextFrame();
const heavyBeforeShellPaint = heavyResourceNames();
await waitForRenderer(mapHost);
const settledRect = mapHost.getBoundingClientRect();
const heavyAfterMobileFallback = heavyResourceNames();
return {
shellState,
heavyAfterMapContainerImport,
heavyBeforeShellPaint,
heavyAfterMobileFallback,
webglProbeCallsAtShell,
webglProbeCallsAfterRenderer: webglProbeCalls,
settled: {
svgMode: mapHost.classList.contains('svg-mode'),
deckMode: mapHost.classList.contains('deckgl-mode'),
globeMode: mapHost.classList.contains('globe-mode'),
width: settledRect.width,
height: settledRect.height,
},
};
} finally {
HTMLCanvasElement.prototype.getContext = originalGetContext;
map?.destroy();
mapHost.remove();
}
});
expect(result.shellState.hasShellClass).toBe(true);
expect(result.shellState.ariaBusy).toBe('true');
expect(result.shellState.pendingRenderer).toBe('svg');
expect(result.shellState.shellChildCount).toBe(1);
expect(result.shellState.width).toBeGreaterThan(0);
expect(result.shellState.height).toBeGreaterThan(0);
expect(result.heavyAfterMapContainerImport).toEqual([]);
expect(result.heavyBeforeShellPaint).toEqual([]);
expect(result.heavyAfterMobileFallback).toEqual([]);
expect(result.webglProbeCallsAtShell).toBe(0);
expect(result.settled.svgMode).toBe(true);
expect(result.settled.deckMode).toBe(false);
expect(result.settled.globeMode).toBe(false);
expect(Math.abs(result.settled.width - result.shellState.width)).toBeLessThanOrEqual(1);
expect(Math.abs(result.settled.height - result.shellState.height)).toBeLessThanOrEqual(1);
});
test('MapContainer waits for desktop map demand before heavy WebGL resources', async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 1000 });
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const { DEFAULT_MAP_LAYERS } = await import('/src/config/index.ts');
const { initI18n } = await import('/src/services/i18n.ts');
await initI18n();
const isHeavyMapResource = (name: string): boolean =>
/DeckGLMap|GlobeMap|maplibre|deck-stack|globe\.gl|pmtiles|\.pmtiles|@deck\.gl|@luma\.gl|@loaders\.gl/i.test(name);
const heavyResourceNames = (): string[] =>
performance.getEntriesByType('resource')
.map((entry) => entry.name)
.filter(isHeavyMapResource);
const nextFrame = (): Promise<void> => new Promise((resolve) => requestAnimationFrame(() => resolve()));
const wait = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
performance.clearResourceTimings();
const { MapContainer } = await import('/src/components/MapContainer.ts');
const heavyAfterMapContainerImport = heavyResourceNames();
const mapHost = document.createElement('div');
mapHost.className = 'map-container';
mapHost.style.width = '960px';
mapHost.style.height = '360px';
mapHost.style.position = 'relative';
document.body.appendChild(mapHost);
const originalGetContext = HTMLCanvasElement.prototype.getContext;
const fakeDebugInfo = { UNMASKED_RENDERER_WEBGL: 0x9246 };
const fakeWebGL2 = {
getExtension(name: string) {
return name === 'WEBGL_debug_renderer_info' ? fakeDebugInfo : null;
},
getParameter() {
return 'ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)';
},
};
let map: InstanceType<typeof MapContainer> | null = null;
try {
HTMLCanvasElement.prototype.getContext = (function (
this: HTMLCanvasElement,
contextId: string,
options?: unknown,
) {
if (contextId === 'webgl2') return fakeWebGL2 as unknown as RenderingContext;
return originalGetContext.call(this, contextId, options as never);
}) as typeof HTMLCanvasElement.prototype.getContext;
map = new MapContainer(mapHost, {
zoom: 1,
pan: { x: 0, y: 0 },
view: 'global',
layers: { ...DEFAULT_MAP_LAYERS },
timeRange: '7d',
});
await nextFrame();
await nextFrame();
const shellRect = mapHost.getBoundingClientRect();
await wait(700);
return {
heavyAfterMapContainerImport,
heavyBeforeDemand: heavyResourceNames(),
shellState: {
hasShellClass: mapHost.classList.contains('map-renderer-shell'),
ariaBusy: mapHost.getAttribute('aria-busy'),
pendingRenderer: mapHost.dataset.mapRendererPending ?? null,
deckMode: mapHost.classList.contains('deckgl-mode'),
svgMode: mapHost.classList.contains('svg-mode'),
width: shellRect.width,
height: shellRect.height,
},
};
} finally {
HTMLCanvasElement.prototype.getContext = originalGetContext;
map?.destroy();
mapHost.remove();
}
});
expect(result.heavyAfterMapContainerImport).toEqual([]);
expect(result.heavyBeforeDemand).toEqual([]);
expect(result.shellState.hasShellClass).toBe(true);
expect(result.shellState.ariaBusy).toBe('true');
expect(result.shellState.pendingRenderer).toBe('deck');
expect(result.shellState.deckMode).toBe(false);
expect(result.shellState.svgMode).toBe(false);
expect(result.shellState.width).toBeGreaterThan(0);
expect(result.shellState.height).toBeGreaterThan(0);
});
test('MapContainer starts desktop WebGL resources on first map interaction', async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 1000 });
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const { DEFAULT_MAP_LAYERS } = await import('/src/config/index.ts');
const { initI18n } = await import('/src/services/i18n.ts');
await initI18n();
const isHeavyMapResource = (name: string): boolean =>
/DeckGLMap|GlobeMap|maplibre|deck-stack|globe\.gl|pmtiles|\.pmtiles|@deck\.gl|@luma\.gl|@loaders\.gl/i.test(name);
const heavyResourceNames = (): string[] =>
performance.getEntriesByType('resource')
.map((entry) => entry.name)
.filter(isHeavyMapResource);
const nextFrame = (): Promise<void> => new Promise((resolve) => requestAnimationFrame(() => resolve()));
const wait = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
const waitForHeavyResource = async (): Promise<string[]> => {
const deadline = performance.now() + 5_000;
while (performance.now() < deadline) {
const names = heavyResourceNames();
if (names.length > 0) return names;
await wait(50);
}
return heavyResourceNames();
};
performance.clearResourceTimings();
const { MapContainer } = await import('/src/components/MapContainer.ts');
const mapHost = document.createElement('div');
mapHost.className = 'map-container';
mapHost.style.width = '960px';
mapHost.style.height = '360px';
mapHost.style.position = 'relative';
document.body.appendChild(mapHost);
const originalGetContext = HTMLCanvasElement.prototype.getContext;
const fakeDebugInfo = { UNMASKED_RENDERER_WEBGL: 0x9246 };
const fakeWebGL2 = {
getExtension(name: string) {
return name === 'WEBGL_debug_renderer_info' ? fakeDebugInfo : null;
},
getParameter() {
return 'ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)';
},
};
let map: InstanceType<typeof MapContainer> | null = null;
try {
HTMLCanvasElement.prototype.getContext = (function (
this: HTMLCanvasElement,
contextId: string,
options?: unknown,
) {
if (contextId === 'webgl2') return fakeWebGL2 as unknown as RenderingContext;
return originalGetContext.call(this, contextId, options as never);
}) as typeof HTMLCanvasElement.prototype.getContext;
map = new MapContainer(mapHost, {
zoom: 1,
pan: { x: 0, y: 0 },
view: 'global',
layers: { ...DEFAULT_MAP_LAYERS },
timeRange: '7d',
});
await nextFrame();
await nextFrame();
await wait(100);
const heavyBeforeInteraction = heavyResourceNames();
mapHost.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, pointerId: 1 }));
const heavyAfterInteraction = await waitForHeavyResource();
return {
heavyBeforeInteraction,
heavyAfterInteraction,
pendingRenderer: mapHost.dataset.mapRendererPending ?? null,
};
} finally {
HTMLCanvasElement.prototype.getContext = originalGetContext;
map?.destroy();
mapHost.remove();
}
});
expect(result.heavyBeforeInteraction).toEqual([]);
expect(result.pendingRenderer).toBe('deck');
expect(result.heavyAfterInteraction.some((name) => (
/DeckGLMap|maplibre|@deck\.gl|@luma\.gl|@loaders\.gl/i.test(name)
))).toBe(true);
});
test('MapContainer replays escalation getter setup after deferred renderer mount', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const { DEFAULT_MAP_LAYERS } = await import('/src/config/index.ts');
const { initI18n } = await import('/src/services/i18n.ts');
await initI18n();
const { MapComponent } = await import('/src/components/Map.ts');
const { MapContainer } = await import('/src/components/MapContainer.ts');
const proto = MapComponent.prototype as {
initEscalationGetters: () => void;
};
const originalInitEscalationGetters = proto.initEscalationGetters;
let initCalls = 0;
const nextFrame = (): Promise<void> => new Promise((resolve) => requestAnimationFrame(() => resolve()));
const waitForSvgRenderer = async (host: HTMLElement): Promise<void> => {
const deadline = performance.now() + 5_000;
while (!host.classList.contains('svg-mode')) {
if (performance.now() > deadline) throw new Error('SVG renderer did not settle');
await nextFrame();
}
};
proto.initEscalationGetters = function (this: InstanceType<typeof MapComponent>): void {
initCalls += 1;
originalInitEscalationGetters.call(this);
};
const mapHost = document.createElement('div');
mapHost.className = 'map-container';
mapHost.style.width = '390px';
mapHost.style.height = '260px';
mapHost.style.position = 'relative';
document.body.appendChild(mapHost);
let map: InstanceType<typeof MapContainer> | null = null;
try {
map = new MapContainer(mapHost, {
zoom: 2.5,
pan: { x: 0, y: 0 },
view: 'global',
layers: { ...DEFAULT_MAP_LAYERS },
timeRange: '7d',
});
map.initEscalationGetters();
const callsBeforeRenderer = initCalls;
await waitForSvgRenderer(mapHost);
return {
callsBeforeRenderer,
callsAfterRenderer: initCalls,
svgMode: mapHost.classList.contains('svg-mode'),
};
} finally {
map?.destroy();
mapHost.remove();
proto.initEscalationGetters = originalInitEscalationGetters;
}
});
expect(result.callsBeforeRenderer).toBe(0);
expect(result.callsAfterRenderer).toBe(1);
expect(result.svgMode).toBe(true);
});
test('MapContainer replays pending view and zoom after deferred renderer mount', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const { DEFAULT_MAP_LAYERS } = await import('/src/config/index.ts');
const { initI18n } = await import('/src/services/i18n.ts');
await initI18n();
const { MapComponent } = await import('/src/components/Map.ts');
const { MapContainer } = await import('/src/components/MapContainer.ts');
const proto = MapComponent.prototype as {
setView: (view: string, zoom?: number) => void;
setZoom: (zoom: number) => void;
};
const originalSetView = proto.setView;
const originalSetZoom = proto.setZoom;
const viewCalls: Array<{ view: string; zoom?: number }> = [];
const zoomCalls: number[] = [];
const nextFrame = (): Promise<void> => new Promise((resolve) => requestAnimationFrame(() => resolve()));
const waitForSvgRenderer = async (host: HTMLElement): Promise<void> => {
const deadline = performance.now() + 5_000;
while (!host.classList.contains('svg-mode')) {
if (performance.now() > deadline) throw new Error('SVG renderer did not settle');
await nextFrame();
}
};
proto.setView = function (this: InstanceType<typeof MapComponent>, view: string, zoom?: number): void {
viewCalls.push({ view, zoom });
originalSetView.call(this, view as never, zoom);
};
proto.setZoom = function (this: InstanceType<typeof MapComponent>, zoom: number): void {
zoomCalls.push(zoom);
originalSetZoom.call(this, zoom);
};
const mapHost = document.createElement('div');
mapHost.className = 'map-container';
mapHost.style.width = '390px';
mapHost.style.height = '260px';
mapHost.style.position = 'relative';
document.body.appendChild(mapHost);
let map: InstanceType<typeof MapContainer> | null = null;
try {
map = new MapContainer(mapHost, {
zoom: 2.5,
pan: { x: 0, y: 0 },
view: 'global',
layers: { ...DEFAULT_MAP_LAYERS },
timeRange: '7d',
});
map.setView('mena', 4);
map.setZoom(5);
const callsBeforeRenderer = { view: viewCalls.length, zoom: zoomCalls.length };
await waitForSvgRenderer(mapHost);
return {
callsBeforeRenderer,
viewCalls,
zoomCalls,
state: map.getState(),
svgMode: mapHost.classList.contains('svg-mode'),
};
} finally {
map?.destroy();
mapHost.remove();
proto.setView = originalSetView;
proto.setZoom = originalSetZoom;
}
});
expect(result.callsBeforeRenderer).toEqual({ view: 0, zoom: 0 });
expect(result.viewCalls).toEqual([{ view: 'mena', zoom: 4 }]);
expect(result.zoomCalls).toEqual([5]);
expect(result.state.view).toBe('mena');
expect(result.state.zoom).toBe(5);
expect(result.svgMode).toBe(true);
});
test('MapContainer applies pending time range before deferred renderer mount', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const { DEFAULT_MAP_LAYERS } = await import('/src/config/index.ts');
const { initI18n } = await import('/src/services/i18n.ts');
await initI18n();
const { MapComponent } = await import('/src/components/Map.ts');
const { MapContainer } = await import('/src/components/MapContainer.ts');
const proto = MapComponent.prototype as {
setTimeRange: (range: string) => void;
};
const originalSetTimeRange = proto.setTimeRange;
const rendererRanges: string[] = [];
const callbackRanges: string[] = [];
const nextFrame = (): Promise<void> => new Promise((resolve) => requestAnimationFrame(() => resolve()));
const waitForSvgRenderer = async (host: HTMLElement): Promise<void> => {
const deadline = performance.now() + 5_000;
while (!host.classList.contains('svg-mode')) {
if (performance.now() > deadline) throw new Error('SVG renderer did not settle');
await nextFrame();
}
};
proto.setTimeRange = function (this: InstanceType<typeof MapComponent>, range: string): void {
rendererRanges.push(range);
originalSetTimeRange.call(this, range as never);
};
const mapHost = document.createElement('div');
mapHost.className = 'map-container';
mapHost.style.width = '390px';
mapHost.style.height = '260px';
mapHost.style.position = 'relative';
document.body.appendChild(mapHost);
let map: InstanceType<typeof MapContainer> | null = null;
try {
map = new MapContainer(mapHost, {
zoom: 2.5,
pan: { x: 0, y: 0 },
view: 'global',
layers: { ...DEFAULT_MAP_LAYERS },
timeRange: '7d',
});
map.onTimeRangeChanged((range) => callbackRanges.push(range));
map.setTimeRange('24h');
const beforeRenderer = {
callbackRanges: [...callbackRanges],
rendererRanges: [...rendererRanges],
timeRange: map.getTimeRange(),
};
await waitForSvgRenderer(mapHost);
return {
beforeRenderer,
afterRenderer: {
callbackRanges,
rendererRanges,
timeRange: map.getTimeRange(),
stateRange: map.getState().timeRange,
},
svgMode: mapHost.classList.contains('svg-mode'),
};
} finally {
map?.destroy();
mapHost.remove();
proto.setTimeRange = originalSetTimeRange;
}
});
expect(result.beforeRenderer).toEqual({
callbackRanges: ['24h'],
rendererRanges: [],
timeRange: '24h',
});
expect(result.afterRenderer.callbackRanges).toEqual(['24h']);
expect(result.afterRenderer.rendererRanges).toEqual([]);
expect(result.afterRenderer.timeRange).toBe('24h');
expect(result.afterRenderer.stateRange).toBe('24h');
expect(result.svgMode).toBe(true);
});
test('MapContainer replays hidden layer toggles after deferred renderer mount', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const { DEFAULT_MAP_LAYERS } = await import('/src/config/index.ts');
const { initI18n } = await import('/src/services/i18n.ts');
await initI18n();
const { MapComponent } = await import('/src/components/Map.ts');
const { MapContainer } = await import('/src/components/MapContainer.ts');
const proto = MapComponent.prototype as {
hideLayerToggle: (layer: string) => void;
};
const originalHideLayerToggle = proto.hideLayerToggle;
const hiddenLayers: string[] = [];
const nextFrame = (): Promise<void> => new Promise((resolve) => requestAnimationFrame(() => resolve()));
const waitForSvgRenderer = async (host: HTMLElement): Promise<void> => {
const deadline = performance.now() + 5_000;
while (!host.classList.contains('svg-mode')) {
if (performance.now() > deadline) throw new Error('SVG renderer did not settle');
await nextFrame();
}
};
proto.hideLayerToggle = function (this: InstanceType<typeof MapComponent>, layer: string): void {
hiddenLayers.push(layer);
originalHideLayerToggle.call(this, layer as never);
};
const mapHost = document.createElement('div');
mapHost.className = 'map-container';
mapHost.style.width = '390px';
mapHost.style.height = '260px';
mapHost.style.position = 'relative';
document.body.appendChild(mapHost);
let map: InstanceType<typeof MapContainer> | null = null;
try {
map = new MapContainer(mapHost, {
zoom: 2.5,
pan: { x: 0, y: 0 },
view: 'global',
layers: { ...DEFAULT_MAP_LAYERS },
timeRange: '7d',
});
map.hideLayerToggle('outages');
const callsBeforeRenderer = hiddenLayers.length;
await waitForSvgRenderer(mapHost);
return {
callsBeforeRenderer,
hiddenLayers,
svgMode: mapHost.classList.contains('svg-mode'),
};
} finally {
map?.destroy();
mapHost.remove();
proto.hideLayerToggle = originalHideLayerToggle;
}
});
expect(result.callsBeforeRenderer).toBe(0);
expect(result.hiddenLayers).toEqual(['outages']);
expect(result.svgMode).toBe(true);
});
test('MapContainer preserves early enabled layers for deferred renderer mount', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const { DEFAULT_MAP_LAYERS } = await import('/src/config/index.ts');
const { initI18n } = await import('/src/services/i18n.ts');
await initI18n();
const { MapComponent } = await import('/src/components/Map.ts');
const { MapContainer } = await import('/src/components/MapContainer.ts');
const proto = MapComponent.prototype as {
enableLayer: (layer: string) => void;
};
const originalEnableLayer = proto.enableLayer;
const enableCalls: string[] = [];
const nextFrame = (): Promise<void> => new Promise((resolve) => requestAnimationFrame(() => resolve()));
const waitForSvgRenderer = async (host: HTMLElement): Promise<void> => {
const deadline = performance.now() + 5_000;
while (!host.classList.contains('svg-mode')) {
if (performance.now() > deadline) throw new Error('SVG renderer did not settle');
await nextFrame();
}
};
proto.enableLayer = function (this: InstanceType<typeof MapComponent>, layer: string): void {
enableCalls.push(layer);
originalEnableLayer.call(this, layer as never);
};
const mapHost = document.createElement('div');
mapHost.className = 'map-container';
mapHost.style.width = '390px';
mapHost.style.height = '260px';
mapHost.style.position = 'relative';
document.body.appendChild(mapHost);
let map: InstanceType<typeof MapContainer> | null = null;
try {
map = new MapContainer(mapHost, {
zoom: 2.5,
pan: { x: 0, y: 0 },
view: 'global',
layers: { ...DEFAULT_MAP_LAYERS, outages: false },
timeRange: '7d',
});
map.enableLayer('outages');
const beforeRenderer = {
enableCalls: [...enableCalls],
layerEnabled: map.getState().layers.outages,
};
await waitForSvgRenderer(mapHost);
return {
beforeRenderer,
afterRenderer: {
enableCalls,
layerEnabled: map.getState().layers.outages,
},
svgMode: mapHost.classList.contains('svg-mode'),
};
} finally {
map?.destroy();
mapHost.remove();
proto.enableLayer = originalEnableLayer;
}
});
expect(result.beforeRenderer).toEqual({ enableCalls: [], layerEnabled: true });
expect(result.afterRenderer.enableCalls).toEqual([]);
expect(result.afterRenderer.layerEnabled).toBe(true);
expect(result.svgMode).toBe(true);
});
test('MapContainer replays early chokepoint data after deferred renderer mount', async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const { DEFAULT_MAP_LAYERS } = await import('/src/config/index.ts');
const { initI18n } = await import('/src/services/i18n.ts');
await initI18n();
const { MapComponent } = await import('/src/components/Map.ts');
const { MapContainer } = await import('/src/components/MapContainer.ts');
const proto = MapComponent.prototype as {
setChokepointData: (data: unknown) => void;
};
const originalChokepoint = proto.setChokepointData;
let calls = 0;
const nextFrame = (): Promise<void> => new Promise((resolve) => requestAnimationFrame(() => resolve()));
const waitForSvgRenderer = async (host: HTMLElement): Promise<void> => {
const deadline = performance.now() + 5_000;
while (!host.classList.contains('svg-mode')) {
if (performance.now() > deadline) throw new Error('SVG renderer did not settle');
await nextFrame();
}
};
proto.setChokepointData = function (this: InstanceType<typeof MapComponent>, data: unknown): void {
if (data) calls += 1;
originalChokepoint.call(this, data as never);
};
const mapHost = document.createElement('div');
mapHost.className = 'map-container';
mapHost.style.width = '390px';
mapHost.style.height = '260px';
mapHost.style.position = 'relative';
document.body.appendChild(mapHost);
let map: InstanceType<typeof MapContainer> | null = null;
try {
map = new MapContainer(mapHost, {
zoom: 1,
pan: { x: 0, y: 0 },
view: 'global',
layers: { ...DEFAULT_MAP_LAYERS },
timeRange: '7d',
});
map.setChokepointData({ chokepoints: [] } as never);
const callsBeforeRenderer = calls;
await waitForSvgRenderer(mapHost);
return {
callsBeforeRenderer,
callsAfterRenderer: calls,
svgMode: mapHost.classList.contains('svg-mode'),
};
} finally {
map?.destroy();
mapHost.remove();
proto.setChokepointData = originalChokepoint;
}
});
expect(result.callsBeforeRenderer).toBe(0);
expect(result.callsAfterRenderer).toBe(1);
expect(result.svgMode).toBe(true);
});
test('MapContainer falls back to SVG when WebGL2 is unavailable', async ({ page }) => {
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const { DEFAULT_MAP_LAYERS } = await import('/src/config/index.ts');
const { initI18n } = await import('/src/services/i18n.ts');
await initI18n();
const { MapContainer } = await import('/src/components/MapContainer.ts');
const mapHost = document.createElement('div');
mapHost.className = 'map-container';
mapHost.style.width = '1200px';
mapHost.style.height = '720px';
document.body.appendChild(mapHost);
const originalGetContext = HTMLCanvasElement.prototype.getContext;
let map: InstanceType<typeof MapContainer> | null = null;
const waitForRenderer = async (): Promise<void> => {
const deadline = performance.now() + 5_000;
while (!mapHost.classList.contains('svg-mode')) {
if (performance.now() > deadline) throw new Error('Map renderer did not settle');
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
}
};
try {
HTMLCanvasElement.prototype.getContext = (function (
this: HTMLCanvasElement,
contextId: string,
options?: unknown
) {
if (contextId === 'webgl2') return null;
return originalGetContext.call(this, contextId, options as never);
}) as typeof HTMLCanvasElement.prototype.getContext;
map = new MapContainer(mapHost, {
zoom: 1,
pan: { x: 0, y: 0 },
view: 'global',
layers: { ...DEFAULT_MAP_LAYERS },
timeRange: '7d',
});
await new Promise<void>((resolve) => requestAnimationFrame(() => {
requestAnimationFrame(() => window.setTimeout(resolve, 0));
}));
mapHost.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));
await waitForRenderer();
return {
isDeckGLMode: map.isDeckGLMode(),
hasSvgModeClass: mapHost.classList.contains('svg-mode'),
hasDeckModeClass: mapHost.classList.contains('deckgl-mode'),
deckWrapperCount: mapHost.querySelectorAll('.deckgl-map-wrapper').length,
svgWrapperCount: mapHost.querySelectorAll('.map-wrapper').length,
};
} finally {
HTMLCanvasElement.prototype.getContext = originalGetContext;
map?.destroy();
mapHost.remove();
}
});
expect(result.isDeckGLMode).toBe(false);
expect(result.hasSvgModeClass).toBe(true);
expect(result.hasDeckModeClass).toBe(false);
expect(result.deckWrapperCount).toBe(0);
expect(result.svgWrapperCount).toBe(1);
});
test('MapContainer clears partial DeckGL DOM after constructor failure fallback', async ({ page }) => {
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const { DEFAULT_MAP_LAYERS } = await import('/src/config/index.ts');
const { initI18n } = await import('/src/services/i18n.ts');
await initI18n();
const { MapContainer } = await import('/src/components/MapContainer.ts');
const mapHost = document.createElement('div');
mapHost.className = 'map-container';
mapHost.style.width = '1200px';
mapHost.style.height = '720px';
document.body.appendChild(mapHost);
const originalGetContext = HTMLCanvasElement.prototype.getContext;
const originalGetElementById = Document.prototype.getElementById;
let map: InstanceType<typeof MapContainer> | null = null;
const waitForRenderer = async (): Promise<void> => {
const deadline = performance.now() + 5_000;
while (!mapHost.classList.contains('svg-mode')) {
if (performance.now() > deadline) throw new Error('Map renderer did not settle');
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
}
};
try {
HTMLCanvasElement.prototype.getContext = (function (
this: HTMLCanvasElement,
contextId: string,
options?: unknown
) {
if (contextId === 'webgl2') {
return {
getExtension: () => null,
getParameter: () => null,
} as unknown as WebGL2RenderingContext;
}
return originalGetContext.call(this, contextId, options as never);
}) as typeof HTMLCanvasElement.prototype.getContext;
Document.prototype.getElementById = (function (
this: Document,
id: string
): HTMLElement | null {
if (id === 'deckgl-basemap') {
throw new Error('forced DeckGL init failure');
}
return originalGetElementById.call(this, id);
}) as typeof Document.prototype.getElementById;
map = new MapContainer(mapHost, {
zoom: 1,
pan: { x: 0, y: 0 },
view: 'global',
layers: { ...DEFAULT_MAP_LAYERS },
timeRange: '7d',
});
await waitForRenderer();
return {
isDeckGLMode: map.isDeckGLMode(),
hasSvgModeClass: mapHost.classList.contains('svg-mode'),
hasDeckModeClass: mapHost.classList.contains('deckgl-mode'),
deckWrapperCount: mapHost.querySelectorAll('.deckgl-map-wrapper').length,
svgWrapperCount: mapHost.querySelectorAll('.map-wrapper').length,
};
} finally {
HTMLCanvasElement.prototype.getContext = originalGetContext;
Document.prototype.getElementById = originalGetElementById;
map?.destroy();
mapHost.remove();
}
});
expect(result.isDeckGLMode).toBe(false);
expect(result.hasSvgModeClass).toBe(true);
expect(result.hasDeckModeClass).toBe(false);
expect(result.deckWrapperCount).toBe(0);
expect(result.svgWrapperCount).toBe(1);
});
test('MapContainer falls back to SVG for software WebGL renderers', async ({ page }) => {
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const { DEFAULT_MAP_LAYERS } = await import('/src/config/index.ts');
const { initI18n } = await import('/src/services/i18n.ts');
await initI18n();
const { MapContainer } = await import('/src/components/MapContainer.ts');
const mapHost = document.createElement('div');
mapHost.className = 'map-container';
mapHost.style.width = '1200px';
mapHost.style.height = '720px';
document.body.appendChild(mapHost);
const originalGetContext = HTMLCanvasElement.prototype.getContext;
let map: InstanceType<typeof MapContainer> | null = null;
const waitForRenderer = async (): Promise<void> => {
const deadline = performance.now() + 5_000;
while (!(
mapHost.classList.contains('svg-mode')
|| mapHost.classList.contains('deckgl-mode')
|| mapHost.classList.contains('globe-mode')
)) {
if (performance.now() > deadline) throw new Error('Map renderer did not settle');
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
}
};
try {
const rendererInfo = { UNMASKED_RENDERER_WEBGL: 0x9246 };
HTMLCanvasElement.prototype.getContext = (function (
this: HTMLCanvasElement,
contextId: string,
options?: unknown
) {
if (contextId === 'webgl2') {
return {
getExtension: (name: string) => name === 'WEBGL_debug_renderer_info' ? rendererInfo : null,
getParameter: (param: number) => param === rendererInfo.UNMASKED_RENDERER_WEBGL ? 'Google SwiftShader' : null,
} as unknown as WebGL2RenderingContext;
}
return originalGetContext.call(this, contextId, options as never);
}) as typeof HTMLCanvasElement.prototype.getContext;
map = new MapContainer(mapHost, {
zoom: 1,
pan: { x: 0, y: 0 },
view: 'global',
layers: { ...DEFAULT_MAP_LAYERS },
timeRange: '7d',
});
await waitForRenderer();
return {
isDeckGLMode: map.isDeckGLMode(),
hasSvgModeClass: mapHost.classList.contains('svg-mode'),
hasDeckModeClass: mapHost.classList.contains('deckgl-mode'),
deckWrapperCount: mapHost.querySelectorAll('.deckgl-map-wrapper').length,
svgWrapperCount: mapHost.querySelectorAll('.map-wrapper').length,
};
} finally {
HTMLCanvasElement.prototype.getContext = originalGetContext;
map?.destroy();
mapHost.remove();
}
});
expect(result.isDeckGLMode).toBe(false);
expect(result.hasSvgModeClass).toBe(true);
expect(result.hasDeckModeClass).toBe(false);
expect(result.deckWrapperCount).toBe(0);
expect(result.svgWrapperCount).toBe(1);
});
test('loadMarkets keeps Yahoo-backed data when Finnhub is skipped', async ({ page }) => {
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const { DataLoaderManager } = await import('/src/app/data-loader.ts');
const originalFetch = window.fetch.bind(window);
const calls: string[] = [];
const toUrl = (input: RequestInfo | URL): string => {
if (typeof input === 'string') return new URL(input, window.location.origin).toString();
if (input instanceof URL) return input.toString();
return new URL(input.url, window.location.origin).toString();
};
const responseJson = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), {
status,
headers: { 'content-type': 'application/json' },
});
const yahooChart = (symbol: string) => {
const base = symbol.length * 100;
return {
chart: {
result: [{
meta: {
regularMarketPrice: base + 1,
previousClose: base,
},
indicators: {
quote: [{ close: [base - 2, base - 1, base, base + 1] }],
},
}],
},
};
};
const marketRenders: Array<Array<{ symbol?: string }>> = [];
const marketConfigErrors: string[] = [];
const heatmapRenders: number[] = [];
const heatmapConfigErrors: string[] = [];
const commoditiesRenders: number[] = [];
const commoditiesConfigErrors: string[] = [];
const cryptoRenders: number[] = [];
const apiStatuses: Array<{ name: string; status: string }> = [];
// Representative Yahoo-only symbols, including exchange-qualified China
// entries from the default basket. Omit SMIC below to prove one missing
// quote does not blank the available Shanghai/Hong Kong rows.
const yahooOnly = new Set([
'^GSPC', '^DJI', '^IXIC', '^VIX', 'GC=F', 'CL=F', 'NG=F', 'SI=F', 'HG=F',
'600519.SS', '0700.HK', '688981.SS',
]);
window.fetch = (async (input: RequestInfo | URL) => {
const url = toUrl(input);
calls.push(url);
const parsed = new URL(url);
if (parsed.pathname === '/api/market/v1/list-market-quotes') {
const symbols = parsed.searchParams.getAll('symbols');
const quotes = symbols
.filter((s: string) => yahooOnly.has(s) && s !== '688981.SS')
.map((s: string) => {
const base = s.length * 100;
return { symbol: s, name: s, display: s, price: base + 1, change: ((base + 1) - base) / base * 100, sparkline: [base - 2, base - 1, base, base + 1] };
});
return responseJson({
quotes,
finnhubSkipped: true,
skipReason: 'FINNHUB_API_KEY not configured',
});
}
if (parsed.pathname === '/api/market/v1/list-commodity-quotes') {
const symbols = parsed.searchParams.getAll('symbols');
return responseJson({
quotes: symbols.map((symbol) => ({
symbol,
name: symbol,
display: symbol,
price: symbol.length * 100 + 1,
change: 0.25,
sparkline: [1, 2, 3],
})),
});
}
// Sebuf proto: POST /api/market/v1/list-crypto-quotes
if (parsed.pathname === '/api/market/v1/list-crypto-quotes') {
return responseJson({
quotes: [
{ name: 'Bitcoin', symbol: 'BTC', price: 50000, change: 1.2, sparkline: [1, 2, 3] },
{ name: 'Ethereum', symbol: 'ETH', price: 3000, change: -0.5, sparkline: [1, 2, 3] },
{ name: 'Solana', symbol: 'SOL', price: 120, change: 2.1, sparkline: [1, 2, 3] },
],
});
}
return responseJson({});
}) as typeof window.fetch;
const fakeApp = {
ctx: {
latestMarkets: [] as Array<unknown>,
panels: {
markets: {
renderMarkets: (data: Array<{ symbol?: string }>) => marketRenders.push(data),
showConfigError: (message: string) => marketConfigErrors.push(message),
},
heatmap: {
renderHeatmap: (data: Array<unknown>) => heatmapRenders.push(data.length),
showConfigError: (message: string) => heatmapConfigErrors.push(message),
},
commodities: {
renderCommodities: (data: Array<unknown>) => commoditiesRenders.push(data.length),
showConfigError: (message: string) => commoditiesConfigErrors.push(message),
showRetrying: () => {},
},
crypto: {
renderCrypto: (data: Array<unknown>) => cryptoRenders.push(data.length),
showRetrying: () => {},
},
},
statusPanel: {
updateApi: (name: string, payload: { status?: string }) => {
apiStatuses.push({ name, status: payload.status ?? '' });
},
},
},
};
try {
await (DataLoaderManager.prototype as unknown as { loadMarkets: () => Promise<void> })
.loadMarkets.call(fakeApp);
const marketQuoteCalls = calls.filter((url) =>
new URL(url).pathname === '/api/market/v1/list-market-quotes'
);
const commodityQuoteCalls = calls.filter((url) =>
new URL(url).pathname === '/api/market/v1/list-commodity-quotes'
);
return {
marketRenders,
marketConfigErrors,
heatmapRenders,
heatmapConfigErrors,
commoditiesRenders,
commoditiesConfigErrors,
cryptoRenders,
apiStatuses,
latestMarketsCount: fakeApp.ctx.latestMarkets.length,
latestMarketSymbols: (fakeApp.ctx.latestMarkets as Array<{ symbol?: string }>).map((entry) => entry.symbol),
marketQuoteCalls: marketQuoteCalls.length,
commodityQuoteCalls: commodityQuoteCalls.length,
};
} finally {
window.fetch = originalFetch;
}
});
expect(result.marketRenders.some((render) => render.length > 0)).toBe(true);
expect(result.latestMarketsCount).toBeGreaterThan(0);
expect(result.latestMarketSymbols).toContain('600519.SS');
expect(result.latestMarketSymbols).toContain('0700.HK');
expect(result.latestMarketSymbols).not.toContain('688981.SS');
expect(result.marketConfigErrors.length).toBe(0);
expect(result.heatmapRenders.length).toBe(0);
expect(result.heatmapConfigErrors).toEqual(['FINNHUB_API_KEY not configured — add in Settings']);
expect(result.commoditiesRenders.some((count) => count > 0)).toBe(true);
expect(result.commoditiesConfigErrors.length).toBe(0);
expect(result.marketQuoteCalls).toBeGreaterThanOrEqual(1);
expect(result.commodityQuoteCalls).toBeGreaterThanOrEqual(1);
expect(result.cryptoRenders.some((count) => count > 0)).toBe(true);
expect(result.apiStatuses.some((entry) => entry.name === 'Finnhub' && entry.status === 'error')).toBe(true);
expect(result.apiStatuses.some((entry) => entry.name === 'CoinGecko' && entry.status === 'ok')).toBe(true);
});
test('fetchHapiSummary maps proto countryCode to iso2 field', async ({ page }) => {
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const originalFetch = window.fetch.bind(window);
const toUrl = (input: RequestInfo | URL): string => {
if (typeof input === 'string') return new URL(input, window.location.origin).toString();
if (input instanceof URL) return input.toString();
return new URL(input.url, window.location.origin).toString();
};
const responseJson = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), {
status,
headers: { 'content-type': 'application/json' },
});
const seenCountryCodes = new Set<string>();
window.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const parsed = new URL(toUrl(input));
if (parsed.pathname === '/api/conflict/v1/get-humanitarian-summary') {
const body = init?.body ? JSON.parse(String(init.body)) : {};
const countryCode = String(body.countryCode || '').toUpperCase();
seenCountryCodes.add(countryCode);
return responseJson({
summary: {
countryCode,
countryName: countryCode,
conflictEventsTotal: 1,
conflictPoliticalViolenceEvents: 1,
conflictFatalities: 1,
referencePeriod: '2026-02',
conflictDemonstrations: 0,
updatedAt: Date.now(),
},
});
}
return responseJson({});
}) as typeof window.fetch;
try {
const conflict = await import('/src/services/conflict/index.ts');
const summaries = await conflict.fetchHapiSummary();
const us = summaries.get('US') as Record<string, unknown> | undefined;
return {
fetchedCount: seenCountryCodes.size,
usIso2: us?.iso2 ?? null,
hasIso3Field: !!us && Object.hasOwn(us, 'iso3'),
};
} finally {
window.fetch = originalFetch;
}
});
expect(result.fetchedCount).toBeGreaterThan(0);
expect(result.usIso2).toBe('US');
expect(result.hasIso3Field).toBe(false);
});
test('cloud fallback blocked without WorldMonitor API key', async ({ page }) => {
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const runtime = await import('/src/services/runtime.ts');
const globalWindow = window as unknown as Record<string, unknown>;
const originalFetch = window.fetch.bind(window);
const calls: string[] = [];
const proxiedPaths: string[] = [];
const responseJson = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), {
status,
headers: { 'content-type': 'application/json' },
});
window.fetch = (async (input: RequestInfo | URL) => {
const url =
typeof input === 'string'
? input
: input instanceof URL
? input.toString()
: input.url;
calls.push(url);
if (url.includes('worldmonitor.app/api/fred-data')) {
return responseJson({ observations: [{ value: '999' }] }, 200);
}
return responseJson({ ok: true }, 200);
}) as typeof window.fetch;
const previousTauri = globalWindow.__TAURI__;
globalWindow.__TAURI__ = {
core: {
invoke: async (command: string, payload?: { request?: { path?: string } }) => {
if (command === 'proxy_local_api_request') {
proxiedPaths.push(payload?.request?.path || '');
throw new Error('ECONNREFUSED');
}
return null;
},
},
};
delete globalWindow.__wmFetchPatched;
try {
runtime.installRuntimeFetchPatch();
let fetchError: string | null = null;
try {
await window.fetch('/api/fred-data?series_id=CPIAUCSL');
} catch (err) {
fetchError = err instanceof Error ? err.message : String(err);
}
const cloudCalls = calls.filter(u => u.includes('worldmonitor.app'));
return {
fetchError,
cloudCalls: cloudCalls.length,
localCalls: proxiedPaths.length,
};
} finally {
window.fetch = originalFetch;
delete globalWindow.__wmFetchPatched;
if (previousTauri === undefined) {
delete globalWindow.__TAURI__;
} else {
globalWindow.__TAURI__ = previousTauri;
}
}
});
expect(result.fetchError).not.toBeNull();
expect(result.cloudCalls).toBe(0);
expect(result.localCalls).toBeGreaterThan(0);
});
test('cloud fallback allowed with valid WorldMonitor API key', async ({ page }) => {
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const runtime = await import('/src/services/runtime.ts');
const runtimeConfig = await import('/src/services/runtime-config.ts');
const globalWindow = window as unknown as Record<string, unknown>;
const originalFetch = window.fetch.bind(window);
const calls: string[] = [];
const proxiedPaths: string[] = [];
const capturedHeaders: Record<string, string> = {};
const responseJson = (body: unknown, status = 200) =>
new Response(JSON.stringify(body), {
status,
headers: { 'content-type': 'application/json' },
});
window.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const url =
typeof input === 'string'
? input
: input instanceof URL
? input.toString()
: input.url;
calls.push(url);
if (url.includes('worldmonitor.app') && init?.headers) {
const h = new Headers(init.headers);
const wmKey = h.get('X-WorldMonitor-Key');
if (wmKey) capturedHeaders['X-WorldMonitor-Key'] = wmKey;
}
if (url.includes('worldmonitor.app/api/market/v1/test')) {
return responseJson({ quotes: [] }, 200);
}
return responseJson({ ok: true }, 200);
}) as typeof window.fetch;
const previousTauri = globalWindow.__TAURI__;
globalWindow.__TAURI__ = {
core: {
invoke: async (command: string, payload?: { request?: { path?: string } }) => {
if (command === 'proxy_local_api_request') {
proxiedPaths.push(payload?.request?.path || '');
throw new Error('ECONNREFUSED');
}
return null;
},
},
};
delete globalWindow.__wmFetchPatched;
const testKey = 'wm_test_key_1234567890abcdef';
await runtimeConfig.setSecretValue('WORLDMONITOR_API_KEY' as import('/src/services/runtime-config.ts').RuntimeSecretKey, testKey);
try {
runtime.installRuntimeFetchPatch();
const response = await window.fetch('/api/market/v1/test');
const body = await response.json() as { quotes?: unknown[] };
return {
status: response.status,
hasQuotes: Array.isArray(body.quotes),
cloudCalls: calls.filter(u => u.includes('worldmonitor.app')).length,
wmKeyHeader: capturedHeaders['X-WorldMonitor-Key'] || null,
proxiedPaths,
};
} finally {
window.fetch = originalFetch;
delete globalWindow.__wmFetchPatched;
if (previousTauri === undefined) {
delete globalWindow.__TAURI__;
} else {
globalWindow.__TAURI__ = previousTauri;
}
await runtimeConfig.setSecretValue('WORLDMONITOR_API_KEY' as import('/src/services/runtime-config.ts').RuntimeSecretKey, '');
}
});
expect(result.status).toBe(200);
expect(result.hasQuotes).toBe(true);
expect(result.cloudCalls).toBe(1);
expect(result.wmKeyHeader).toBeNull();
expect(result.proxiedPaths).toContain('/api/market/v1/test');
});
test('country-instability HAPI fallback ignores eventsCivilianTargeting in score', async ({ page }) => {
await page.goto('/tests/runtime-harness.html');
const result = await page.evaluate(async () => {
const cii = await import('/src/services/country-instability.ts');
const makeSummary = (eventsCivilianTargeting: number) => ({
iso2: 'US',
locationName: 'United States',
month: '2026-02',
eventsTotal: 0,
eventsPoliticalViolence: 1,
eventsCivilianTargeting,
eventsDemonstrations: 0,
fatalitiesTotalPoliticalViolence: 0,
fatalitiesTotalCivilianTargeting: 0,
});
cii.clearCountryData();
cii.ingestHapiForCII(new Map([['US', makeSummary(0)]]));
const scoreWithoutCivilian = cii.getCountryScore('US');
cii.clearCountryData();
cii.ingestHapiForCII(new Map([['US', makeSummary(999)]]));
const scoreWithCivilian = cii.getCountryScore('US');
return { scoreWithoutCivilian, scoreWithCivilian };
});
expect(result.scoreWithoutCivilian).not.toBeNull();
expect(result.scoreWithCivilian).not.toBeNull();
expect(result.scoreWithoutCivilian).toBe(result.scoreWithCivilian);
expect(result.scoreWithCivilian as number).toBeLessThan(10);
});
});