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; 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; 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; 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 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(); 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 } }; }; const previousTauri = globalWindow.__TAURI__; const releaseUrl = 'https://github.com/koala73/worldmonitor/releases/latest'; const updaterProto = DesktopUpdater.prototype as unknown as { resolveUpdateDownloadUrl: (releaseUrl: string) => Promise; 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 => new Promise((resolve) => requestAnimationFrame(() => resolve())); const waitForRenderer = async (host: HTMLElement): Promise => { 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 | 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 => new Promise((resolve) => requestAnimationFrame(() => resolve())); const wait = (ms: number): Promise => 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 | 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 => new Promise((resolve) => requestAnimationFrame(() => resolve())); const wait = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); const waitForHeavyResource = async (): Promise => { 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 | 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 => new Promise((resolve) => requestAnimationFrame(() => resolve())); const waitForSvgRenderer = async (host: HTMLElement): Promise => { 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): 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 | 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 => new Promise((resolve) => requestAnimationFrame(() => resolve())); const waitForSvgRenderer = async (host: HTMLElement): Promise => { 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, view: string, zoom?: number): void { viewCalls.push({ view, zoom }); originalSetView.call(this, view as never, zoom); }; proto.setZoom = function (this: InstanceType, 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 | 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 => new Promise((resolve) => requestAnimationFrame(() => resolve())); const waitForSvgRenderer = async (host: HTMLElement): Promise => { 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, 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 | 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 => new Promise((resolve) => requestAnimationFrame(() => resolve())); const waitForSvgRenderer = async (host: HTMLElement): Promise => { 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, 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 | 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 => new Promise((resolve) => requestAnimationFrame(() => resolve())); const waitForSvgRenderer = async (host: HTMLElement): Promise => { 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, 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 | 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 => new Promise((resolve) => requestAnimationFrame(() => resolve())); const waitForSvgRenderer = async (host: HTMLElement): Promise => { 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, 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 | 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 | null = null; const waitForRenderer = async (): Promise => { 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((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((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 | null = null; const waitForRenderer = async (): Promise => { 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((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 | null = null; const waitForRenderer = async (): Promise => { 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((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> = []; 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, panels: { markets: { renderMarkets: (data: Array<{ symbol?: string }>) => marketRenders.push(data), showConfigError: (message: string) => marketConfigErrors.push(message), }, heatmap: { renderHeatmap: (data: Array) => heatmapRenders.push(data.length), showConfigError: (message: string) => heatmapConfigErrors.push(message), }, commodities: { renderCommodities: (data: Array) => commoditiesRenders.push(data.length), showConfigError: (message: string) => commoditiesConfigErrors.push(message), showRetrying: () => {}, }, crypto: { renderCrypto: (data: Array) => 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 }) .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(); 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 | 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; 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; const originalFetch = window.fetch.bind(window); const calls: string[] = []; const proxiedPaths: string[] = []; const capturedHeaders: Record = {}; 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); }); });