v0.22.0 — ISS-022/025/027 hover route trace (job list, on-board rows, relocation panel) #34
+1
-1
@@ -8,7 +8,7 @@
|
||||
- Tests: unit — new `describe('hoverRouteGeoJson …')` in `lib/mapData.test.ts`: no-hover sentinel → empty; two known airports → one arc with exact dataset endpoints; reversed-input symmetry; missing from/to ICAO → empty (baseline 283 → 288). e2e (`e2e/hover.spec.ts`) — open-package test extended with hover-route endpoint assertions (line geometry first/last coords compared against `/api/airports` lookups) + trace-clear check; new `hovering an on-board cargo row draws its trace and keeps the plane halo` (load via API, hover the row, assert trace endpoints + planes-highlight for that plane, abandon the package afterwards to leave a clean instance); new `hovering the Relocate section previews the planned route and target airport` (selects starter + LEMD via the existing testids, hovers `[data-testid="relocate-section"]`, asserts trace endpoints + target-only ring). New helpers: `airportLonLat(page, icao)` and `expectHoverRoute(page, fromIcao, toIcao)`; all new/extended assertions call `fitToAirports` first since `queryRenderedFeatures` is viewport-scoped
|
||||
- Verified: `tsc --noEmit` clean; unit suite green locally (288); `npx playwright test --list` = 41 tests (was 39, +2 new in hover.spec.ts); e2e not run locally by design — CI verifies against an isolated stack
|
||||
- **Fix: hover-route e2e timeouts (CI run #69).** All three trace tests failed at `expectHoverRoute` with the 15 s "waiting for the hover-route line EDDF → LEMD" timeout although the dashed trace WAS drawn correctly — the helper assumed `queryRenderedFeatures(undefined, {layers:['hover-route']})` returns exactly one feature whose endpoints are bit-identical to the `/api/airports` dataset coords. MapLibre GL JS stores GeoJSON geometry in per-tile INTEGER grids (`Math.round(extent·(x·2^z − tx))`, maplibre-gl 6.10) and clips long lines across tiles, so a 1420 km leg comes back as one feature PER intersecting tile (empirically 4 at z=4 / 5 at z=5 for EDDF→LEMD, verified against the installed build's GeoJSONVT) with endpoints quantized by up to ~half a grid step (~0.001° observed at z≥4, ~0.02° worst case at z=0): both the `feats.length !== 1` guard and the strict `===` coordinate comparisons could never be true. The helper now accepts any number of segments and matches endpoints within 0.05° (~5 km — above worst-case quantization at any zoom, below the distance between any two EU airports): some segment must start at origin and some end at destination; all other assertions unchanged. App side verified correct (no race: `ensureLayers` awaits the airport fetch before creating sources/layers and re-applies hover afterwards; layer/source ids match) — no app changes
|
||||
- **Fix: hover-route e2e TypeError (CI run #70).** `expectHoverRoute` in `e2e/hover.spec.ts` crashed with `Cannot read properties of undefined (reading 'coordinates')` because MapLibre's `queryRenderedFeatures` can return per-tile artifact features without a usable LineString geometry; a throw inside the `expect.poll` callback fails instantly instead of retrying, killing all three trace tests in ~2 s. The helper now filters to real line segments (LineString with ≥2 coordinates) and treats 'none yet' as keep-polling — no app changes
|
||||
- **Fix: hover-route e2e — evaluate maplibre geometry in-page before Playwright serialization (CI runs #69–#71).** All three trace tests failed for a serialization reason, not a data one: `renderedFeatures` returned live `GeoJSONFeature` instances from `queryRenderedFeatures`, whose `geometry` is a lazy PROTOTYPE getter — and Playwright's evaluate serialization copies only own enumerable properties (verified empirically), so every feature arrived in test context WITHOUT geometry. Run #69 timed out on the strict length guard before touching it, run #70 threw `TypeError: Cannot read properties of undefined (reading 'coordinates')`, run #71's defensive filter found nothing and timed out. The helper now maps each feature to a plain object inside page.evaluate so the getter is evaluated in-page; the endpoint-matching predicate keeps its degenerate-segment filter as belt-and-braces — no app changes
|
||||
|
||||
## v0.21.0 — 2026-09-19 21:01
|
||||
- **Relocate panel moves to the left pane (ISS-023).** Flying an empty/grounded plane to another airport is "anything with our plane", so it belongs in the left pane, not as a market row on the right. The `.market-row.market-row-relocate` JSX (`relocate-plane` select of grounded planes labeled `{planeName} — {icao}`, `relocate-dest` name-sorted select, `relocate-go` button → `POST /api/planes/{id}/fly {destIcao}`) moved verbatim from `components/RightPane.tsx` into a new "Relocate" `<section>` in `components/LeftPane.tsx`, inserted between On board and Time. Its state (`relocatePlaneId` / `relocateDestIcao`) and the one-time `/api/airports` fetch (promise-ref pattern, GeoJSON → `{name, icao}[]` sorted by name) moved with it — the relocate-dest select was that fetch's only consumer in RightPane, so nothing else there changed. Busy gating rewired from RightPane's `busy` to LeftPane's own `useGameAction` instance (`actionsBusy`); a grounded-plane filter was added next to the existing `onboard` compute; plane labels reuse the expression already inlined in the Fleet rows. Testids kept byte-identical (`relocate-plane` / `relocate-dest` / `relocate-go`) and the row keeps its `.market-row market-row-relocate` classes (global CSS, works in the 360 px left pane); stale "280 px right pane" comment updated in `app/globals.css`. The map-popup relocation controls (`airport-popup-*`, Map.tsx) are a separate surface fed by the snapshot only — untouched
|
||||
|
||||
+29
-10
@@ -117,12 +117,30 @@ async function waitForAirportCount(page: Page): Promise<void> {
|
||||
}
|
||||
|
||||
// Rendered features of the given layers in the current viewport, [] until the map exists.
|
||||
function renderedFeatures(page: Page, layers: string[]) {
|
||||
function renderedFeatures(
|
||||
page: Page,
|
||||
layers: string[],
|
||||
): Promise<Array<{ type?: unknown; id?: unknown; properties?: unknown; geometry?: unknown }>> {
|
||||
return page.evaluate((ls: string[]) => {
|
||||
const m = (window as any).__flightMap;
|
||||
if (!m) return [];
|
||||
try {
|
||||
return m.queryRenderedFeatures(undefined, { layers: ls });
|
||||
// queryRenderedFeatures returns live GeoJSONFeature instances whose `geometry` is a lazy
|
||||
// PROTOTYPE getter — Playwright's evaluate serialization copies only own enumerable props,
|
||||
// so geometry silently vanishes across the page boundary (CI runs #69–#71: strict-equality
|
||||
// timeouts → TypeError on f.geometry.coordinates → filter-found-nothing timeouts). Map to
|
||||
// plain objects IN-PAGE so the getter is evaluated before serialization.
|
||||
return (m.queryRenderedFeatures(undefined, { layers: ls }) as Array<{
|
||||
type?: unknown;
|
||||
id?: unknown;
|
||||
properties?: unknown;
|
||||
geometry?: unknown;
|
||||
}>).map((f) => ({
|
||||
type: f.type,
|
||||
id: f.id,
|
||||
properties: f.properties,
|
||||
geometry: f.geometry,
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
@@ -182,11 +200,12 @@ async function airportLonLat(page: Page, icao: string): Promise<[number, number]
|
||||
// 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. CI run #70
|
||||
// additionally showed queryRenderedFeatures can return per-tile artifact features WITHOUT a usable
|
||||
// geometry at all (f.geometry undefined), so the helper filters to real LineString segments and
|
||||
// treats "none yet" as keep-polling instead of throwing inside the poll callback. Call
|
||||
// fitToAirports([from, to]) first — rendered features are viewport-scoped.
|
||||
// The true endpoints survive clipping verbatim in whichever tile contains them. (CI runs #69–#71:
|
||||
// features could arrive WITHOUT a usable geometry because Playwright's evaluate serialization drops
|
||||
// the lazy prototype getter — not MapLibre artifacts; renderedFeatures now maps to plain objects
|
||||
// in-page.) The helper still filters to real LineString segments and treats "none yet" as keep-polling
|
||||
// instead of throwing inside the poll callback. 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> {
|
||||
@@ -195,9 +214,9 @@ async function expectHoverRoute(page: Page, fromIcao: string, toIcao: string): P
|
||||
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?: { type: string; coordinates: [number, number][] } }>;
|
||||
// CI run #70: MapLibre can return per-tile artifacts WITHOUT a usable LineString geometry
|
||||
// (f.geometry undefined). A throw inside an expect.poll callback fails the test instantly
|
||||
// instead of retrying — so keep only real line segments; "none yet" means keep polling.
|
||||
// Features are plain objects with in-page-evaluated geometry (see renderedFeatures). The filter
|
||||
// guards against degenerate/zero-length clipped segments and keeps this poll callback total — a
|
||||
// throw inside expect.poll fails the test instantly instead of retrying. "None yet" → keep polling.
|
||||
const segs = feats.filter(
|
||||
(f): f is { geometry: { type: 'LineString'; coordinates: [number, number][] } } =>
|
||||
!!f.geometry && f.geometry.type === 'LineString' && Array.isArray(f.geometry.coordinates) && f.geometry.coordinates.length >= 2,
|
||||
|
||||
Reference in New Issue
Block a user