fix: hover-route line not drawn in e2e — MapLibre clips queried lines across tiles and quantizes their coordinates (strict single-feature equality never matched)
CI / build + unit + e2e (pull_request) Failing after 6m44s

This commit is contained in:
2026-09-19 22:52:12 +00:00
parent ac4e3fa316
commit 2655ef81c8
2 changed files with 23 additions and 12 deletions
+22 -12
View File
@@ -174,22 +174,32 @@ async function airportLonLat(page: Page, icao: string): Promise<[number, number]
return [f.properties.lon, f.properties.lat];
}
// ISS-022/025/027: the hover-route layer must hold exactly one LineString whose first and last
// coordinates are the two airports' exact dataset coords (greatCircleArc returns them verbatim
// at t=0/t=1, so strict equality is safe). Call fitToAirports([from, to]) first — rendered
// features are viewport-scoped.
// ISS-022/025/027: the hover-route layer must hold the great-circle trace from origin to dest.
// MapLibre stores GeoJSON geometry in per-tile INTEGER grids and clips long lines across tiles, so
// queryRenderedFeatures returns ONE FEATURE PER TILE the line crosses (the 1420 km EDDF→LEMD leg is
// 4 segments at z=4 / 5 at z=5) with coordinates quantized to the rendered zoom's grid — up to ~half
// a grid step off (~0.001° observed at z≥4, ~0.02° worst case at z=0). A strict single-feature +
// exact-coords comparison can therefore never match even when the line is drawn correctly (CI run #69:
// all three hover-route timeouts while the trace was visibly rendered), so we accept any number of
// segments and require some segment to START within EPS of `from` and some to END within EPS of `to`.
// The true endpoints survive clipping verbatim in whichever tile contains them. Call
// fitToAirports([from, to]) first — rendered features are viewport-scoped.
const HOVER_ROUTE_EPS_DEG = 0.05; // ~5 km: above worst-case quantization at any zoom, below the distance between any two EU airports
async function expectHoverRoute(page: Page, fromIcao: string, toIcao: string): Promise<void> {
const [from, to] = await Promise.all([airportLonLat(page, fromIcao), airportLonLat(page, toIcao)]);
const near = (a: [number, number], b: [number, number]) =>
Math.abs(a[0] - b[0]) <= HOVER_ROUTE_EPS_DEG && Math.abs(a[1] - b[1]) <= HOVER_ROUTE_EPS_DEG;
await expect.poll(async () => {
const feats = (await renderedFeatures(page, ['hover-route'])) as Array<{ geometry: { coordinates: [number, number][] } }>;
if (feats.length !== 1) return false;
const coords = feats[0].geometry.coordinates;
return (
coords[0][0] === from[0] &&
coords[0][1] === from[1] &&
coords[coords.length - 1][0] === to[0] &&
coords[coords.length - 1][1] === to[1]
);
if (!feats.length) return false;
// The line may be clipped into several per-tile segments — check the endpoints across all of them.
const startsAtFrom = feats.some((f) => near(f.geometry.coordinates[0], from));
const endsAtTo = feats.some((f) => {
const c = f.geometry.coordinates;
return near(c[c.length - 1], to);
});
return startsAtFrom && endsAtTo;
}, { timeout: 15_000, message: `waiting for the hover-route line ${fromIcao} → ${toIcao}` }).toBe(true);
}