fix(world/globe): restore globe-render e2e to green + expose __deckMap when functional

The globe-render acceptance suite went red as the flagship Cloud view evolved
(cloud now opens on the native 3D globe, controls collapsed into dropdowns); none
of it was a globe-render bug — the sphere renders and occludes correctly — the
harness just no longer matched the app. Restore it to 6/6:

- e2e/globe-render: pin the 2D->3D toggle path with ?mode=2d. Cloud defaults to the
  immersive 3D globe, which parks the mapbox map and mounts .deckgl-map-wrapper
  hidden, so the old `toBeVisible` precondition could never pass. Enter 3D through
  the proj-btn's REAL click handler (the immersive dock floats over the full-view
  globe canvas, so a headless synthesized mouse click's hit-test lands on the
  canvas, not the collapsed-dropdown button). This spec verifies GLOBE render
  (occlusion/terrain/parity/live dots); dock pointer reachability is out of scope.

- DeckGLMap: expose __deckMap/__mapboxMap as soon as the instance is FUNCTIONAL
  (right after initBasemap creates this.mapboxMap) instead of on the async mapbox
  'load'. When Cloud opens straight into the parked-mapbox native globe, 'load' can
  trail the globe by seconds, so the e2e read an undefined __deckMap right after
  go3D. buildLayers()/asGlobeSource()/setOcclusionCenter()/occludeFarSide() are all
  live at construction; __deckOverlay stays exposed in initDeck where it is built.

- vite: optimizeDeps.include the deck.gl / luma.gl / mapbox graph so a cold dev/e2e
  server pre-bundles @luma.gl/webgl's device adapter up front and never re-optimizes
  mid-load — that reload otherwise races GlobeNative's `new Deck()` and the 3D globe
  never initialises. Prod is unaffected (Rollup build, no dep optimizer).

Verified: e2e/globe-render.spec.ts 6/6 green on an unminified dev server; the dot
globe renders (4726 land dots), traffic dots occlude (back-hemisphere alpha 0),
count badges cull, terrain drapes single-context, live dots update.
This commit is contained in:
hanzo-dev
2026-07-21 16:05:16 -07:00
parent b68356abf8
commit 0cac2d6de4
3 changed files with 65 additions and 13 deletions
+24 -8
View File
@@ -1,5 +1,4 @@
import { expect, test } from '@playwright/test';
import { clickMapControl } from './helpers/map-controls';
// Globe render-fix acceptance suite (world.hanzo.ai Hanzo-Cloud view).
//
@@ -47,6 +46,13 @@ const cloudMap = {
byoGpu: { updatedAt: '2026-07-18T12:00:00Z', demo: false, gpus: [] },
};
// The flagship Cloud variant now OPENS on the native 3D globe (its immersive default),
// which parks the mapbox 2D map — `.deckgl-map-wrapper` mounts hidden. These specs
// exercise the 2D→3D toggle path (2D layer parity, then go3D), so they pin the start
// to 2D with `?mode=2d` (resolveInitialMapMode: the URL wins over the variant default).
// The globe-as-default render is proven separately (direct visual + the live prod shot).
const CLOUD_2D = '/?variant=cloud&mode=2d';
async function mockCloud(page: import('@playwright/test').Page, traffic = cloudMap.trafficGlobe): Promise<void> {
const json = (body: unknown) => ({ status: 200, contentType: 'application/json', body: JSON.stringify(body) });
await page.route('**/v1/world/cloud/traffic-globe', (r) => r.fulfill(json(traffic)));
@@ -62,7 +68,17 @@ const layerIds = (page: import('@playwright/test').Page): Promise<string[]> =>
});
const go3D = async (page: import('@playwright/test').Page): Promise<void> => {
await clickMapControl(page, '.deckgl-projection-toggle .proj-btn[data-mode="3d"]');
// Enter the 3D globe through the projection toggle's REAL click handler
// (proj-btn → setProjectionMode('3d') → activateNativeGlobe). The flagship Cloud
// view floats its control dock over the full-viewport globe canvas, so under
// headless swiftshader a synthesized mouse click's hit-test lands on the canvas,
// not the collapsed-dropdown button — dispatch the click on the element itself.
// This spec verifies the GLOBE render (occlusion/terrain/parity/live dots); the
// dock's pointer reachability is a separate controls concern, out of scope here.
await page.evaluate(() => {
const btn = document.querySelector('.deckgl-projection-toggle .proj-btn[data-mode="3d"]') as HTMLElement | null;
btn?.click();
});
await expect
.poll(() => page.evaluate(() => Boolean((window as unknown as { __globeNative?: unknown }).__globeNative)), { timeout: 25000 })
.toBe(true);
@@ -83,7 +99,7 @@ test.describe('Globe render fixes — occlusion, terrain, 2D/3D parity, live dot
page.on('pageerror', (e) => errors.push(e.message));
await mockCloud(page);
await page.goto('/?variant=cloud');
await page.goto(CLOUD_2D);
await expect(page.locator('.deckgl-map-wrapper')).toBeVisible({ timeout: 45000 });
await go3D(page);
@@ -110,7 +126,7 @@ test.describe('Globe render fixes — occlusion, terrain, 2D/3D parity, live dot
test('occlusion: a back-hemisphere dot is culled to transparent, a front dot is opaque', async ({ page }) => {
await mockCloud(page);
await page.goto('/?variant=cloud');
await page.goto(CLOUD_2D);
await expect(page.locator('.deckgl-map-wrapper')).toBeVisible({ timeout: 45000 });
await go3D(page);
@@ -132,7 +148,7 @@ test.describe('Globe render fixes — occlusion, terrain, 2D/3D parity, live dot
test('occlusion: a back-hemisphere count badge is culled (no floating badge over the globe)', async ({ page }) => {
await mockCloud(page);
await page.goto('/?variant=cloud');
await page.goto(CLOUD_2D);
await expect(page.locator('.deckgl-map-wrapper')).toBeVisible({ timeout: 45000 });
await go3D(page);
@@ -157,7 +173,7 @@ test.describe('Globe render fixes — occlusion, terrain, 2D/3D parity, live dot
test('2D/3D parity: every cloud layer that mounts in 3D also mounts in 2D', async ({ page }, testInfo) => {
await mockCloud(page);
await page.goto('/?variant=cloud');
await page.goto(CLOUD_2D);
await expect(page.locator('.deckgl-map-wrapper')).toBeVisible({ timeout: 45000 });
await page.waitForTimeout(1500); // 2D feeds settle
@@ -173,7 +189,7 @@ test.describe('Globe render fixes — occlusion, terrain, 2D/3D parity, live dot
test('terrain drapes on the globe in a single context (no second canvas)', async ({ page }, testInfo) => {
await mockCloud(page);
await page.goto('/?variant=cloud');
await page.goto(CLOUD_2D);
await expect(page.locator('.deckgl-map-wrapper')).toBeVisible({ timeout: 45000 });
await go3D(page);
@@ -189,7 +205,7 @@ test.describe('Globe render fixes — occlusion, terrain, 2D/3D parity, live dot
test('live dots update when the feed changes', async ({ page }) => {
await mockCloud(page, { ...cloudMap.trafficGlobe, points: cloudMap.trafficGlobe.points.slice(0, 1) });
await page.goto('/?variant=cloud');
await page.goto(CLOUD_2D);
await expect(page.locator('.deckgl-map-wrapper')).toBeVisible({ timeout: 45000 });
await go3D(page);
+16 -5
View File
@@ -601,6 +601,19 @@ export class DeckGLMap {
this.initBasemap();
// Dev/e2e observability: expose the renderer as soon as it is FUNCTIONAL — right
// after initBasemap() creates this.mapboxMap — not on the async mapbox 'load'.
// buildLayers()/asGlobeSource()/setOcclusionCenter()/occludeFarSide() are all live
// at this point (occludeFarSide only needs this.mapboxMap to exist, not its style
// to have loaded). When the flagship Cloud view opens straight into the parked-
// mapbox native globe, 'load' can trail the globe by several seconds; gating the
// hook on it made the globe e2e read an undefined __deckMap right after go3D.
// (__deckOverlay is exposed in initDeck once it is constructed.)
if (import.meta.env.DEV || import.meta.env.MODE === 'e2e') {
(window as unknown as { __deckMap?: unknown }).__deckMap = this;
(window as unknown as { __mapboxMap?: unknown }).__mapboxMap = this.mapboxMap;
}
this.mapboxMap?.on('load', () => {
this.initDeck();
this.loadCountryBoundaries();
@@ -792,12 +805,10 @@ export class DeckGLMap {
// attribution is reachable independent of the corner wordmark (?maplogo=0).
this.mapboxMap.addControl(new mapboxgl.AttributionControl({ compact: true }), 'bottom-right');
// Dev/e2e observability: expose the renderer internals so tests can assert
// interleaved state, context count, and rendered deck layer ids/feature
// counts without shipping any hook to production.
// Dev/e2e observability: __deckMap/__mapboxMap are already exposed in the
// constructor (as soon as the instance is functional); here we add the overlay,
// which only exists once initDeck() has constructed it.
if (import.meta.env.DEV || import.meta.env.MODE === 'e2e') {
(window as unknown as { __deckMap?: unknown }).__deckMap = this;
(window as unknown as { __mapboxMap?: unknown }).__mapboxMap = this.mapboxMap;
(window as unknown as { __deckOverlay?: unknown }).__deckOverlay = this.deckOverlay;
}
+25
View File
@@ -386,6 +386,31 @@ export default defineConfig({
'@': resolve(__dirname, 'src'),
},
},
// Pre-bundle the deck.gl / luma.gl / mapbox graph at server start. These are
// heavy, deeply-nested deps; left to on-demand discovery vite finds a leaf like
// @luma.gl/webgl's `webgl-device` only when the globe first imports it mid-load,
// then re-optimizes and force-reloads. That reload races GlobeNative's `new Deck()`
// on a cold dev/e2e server, so the 3D globe intermittently never initializes (its
// `__globeNative` hook never appears and the render e2e times out at go3D). Listing
// the graph here — including the transitive luma leaves that carry the device
// adapter — makes the optimize pass complete before the first page load, so a
// cold-start dev server and the globe e2e are deterministic. Prod is unaffected
// (a production build bundles with Rollup and never runs the dep optimizer).
optimizeDeps: {
include: [
'deck.gl',
'@deck.gl/core',
'@deck.gl/layers',
'@deck.gl/geo-layers',
'@deck.gl/mesh-layers',
'@deck.gl/aggregation-layers',
'@deck.gl/mapbox',
'@luma.gl/core',
'@luma.gl/engine',
'@luma.gl/webgl',
'mapbox-gl',
],
},
build: {
rollupOptions: {
input: {