feat: v0.22.0 — ISS-022 target airport distinct-color ring on job-list / relocation hover
CI / build + unit + e2e (pull_request) Failing after 8m51s
CI / build + unit + e2e (pull_request) Failing after 8m51s
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
- **Hover route trace on job-list rows (ISS-022).** Hovering an open package in the right pane now draws the ENTIRE great-circle trace between origin and destination, not just the two endpoint rings. Zero pane edits — the rows already sent `{kind:'package', originIcao, destIcao}`; `components/Map.tsx` now derives from/to for any hover target carrying two ICAOs and pushes a new pure builder's output (`hoverRouteGeoJson(fromIcao, toIcao, airportsByIcao)` in `lib/mapData.ts`: one LineString via the existing `greatCircleArc` slerp, empty FeatureCollection when either endpoint is missing from the airport index or the `''` no-hover sentinel) into a NEW dedicated `hover-routes` GeoJSON source + top-most dashed `hover-route` LineLayer (paint: `#facc15` matching the airports-highlight ring, width 4, `line-dasharray [2,2]`). The shared `routes` source can't carry this — it only holds FLYING legs, so an open package's or a grounded relocation's planned route was never in it. The layer is added in `ensureLayers` after the highlight layers so style switches keep it; `applyHoverRef` re-pushes data on every hover
|
- **Hover route trace on job-list rows (ISS-022).** Hovering an open package in the right pane now draws the ENTIRE great-circle trace between origin and destination, not just the two endpoint rings. Zero pane edits — the rows already sent `{kind:'package', originIcao, destIcao}`; `components/Map.tsx` now derives from/to for any hover target carrying two ICAOs and pushes a new pure builder's output (`hoverRouteGeoJson(fromIcao, toIcao, airportsByIcao)` in `lib/mapData.ts`: one LineString via the existing `greatCircleArc` slerp, empty FeatureCollection when either endpoint is missing from the airport index or the `''` no-hover sentinel) into a NEW dedicated `hover-routes` GeoJSON source + top-most dashed `hover-route` LineLayer (paint: `#facc15` matching the airports-highlight ring, width 4, `line-dasharray [2,2]`). The shared `routes` source can't carry this — it only holds FLYING legs, so an open package's or a grounded relocation's planned route was never in it. The layer is added in `ensureLayers` after the highlight layers so style switches keep it; `applyHoverRef` re-pushes data on every hover
|
||||||
- **Same trace on On-board cargo rows (ISS-025).** Each `onboard-package-{id}` row in the left pane's On board section now reports `{kind:'package', originIcao, destIcao, planeId: plane.id}` on mouseenter — same full trace as job-list rows PLUS the carrying plane's halo stays lit via the new optional `planeId` field (Map uses `h.planeId ?? ''` for the planes/routes-highlight filters). Mouseleave restores the block's plain `{kind:'plane', id}` highlight; leaving the whole block still ends at null because React fires the child's mouseleave before the parent's
|
- **Same trace on On-board cargo rows (ISS-025).** Each `onboard-package-{id}` row in the left pane's On board section now reports `{kind:'package', originIcao, destIcao, planeId: plane.id}` on mouseenter — same full trace as job-list rows PLUS the carrying plane's halo stays lit via the new optional `planeId` field (Map uses `h.planeId ?? ''` for the planes/routes-highlight filters). Mouseleave restores the block's plain `{kind:'plane', id}` highlight; leaving the whole block still ends at null because React fires the child's mouseleave before the parent's
|
||||||
- **Relocate panel hover previews the planned flight (ISS-027).** The left pane's Relocate section gains `data-testid="relocate-section"` + enter/leave handlers: with a grounded plane AND destination selected it reports `{kind:'route', fromIcao: relocatePlane.locationIcao, toIcao: relocateDest.icao}` (else null), and Map draws the planned trace AND rings ONLY the target airport in airports-highlight (origin stays unmarked — the destination is what the issue asks to see). `HoverTarget` (`components/hoverTypes.ts`) extends to `{kind:'plane';id} | {kind:'package';originIcao;destIcao;planeId?} | {kind:'route';fromIcao;toIcao}`. Existing semantics unchanged: plane hovers filter the three highlight layers exactly as before, package hovers keep BOTH endpoint rings (now plus the trace)
|
- **Relocate panel hover previews the planned flight (ISS-027).** The left pane's Relocate section gains `data-testid="relocate-section"` + enter/leave handlers: with a grounded plane AND destination selected it reports `{kind:'route', fromIcao: relocatePlane.locationIcao, toIcao: relocateDest.icao}` (else null), and Map draws the planned trace AND rings ONLY the target airport in airports-highlight (origin stays unmarked — the destination is what the issue asks to see). `HoverTarget` (`components/hoverTypes.ts`) extends to `{kind:'plane';id} | {kind:'package';originIcao;destIcao;planeId?} | {kind:'route';fromIcao;toIcao}`. Existing semantics unchanged: plane hovers filter the three highlight layers exactly as before, package hovers keep BOTH endpoint rings (now plus the trace)
|
||||||
|
- **Target airport highlighted in a distinct color on job-list / relocation hover (ISS-022).** The destination of any two-endpoint hover now gets its own ring in a NEW top-most `airports-target-highlight` circle layer (`components/Map.tsx`, same `airports` source and r=9 transparent-fill geometry as airports-highlight, but green `#4ade80` stroke width 3 vs the yellow `#facc15` endpoint rings) — added last in `ensureLayers` so it also sits above the hover-route line at its endpoint; `applyHoverRef` filters it to the destination icao for BOTH `'package'` and `'route'` hovers (empty-string sentinel otherwise). airports-highlight itself is untouched, so the existing both-endpoints / target-only assertions stay valid
|
||||||
- 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
|
- 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
|
- 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
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ const AIRPORTS_HIGHLIGHT_LAYER_ID = 'airports-highlight';
|
|||||||
// relocation's planned route is never in it. applyHoverRef pushes data into it on every hover.
|
// relocation's planned route is never in it. applyHoverRef pushes data into it on every hover.
|
||||||
const HOVER_ROUTES_SOURCE_ID = 'hover-routes';
|
const HOVER_ROUTES_SOURCE_ID = 'hover-routes';
|
||||||
const HOVER_ROUTE_LAYER_ID = 'hover-route';
|
const HOVER_ROUTE_LAYER_ID = 'hover-route';
|
||||||
|
// ISS-022 (v0.22.0): the DESTINATION airport's ring — a second, differently-colored circle over
|
||||||
|
// the same `airports` source so the target stands out from the origin's plain yellow ring.
|
||||||
|
// applyHoverRef filters it to the hovered target's destination icao only.
|
||||||
|
const AIRPORTS_TARGET_HIGHLIGHT_LAYER_ID = 'airports-target-highlight';
|
||||||
|
|
||||||
function escapeHtml(s: string): string {
|
function escapeHtml(s: string): string {
|
||||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
@@ -146,6 +150,10 @@ export default function FlightMap({ state, hovered, applySnapshot }: MapProps) {
|
|||||||
['any', ['==', ['get', 'icao'], originIcao], ['==', ['get', 'icao'], destIcao]],
|
['any', ['==', ['get', 'icao'], originIcao], ['==', ['get', 'icao'], destIcao]],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// ISS-022: the destination gets its own distinct-colored ring on top (package AND route hovers).
|
||||||
|
if (map.getLayer(AIRPORTS_TARGET_HIGHLIGHT_LAYER_ID)) {
|
||||||
|
map.setFilter(AIRPORTS_TARGET_HIGHLIGHT_LAYER_ID, ['==', ['get', 'icao'], destIcao]);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -431,6 +439,23 @@ export default function FlightMap({ state, hovered, applySnapshot }: MapProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// ISS-022 (v0.22.0): the destination's ring — same geometry as airports-highlight but a
|
||||||
|
// DIFFERENT color so the target stands out over the origin's yellow ring; added last so it
|
||||||
|
// also sits above the hover-route line at its endpoint. Starts filtered to match nothing.
|
||||||
|
if (!map.getLayer(AIRPORTS_TARGET_HIGHLIGHT_LAYER_ID)) {
|
||||||
|
map.addLayer({
|
||||||
|
id: AIRPORTS_TARGET_HIGHLIGHT_LAYER_ID,
|
||||||
|
type: 'circle',
|
||||||
|
source: AIRPORTS_SOURCE_ID,
|
||||||
|
filter: ['==', ['get', 'icao'], ''],
|
||||||
|
paint: {
|
||||||
|
'circle-radius': 9, // same ring size as airports-highlight (r=5 base circles)
|
||||||
|
'circle-color': 'rgba(0, 0, 0, 0)', // transparent fill — stroke-only ring
|
||||||
|
'circle-stroke-width': 3,
|
||||||
|
'circle-stroke-color': '#4ade80', // green — distinct from the yellow endpoint rings
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
// Layer-scoped listeners survive setStyle (they filter by layer id at event
|
// Layer-scoped listeners survive setStyle (they filter by layer id at event
|
||||||
// time), so off+on with the same reference re-wires without duplicating.
|
// time), so off+on with the same reference re-wires without duplicating.
|
||||||
map.off('click', AIRPORTS_LAYER_ID, onAirportClick);
|
map.off('click', AIRPORTS_LAYER_ID, onAirportClick);
|
||||||
|
|||||||
+25
-1
@@ -8,7 +8,8 @@ import { test, expect, type Page } from '@playwright/test';
|
|||||||
// queryRenderedFeatures, same pattern as e2e/map.spec.ts and e2e/map-interactions.spec.ts.
|
// queryRenderedFeatures, same pattern as e2e/map.spec.ts and e2e/map-interactions.spec.ts.
|
||||||
// ISS-022/025/027 (v0.22.0): any hover carrying two endpoints ALSO draws the full great-circle
|
// ISS-022/025/027 (v0.22.0): any hover carrying two endpoints ALSO draws the full great-circle
|
||||||
// trace between them in a fourth top-most layer (`hover-route`, dashed) — job-list rows, on-board
|
// trace between them in a fourth top-most layer (`hover-route`, dashed) — job-list rows, on-board
|
||||||
// cargo rows and the Relocate section; the relocate preview rings only its TARGET airport.
|
// cargo rows and the Relocate section; the relocate preview rings only its TARGET airport, which
|
||||||
|
// additionally gets a distinct green ring in `airports-target-highlight` (ISS-022).
|
||||||
|
|
||||||
const SCALE = 1000; // fastest sim speed (v0.14.0 ladder top) — keeps flights to seconds of real time
|
const SCALE = 1000; // fastest sim speed (v0.14.0 ladder top) — keeps flights to seconds of real time
|
||||||
const HUB = 'EDDF'; // must match HUB_ICAO in lib/game/seed.ts
|
const HUB = 'EDDF'; // must match HUB_ICAO in lib/game/seed.ts
|
||||||
@@ -262,6 +263,7 @@ test('hovering an open package highlights its endpoint airports', async ({ page
|
|||||||
await fitToAirports(page, [HUB, DEST]);
|
await fitToAirports(page, [HUB, DEST]);
|
||||||
|
|
||||||
expect(await renderedFeatures(page, ['airports-highlight'])).toHaveLength(0);
|
expect(await renderedFeatures(page, ['airports-highlight'])).toHaveLength(0);
|
||||||
|
expect(await renderedFeatures(page, ['airports-target-highlight'])).toHaveLength(0);
|
||||||
|
|
||||||
await row.hover();
|
await row.hover();
|
||||||
await expect.poll(async () => {
|
await expect.poll(async () => {
|
||||||
@@ -274,6 +276,13 @@ test('hovering an open package highlights its endpoint airports', async ({ page
|
|||||||
// not just the endpoint rings.
|
// not just the endpoint rings.
|
||||||
await expectHoverRoute(page, HUB, DEST);
|
await expectHoverRoute(page, HUB, DEST);
|
||||||
|
|
||||||
|
// And the TARGET airport gets its own distinct-colored ring — only the destination, never the origin.
|
||||||
|
await expect.poll(async () => {
|
||||||
|
const feats = (await renderedFeatures(page, ['airports-target-highlight'])) as Array<{ properties: Record<string, unknown> }>;
|
||||||
|
return JSON.stringify(feats.map((f) => f.properties.icao).sort());
|
||||||
|
}, { timeout: 15_000, message: 'waiting for the airports-target-highlight layer to ring only the destination' })
|
||||||
|
.toBe(JSON.stringify([DEST]));
|
||||||
|
|
||||||
await unhover(page);
|
await unhover(page);
|
||||||
await expect.poll(async () => (await renderedFeatures(page, ['airports-highlight'])).length, {
|
await expect.poll(async () => (await renderedFeatures(page, ['airports-highlight'])).length, {
|
||||||
timeout: 15_000,
|
timeout: 15_000,
|
||||||
@@ -283,6 +292,10 @@ test('hovering an open package highlights its endpoint airports', async ({ page
|
|||||||
timeout: 15_000,
|
timeout: 15_000,
|
||||||
message: 'waiting for the hover route trace to clear after mouseleave',
|
message: 'waiting for the hover route trace to clear after mouseleave',
|
||||||
}).toBe(0);
|
}).toBe(0);
|
||||||
|
await expect.poll(async () => (await renderedFeatures(page, ['airports-target-highlight'])).length, {
|
||||||
|
timeout: 15_000,
|
||||||
|
message: 'waiting for the target ring to clear after mouseleave',
|
||||||
|
}).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('hovering an on-board cargo row draws its trace and keeps the plane halo', async ({ page }) => {
|
test('hovering an on-board cargo row draws its trace and keeps the plane halo', async ({ page }) => {
|
||||||
@@ -350,6 +363,7 @@ test('hovering the Relocate section previews the planned route and target airpor
|
|||||||
|
|
||||||
expect(await renderedFeatures(page, ['hover-route'])).toHaveLength(0);
|
expect(await renderedFeatures(page, ['hover-route'])).toHaveLength(0);
|
||||||
expect(await renderedFeatures(page, ['airports-highlight'])).toHaveLength(0);
|
expect(await renderedFeatures(page, ['airports-highlight'])).toHaveLength(0);
|
||||||
|
expect(await renderedFeatures(page, ['airports-target-highlight'])).toHaveLength(0);
|
||||||
|
|
||||||
// ISS-027 (v0.22.0): hovering the panel with a plane + destination selected previews the flight —
|
// ISS-027 (v0.22.0): hovering the panel with a plane + destination selected previews the flight —
|
||||||
// dashed trace from the plane's airport to the destination…
|
// dashed trace from the plane's airport to the destination…
|
||||||
@@ -361,6 +375,12 @@ test('hovering the Relocate section previews the planned route and target airpor
|
|||||||
return JSON.stringify(feats.map((f) => f.properties.icao).sort());
|
return JSON.stringify(feats.map((f) => f.properties.icao).sort());
|
||||||
}, { timeout: 15_000, message: 'waiting for the airports-highlight layer to ring only the target' })
|
}, { timeout: 15_000, message: 'waiting for the airports-highlight layer to ring only the target' })
|
||||||
.toBe(JSON.stringify([DEST]));
|
.toBe(JSON.stringify([DEST]));
|
||||||
|
// …plus the destination's own distinct-colored ring (ISS-022) — same single icao.
|
||||||
|
await expect.poll(async () => {
|
||||||
|
const feats = (await renderedFeatures(page, ['airports-target-highlight'])) as Array<{ properties: Record<string, unknown> }>;
|
||||||
|
return JSON.stringify(feats.map((f) => f.properties.icao).sort());
|
||||||
|
}, { timeout: 15_000, message: 'waiting for the airports-target-highlight layer to ring only the target' })
|
||||||
|
.toBe(JSON.stringify([DEST]));
|
||||||
|
|
||||||
await unhover(page);
|
await unhover(page);
|
||||||
await expect.poll(async () => (await renderedFeatures(page, ['hover-route'])).length, {
|
await expect.poll(async () => (await renderedFeatures(page, ['hover-route'])).length, {
|
||||||
@@ -371,6 +391,10 @@ test('hovering the Relocate section previews the planned route and target airpor
|
|||||||
timeout: 15_000,
|
timeout: 15_000,
|
||||||
message: 'waiting for the highlight to clear after mouseleave',
|
message: 'waiting for the highlight to clear after mouseleave',
|
||||||
}).toBe(0);
|
}).toBe(0);
|
||||||
|
await expect.poll(async () => (await renderedFeatures(page, ['airports-target-highlight'])).length, {
|
||||||
|
timeout: 15_000,
|
||||||
|
message: 'waiting for the target ring to clear after mouseleave',
|
||||||
|
}).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('hovering an on-board block of a flying plane highlights its route', async ({ page }) => {
|
test('hovering an on-board block of a flying plane highlights its route', async ({ page }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user