fix(map): reproject overlay clusters on pan and harden e2e visual determinism

This commit is contained in:
Elie Habib
2026-02-12 18:00:54 +04:00
parent b0e829a84c
commit 2b6add1f0d
58 changed files with 50 additions and 34 deletions

View File

@@ -114,18 +114,13 @@ const waitForHarnessReady = async (
.toBe(true);
};
const getMarkerCenter = async (
const getMarkerInlineTransform = async (
page: import('@playwright/test').Page,
selector: string
): Promise<{ x: number; y: number } | null> => {
): Promise<string | null> => {
return await page.evaluate((sel) => {
const el = document.querySelector(sel);
if (!el) return null;
const rect = el.getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2,
};
const el = document.querySelector(sel) as HTMLElement | null;
return el?.style.transform ?? null;
}, selector);
};
@@ -365,8 +360,8 @@ test.describe('DeckGL map harness', () => {
const markerSelector = '.protest-marker';
await expect(page.locator(markerSelector).first()).toBeVisible();
const before = await getMarkerCenter(page, markerSelector);
expect(before).not.toBeNull();
const beforeTransform = await getMarkerInlineTransform(page, markerSelector);
expect(beforeTransform).not.toBeNull();
await page.evaluate(() => {
const w = window as HarnessWindow;
@@ -375,11 +370,8 @@ test.describe('DeckGL map harness', () => {
await page.waitForTimeout(750);
const after = await getMarkerCenter(page, markerSelector);
expect(after).not.toBeNull();
const dx = Math.abs((after?.x ?? 0) - (before?.x ?? 0));
const dy = Math.abs((after?.y ?? 0) - (before?.y ?? 0));
expect(Math.max(dx, dy)).toBeGreaterThan(10);
const afterTransform = await getMarkerInlineTransform(page, markerSelector);
expect(afterTransform).not.toBeNull();
expect(afterTransform).not.toBe(beforeTransform);
});
});

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -30,10 +30,11 @@ export default defineConfig({
},
},
],
snapshotPathTemplate: '{testDir}/{testFileName}-snapshots/{arg}{ext}',
webServer: {
command: 'npm run dev -- --host 127.0.0.1 --port 4173',
url: 'http://127.0.0.1:4173/map-harness.html',
reuseExistingServer: true,
reuseExistingServer: false,
timeout: 120000,
},
});

View File

@@ -478,6 +478,16 @@ export class DeckGLMap {
return false;
}
private projectClusterCenter(
center: [number, number],
fallback: [number, number]
): [number, number] {
if (!this.maplibreMap) return fallback;
const projected = this.maplibreMap.project(center);
if (!projected) return fallback;
return [projected.x, projected.y];
}
private updateClusterElement(
key: string,
screenPos: [number, number],
@@ -550,10 +560,11 @@ export class DeckGLMap {
this.clusterResultCache.set(cacheKey, clusters);
}
clusters.forEach((cluster) => {
const screenPos = this.projectClusterCenter(cluster.center, cluster.screenPos);
const key = this.getClusterKey('hq', cluster.center, cluster.items.length);
activeKeys.add(key);
if (this.hasClusterMoved(key, cluster.screenPos, cluster.items.length)) {
const element = this.updateClusterElement(key, cluster.screenPos, () => this.createTechHQClusterElement(cluster));
if (this.hasClusterMoved(key, screenPos, cluster.items.length)) {
const element = this.updateClusterElement(key, screenPos, () => this.createTechHQClusterElement(cluster));
if (!element.parentElement) this.clusterOverlay!.appendChild(element);
}
});
@@ -569,10 +580,11 @@ export class DeckGLMap {
this.clusterResultCache.set(cacheKey, clusters);
}
clusters.forEach((cluster) => {
const screenPos = this.projectClusterCenter(cluster.center, cluster.screenPos);
const key = this.getClusterKey('event', cluster.center, cluster.items.length);
activeKeys.add(key);
if (this.hasClusterMoved(key, cluster.screenPos, cluster.items.length)) {
const element = this.updateClusterElement(key, cluster.screenPos, () => this.createTechEventClusterElement(cluster));
if (this.hasClusterMoved(key, screenPos, cluster.items.length)) {
const element = this.updateClusterElement(key, screenPos, () => this.createTechEventClusterElement(cluster));
if (!element.parentElement) this.clusterOverlay!.appendChild(element);
}
});
@@ -589,10 +601,11 @@ export class DeckGLMap {
this.clusterResultCache.set(cacheKey, clusters);
}
clusters.forEach((cluster) => {
const screenPos = this.projectClusterCenter(cluster.center, cluster.screenPos);
const key = this.getClusterKey('protest', cluster.center, cluster.items.length);
activeKeys.add(key);
if (this.hasClusterMoved(key, cluster.screenPos, cluster.items.length)) {
const element = this.updateClusterElement(key, cluster.screenPos, () => this.createProtestClusterElement(cluster));
if (this.hasClusterMoved(key, screenPos, cluster.items.length)) {
const element = this.updateClusterElement(key, screenPos, () => this.createProtestClusterElement(cluster));
if (!element.parentElement) this.clusterOverlay!.appendChild(element);
}
});
@@ -608,10 +621,11 @@ export class DeckGLMap {
this.clusterResultCache.set(cacheKey, clusters);
}
clusters.forEach((cluster) => {
const screenPos = this.projectClusterCenter(cluster.center, cluster.screenPos);
const key = this.getClusterKey('dc', cluster.center, cluster.items.length);
activeKeys.add(key);
if (this.hasClusterMoved(key, cluster.screenPos, cluster.items.length)) {
const element = this.updateClusterElement(key, cluster.screenPos, () => this.createDatacenterClusterElement(cluster));
if (this.hasClusterMoved(key, screenPos, cluster.items.length)) {
const element = this.updateClusterElement(key, screenPos, () => this.createDatacenterClusterElement(cluster));
if (!element.parentElement) this.clusterOverlay!.appendChild(element);
}
});

View File

@@ -193,6 +193,8 @@ const map = new DeckGLMap(app, {
timeRange: '24h',
});
const DETERMINISTIC_BODY_CLASS = 'e2e-deterministic';
const internals = map as unknown as {
buildLayers?: () => Array<{ id: string; props?: { data?: unknown } }>;
lastClusterState?: Map<string, unknown>;
@@ -873,13 +875,20 @@ const ensureDeterministicStyles = (): void => {
const style = document.createElement('style');
style.id = DETERMINISTIC_STYLE_ID;
style.textContent = `
.deckgl-controls,
.deckgl-time-slider,
.deckgl-layer-toggles,
.deckgl-legend,
.deckgl-timestamp,
.maplibregl-ctrl-bottom-right,
.maplibregl-ctrl-bottom-left {
body.${DETERMINISTIC_BODY_CLASS} *,
body.${DETERMINISTIC_BODY_CLASS} *::before,
body.${DETERMINISTIC_BODY_CLASS} *::after {
animation: none !important;
transition: none !important;
}
body.${DETERMINISTIC_BODY_CLASS} .deckgl-controls,
body.${DETERMINISTIC_BODY_CLASS} .deckgl-time-slider,
body.${DETERMINISTIC_BODY_CLASS} .deckgl-layer-toggles,
body.${DETERMINISTIC_BODY_CLASS} .deckgl-legend,
body.${DETERMINISTIC_BODY_CLASS} .deckgl-timestamp,
body.${DETERMINISTIC_BODY_CLASS} .maplibregl-ctrl-bottom-right,
body.${DETERMINISTIC_BODY_CLASS} .maplibregl-ctrl-bottom-left {
display: none !important;
}
`;
@@ -900,7 +909,7 @@ const hideRasterBasemap = (): void => {
};
const enableDeterministicVisualMode = (): void => {
document.body.classList.add('animations-paused');
document.body.classList.add(DETERMINISTIC_BODY_CLASS);
ensureDeterministicStyles();
hideRasterBasemap();
makeNewsLocationsNonRecent();