import { expect, test } from '@playwright/test'; type LayerSnapshot = { id: string; dataCount: number }; type OverlaySnapshot = { protestMarkers: number; datacenterMarkers: number; techEventMarkers: number; techHQMarkers: number; hotspotMarkers: number; }; type VisualScenarioSummary = { id: string; variant: 'both' | 'full' | 'tech' | 'finance'; }; type HarnessWindow = Window & { __mapHarness?: { ready: boolean; variant: 'full' | 'tech' | 'finance'; seedAllDynamicData: () => void; setProtestsScenario: (scenario: 'alpha' | 'beta') => void; setPulseProtestsScenario: ( scenario: | 'none' | 'recent-acled-riot' | 'recent-gdelt-riot' | 'recent-protest' ) => void; setNewsPulseScenario: (scenario: 'none' | 'recent' | 'stale') => void; setHotspotActivityScenario: (scenario: 'none' | 'breaking') => void; forcePulseStartupElapsed: () => void; resetPulseStartupTime: () => void; isPulseAnimationRunning: () => boolean; setZoom: (zoom: number) => void; setLayersForSnapshot: (enabledLayers: string[]) => void; setCamera: (camera: { lon: number; lat: number; zoom: number }) => void; enableDeterministicVisualMode: () => void; getVisualScenarios: () => VisualScenarioSummary[]; prepareVisualScenario: (scenarioId: string) => boolean; isVisualScenarioReady: (scenarioId: string) => boolean; getDeckLayerSnapshot: () => LayerSnapshot[]; getLayerDataCount: (layerId: string) => number; getLayerFirstScreenTransform: (layerId: string) => string | null; getFirstProtestTitle: () => string | null; getProtestClusterCount: () => number; getOverlaySnapshot: () => OverlaySnapshot; getCyberTooltipHtml: (indicator: string) => string; }; }; const EXPECTED_FULL_DECK_LAYERS = [ 'cables-layer', 'pipelines-layer', 'conflict-zones-layer', 'bases-layer', 'nuclear-layer', 'irradiators-layer', 'spaceports-layer', 'hotspots-layer', 'datacenters-layer', 'earthquakes-layer', 'natural-events-layer', 'fires-layer', 'weather-layer', 'outages-layer', 'cyber-threats-layer', 'ais-density-layer', 'ais-disruptions-layer', 'ports-layer', 'cable-advisories-layer', 'repair-ships-layer', 'flight-delays-layer', 'military-vessels-layer', 'military-vessel-clusters-layer', 'military-flights-layer', 'military-flight-clusters-layer', 'waterways-layer', 'economic-centers-layer', 'minerals-layer', 'apt-groups-layer', 'news-locations-layer', ]; const EXPECTED_TECH_DECK_LAYERS = [ 'cables-layer', 'pipelines-layer', 'conflict-zones-layer', 'bases-layer', 'nuclear-layer', 'irradiators-layer', 'spaceports-layer', 'hotspots-layer', 'datacenters-layer', 'earthquakes-layer', 'natural-events-layer', 'fires-layer', 'weather-layer', 'outages-layer', 'cyber-threats-layer', 'ais-density-layer', 'ais-disruptions-layer', 'ports-layer', 'cable-advisories-layer', 'repair-ships-layer', 'flight-delays-layer', 'military-vessels-layer', 'military-vessel-clusters-layer', 'military-flights-layer', 'military-flight-clusters-layer', 'waterways-layer', 'economic-centers-layer', 'minerals-layer', 'startup-hubs-layer', 'accelerators-layer', 'cloud-regions-layer', 'news-locations-layer', ]; const EXPECTED_FINANCE_DECK_LAYERS = [ ...EXPECTED_FULL_DECK_LAYERS, 'stock-exchanges-layer', 'financial-centers-layer', 'central-banks-layer', 'commodity-hubs-layer', 'gulf-investments-layer', ]; const waitForHarnessReady = async ( page: import('@playwright/test').Page ): Promise => { await page.goto('/tests/map-harness.html'); await expect(page.locator('.deckgl-map-wrapper')).toBeVisible(); await expect .poll(async () => { return await page.evaluate(() => { const w = window as HarnessWindow; return Boolean(w.__mapHarness?.ready); }); }, { timeout: 45000 }) .toBe(true); }; const prepareVisualScenario = async ( page: import('@playwright/test').Page, scenarioId: string ): Promise => { const prepared = await page.evaluate((id) => { const w = window as HarnessWindow; return w.__mapHarness?.prepareVisualScenario(id) ?? false; }, scenarioId); expect(prepared).toBe(true); await expect .poll(async () => { return await page.evaluate((id) => { const w = window as HarnessWindow; return w.__mapHarness?.isVisualScenarioReady(id) ?? false; }, scenarioId); }, { timeout: 20000 }) .toBe(true); await page.waitForTimeout(250); }; test.describe('DeckGL map harness', () => { test.describe.configure({ retries: 1 }); test('serves requested runtime variant for this test run', async ({ page }) => { await waitForHarnessReady(page); const runtimeVariant = await page.evaluate(() => { const w = window as HarnessWindow; return w.__mapHarness?.variant ?? 'full'; }); const expectedVariant = process.env.VITE_VARIANT === 'tech' ? 'tech' : process.env.VITE_VARIANT === 'finance' ? 'finance' : 'full'; expect(runtimeVariant).toBe(expectedVariant); }); test('boots without deck assertions or unhandled runtime errors', async ({ page, }) => { const pageErrors: string[] = []; const deckAssertionErrors: string[] = []; const ignorablePageErrorPatterns = [/could not compile fragment shader/i]; page.on('pageerror', (error) => { pageErrors.push(error.message); }); page.on('console', (msg) => { if (msg.type() !== 'error') return; const text = msg.text(); if (text.includes('deck.gl: assertion failed')) { deckAssertionErrors.push(text); } }); await waitForHarnessReady(page); await page.waitForTimeout(1000); const unexpectedPageErrors = pageErrors.filter( (error) => !ignorablePageErrorPatterns.some((pattern) => pattern.test(error)) ); expect(unexpectedPageErrors).toEqual([]); expect(deckAssertionErrors).toEqual([]); }); test('renders non-empty visual data for every renderable layer in current variant', async ({ page, }) => { await waitForHarnessReady(page); await page.evaluate(() => { const w = window as HarnessWindow; w.__mapHarness?.seedAllDynamicData(); w.__mapHarness?.setZoom(5); }); const variant = await page.evaluate(() => { const w = window as HarnessWindow; return w.__mapHarness?.variant ?? 'full'; }); const expectedDeckLayers = variant === 'tech' ? EXPECTED_TECH_DECK_LAYERS : variant === 'finance' ? EXPECTED_FINANCE_DECK_LAYERS : EXPECTED_FULL_DECK_LAYERS; await expect .poll(async () => { const snapshot = await page.evaluate(() => { const w = window as HarnessWindow; return w.__mapHarness?.getDeckLayerSnapshot() ?? []; }); const nonEmptyIds = new Set( snapshot.filter((layer) => layer.dataCount > 0).map((layer) => layer.id) ); return expectedDeckLayers.filter((id) => !nonEmptyIds.has(id)).length; }, { timeout: 40000 }) .toBe(0); await expect .poll(async () => { return await page.evaluate(() => { const w = window as HarnessWindow; const layers = w.__mapHarness?.getDeckLayerSnapshot() ?? []; return layers.find((layer) => layer.id === 'protest-clusters-layer')?.dataCount ?? 0; }); }, { timeout: 20000 }) .toBeGreaterThan(0); await page.evaluate(() => { const w = window as HarnessWindow; w.__mapHarness?.setZoom(3); }); await expect .poll(async () => { return await page.evaluate(() => { const w = window as HarnessWindow; const layers = w.__mapHarness?.getDeckLayerSnapshot() ?? []; return layers.find((layer) => layer.id === 'datacenter-clusters-layer')?.dataCount ?? 0; }); }, { timeout: 20000 }) .toBeGreaterThan(0); if (variant === 'tech') { await page.evaluate(() => { const w = window as HarnessWindow; w.__mapHarness?.setCamera({ lon: -122.42, lat: 37.77, zoom: 5.2 }); }); await expect .poll(async () => { return await page.evaluate(() => { const w = window as HarnessWindow; const layers = w.__mapHarness?.getDeckLayerSnapshot() ?? []; return layers.find((layer) => layer.id === 'tech-hq-clusters-layer')?.dataCount ?? 0; }); }, { timeout: 20000 }) .toBeGreaterThan(0); await expect .poll(async () => { return await page.evaluate(() => { const w = window as HarnessWindow; const layers = w.__mapHarness?.getDeckLayerSnapshot() ?? []; return layers.find((layer) => layer.id === 'tech-event-clusters-layer')?.dataCount ?? 0; }); }, { timeout: 20000 }) .toBeGreaterThan(0); } }); test('renders GCC investments layer when enabled in finance variant', async ({ page }) => { await waitForHarnessReady(page); const variant = await page.evaluate(() => { const w = window as HarnessWindow; return w.__mapHarness?.variant ?? 'full'; }); test.skip(variant !== 'finance', 'Finance variant only'); await page.evaluate(() => { const w = window as HarnessWindow; w.__mapHarness?.seedAllDynamicData(); w.__mapHarness?.setLayersForSnapshot(['gulfInvestments']); w.__mapHarness?.setCamera({ lon: 55.27, lat: 25.2, zoom: 4.2 }); }); await expect .poll(async () => { return await page.evaluate(() => { const w = window as HarnessWindow; return w.__mapHarness?.getLayerDataCount('gulf-investments-layer') ?? 0; }); }, { timeout: 30000 }) .toBeGreaterThan(0); }); test('sanitizes cyber threat tooltip content', async ({ page }) => { await waitForHarnessReady(page); const html = await page.evaluate(() => { const w = window as HarnessWindow; return w.__mapHarness?.getCyberTooltipHtml('') ?? ''; }); expect(html).toContain('<script>alert(1)</script>'); expect(html).not.toContain('