fix(map): reproject overlay clusters on pan and harden e2e visual determinism
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 6.3 KiB |
BIN
e2e/map-harness.spec.ts-snapshots/layer-full-news-z5.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
BIN
e2e/map-harness.spec.ts-snapshots/layer-tech-news-z5.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||