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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user