Queue UI fixes: disable would-409 controls, hover trace from landing airport, grounded queues draw dotted lines (ISS-077, v0.41.0)
CI / build + unit + e2e (pull_request) Successful in 9m11s
CI / build + unit + e2e (pull_request) Successful in 9m11s
This commit is contained in:
+41
-32
@@ -15,6 +15,8 @@ const SCALE = 1000; // fastest sim speed (v0.14.0 ladder top) — keeps flights
|
||||
const HUB = 'EDDF'; // must match HUB_ICAO in lib/game/seed.ts
|
||||
// Madrid–Barajas — same long leg as e2e/map-interactions.spec.ts: ~1420 km → ~10 s at scale 1000.
|
||||
const DEST = 'LEMD';
|
||||
// Amsterdam — a third airport, distinct from hub and destination (the queued stop in the ISS-077 test).
|
||||
const QUEUE = 'EHAM';
|
||||
|
||||
interface PlaneState {
|
||||
id: string;
|
||||
@@ -500,7 +502,7 @@ test('hovering the fleet row of a flying plane highlights its route', async ({ p
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test('hovering an on-board package while its carrier is mid-flight traces from current position', async ({ page }) => {
|
||||
test('hovering an on-board package while its carrier is mid-flight traces from the landing airport', async ({ page }) => {
|
||||
test.setTimeout(600_000); // self-heal + one flight at scale 1000 + fly-home cleanup
|
||||
await flyHomeViaApi(page);
|
||||
|
||||
@@ -511,44 +513,42 @@ test('hovering an on-board package while its carrier is mid-flight traces from c
|
||||
await page.goto('/');
|
||||
await waitForAirportCount(page);
|
||||
|
||||
// Load + fly via the API (same pattern as e2e/map-interactions.spec.ts), then speed up.
|
||||
const pkg = await createPackage(page, HUB, DEST);
|
||||
// ISS-077 (v0.41.0): the package is destined for the QUEUED stop (EHAM), not the active leg's
|
||||
// destination — load it, queue EHAM while grounded (pre-built routes are allowed), then start the
|
||||
// HUB→DEST leg via the API (same pattern as e2e/map-interactions.spec.ts). The hover must trace
|
||||
// from DEST (where this flight lands) to EHAM — NOT from the plane's live mid-flight position.
|
||||
const pkg = await createPackage(page, HUB, QUEUE);
|
||||
const load = await page.request.post(`/api/packages/${pkg.id}/load`, { data: { planeId: 'plane-starter' } });
|
||||
expect(load.status()).toBe(200);
|
||||
const queueRes = await page.request.post('/api/planes/plane-starter/queue', { data: { destIcao: QUEUE } });
|
||||
expect(queueRes.status()).toBe(200);
|
||||
const fly = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: DEST } });
|
||||
expect(fly.status()).toBe(200);
|
||||
await setScale(page, SCALE);
|
||||
// The endpoint's OWN post-action snapshot already shows the plane flying — no poll race.
|
||||
const flyBody = (await fly.json()) as StateBody;
|
||||
expect(flyBody.planes.find((p) => p.id === 'plane-starter')?.status).toBe('flying');
|
||||
|
||||
// Wait until the starter is GENUINELY mid-flight — its live position clearly away from EDDF in
|
||||
// both axes (well past HOVER_ROUTE_EPS_DEG) so "the trace starts at the plane" and "it starts at
|
||||
// the pickup airport" are distinguishable. The ~10 s leg at scale 1000 gives a wide window.
|
||||
const hubLonLat = await airportLonLat(page, HUB);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const p = (await getState(page)).planes.find((x) => x.id === 'plane-starter');
|
||||
if (!p || p.status !== 'flying') return false;
|
||||
return Math.abs(p.lon - hubLonLat[0]) > 0.2 && Math.abs(p.lat - hubLonLat[1]) > 0.2;
|
||||
}, { timeout: 60_000, message: 'waiting for plane-starter to be clearly away from the hub' })
|
||||
.toBe(true);
|
||||
|
||||
// Freeze mid-flight so the position is stable while we assert (same pause pattern as the
|
||||
// flying-plane hover test above): paused, every /api/state snapshot reports the same lat/lon.
|
||||
// Freeze immediately: at a leftover scale of 1000 the ~10 s leg could otherwise land under us and
|
||||
// auto-chain the queued EHAM leg, which would change (or empty) the trace while we assert. Paused,
|
||||
// every /api/state snapshot reports the same state — and the new rule's start point is the LANDING
|
||||
// airport regardless of progress, so no mid-flight position window is needed at all.
|
||||
await postSim(page, { paused: true });
|
||||
|
||||
const row = page.locator(`[data-testid="onboard-package-${pkg.id}"]`);
|
||||
await expect(row, 'the loaded package appears in the On board section').toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Both endpoints must be in-viewport for queryRenderedFeatures to see the trace.
|
||||
await fitToAirports(page, [HUB, DEST]);
|
||||
// Both trace endpoints must be in-viewport for queryRenderedFeatures to see them.
|
||||
await fitToAirports(page, [DEST, QUEUE]);
|
||||
|
||||
const live = (await getState(page)).planes.find((x) => x.id === 'plane-starter')!;
|
||||
const liveLonLat: [number, number] = [live.lon, live.lat]; // stable — the sim is paused
|
||||
const destLonLat = await airportLonLat(page, DEST);
|
||||
const queueLonLat = await airportLonLat(page, QUEUE);
|
||||
const hubLonLat = await airportLonLat(page, HUB);
|
||||
|
||||
await row.hover();
|
||||
// ISS-040 (v0.23.2): the trace starts at the carrier's LIVE position and ends at the destination,
|
||||
// and NO segment may start near the pickup airport (the old origin-based behavior). Same per-tile
|
||||
// clipping/quantization caveats as expectHoverRoute — check endpoints across all segments with EPS.
|
||||
// ISS-077 (v0.41.0): the trace starts at the LANDING airport (DEST) and ends at the package's
|
||||
// destination (the queued stop EHAM), and NO segment may start near the departure airport — that
|
||||
// was the old live-position behavior right after takeoff. Same per-tile clipping/quantization
|
||||
// caveats as expectHoverRoute — check endpoints across all segments with EPS.
|
||||
await expect.poll(async () => {
|
||||
const feats = (await renderedFeatures(page, ['hover-route'])) as Array<{ geometry?: { type: string; coordinates: [number, number][] } }>;
|
||||
const segs = feats.filter(
|
||||
@@ -558,11 +558,14 @@ test('hovering an on-board package while its carrier is mid-flight traces from c
|
||||
if (!segs.length) return false;
|
||||
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;
|
||||
const startsAtLive = segs.some((f) => near(f.geometry.coordinates[0], liveLonLat));
|
||||
const endsAtDest = segs.some((f) => near(f.geometry.coordinates[f.geometry.coordinates.length - 1], destLonLat));
|
||||
const startsAtDest = segs.some((f) => near(f.geometry.coordinates[0], destLonLat));
|
||||
const endsAtQueue = segs.some((f) => {
|
||||
const c = f.geometry.coordinates;
|
||||
return near(c[c.length - 1], queueLonLat);
|
||||
});
|
||||
const noneStartsAtHub = !segs.some((f) => near(f.geometry.coordinates[0], hubLonLat));
|
||||
return startsAtLive && endsAtDest && noneStartsAtHub;
|
||||
}, { timeout: 15_000, message: 'waiting for the hover-route trace to run from the live position to the destination' }).toBe(true);
|
||||
return startsAtDest && endsAtQueue && noneStartsAtHub;
|
||||
}, { timeout: 15_000, message: 'waiting for the hover-route trace to run from the landing airport (DEST) to the queued stop' }).toBe(true);
|
||||
|
||||
await unhover(page);
|
||||
await expect.poll(async () => (await renderedFeatures(page, ['hover-route'])).length, {
|
||||
@@ -570,9 +573,12 @@ test('hovering an on-board package while its carrier is mid-flight traces from c
|
||||
message: 'waiting for the hover route trace to clear after mouseleave',
|
||||
}).toBe(0);
|
||||
|
||||
// Resume and fast-forward to arrival — landing at DEST auto-delivers the package (v0.12.0), so
|
||||
// flying home afterwards leaves the starter clean for the other specs.
|
||||
await postSim(page, { timeScale: SCALE, paused: false });
|
||||
// Cleanup: cancel the queued leg first so landing at DEST is a plain grounding (no auto-chain),
|
||||
// resume + fast-forward to arrival, abandon the still-loaded package (it was destined for EHAM,
|
||||
// not DEST — it never delivered), then fly home.
|
||||
const cancelRes = await page.request.post('/api/planes/plane-starter/queue/cancel', { data: { index: 0 } });
|
||||
expect(cancelRes.status()).toBe(200);
|
||||
await setScale(page, SCALE); // also clears the pause above
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const p = (await getState(page)).planes.find((x) => x.id === 'plane-starter');
|
||||
@@ -580,6 +586,9 @@ test('hovering an on-board package while its carrier is mid-flight traces from c
|
||||
}, { timeout: 120_000, message: `waiting for plane-starter to land at ${DEST}` })
|
||||
.toBe(true);
|
||||
|
||||
const abandon = await page.request.post(`/api/packages/${pkg.id}/abandon`);
|
||||
expect(abandon.status()).toBe(200); // only possible once grounded — the poll above guarantees it
|
||||
|
||||
// ISS-041: the arrival above paused the sim, so re-set SCALE for this leg (setScale clears pause).
|
||||
await setScale(page, SCALE);
|
||||
const home = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: HUB } });
|
||||
|
||||
@@ -350,6 +350,64 @@ test('dotted queue line renders the queued path on the map (ISS-043 part 2)', as
|
||||
}).toBe(HUB);
|
||||
});
|
||||
|
||||
test('grounded plane with a pre-built queue draws its dotted route from its airport (ISS-077)', async ({ page }) => {
|
||||
test.setTimeout(300_000); // self-heal + map assertions only — no flight in this test
|
||||
await flyHomeViaApi(page); // deterministic: the starter sits grounded at EDDF
|
||||
|
||||
await page.goto('/');
|
||||
await waitForAirportCount(page);
|
||||
|
||||
// Pre-build a two-leg route while GROUNDED via the raw API (queuing is allowed from any state,
|
||||
// ISS-043 part 1) — no package or flight needed. The dotted line must chain HUB → DEST → QUEUE.
|
||||
const q1 = await page.request.post('/api/planes/plane-starter/queue', { data: { destIcao: DEST } });
|
||||
expect(q1.status()).toBe(200);
|
||||
const q2 = await page.request.post('/api/planes/plane-starter/queue', { data: { destIcao: QUEUE } });
|
||||
expect(q2.status()).toBe(200);
|
||||
|
||||
// All chain endpoints must be in-viewport for queryRenderedFeatures to see them.
|
||||
await fitToAirports(page, [HUB, DEST, QUEUE]);
|
||||
|
||||
const hubLL = await airportLonLat(page, HUB);
|
||||
const destLL = await airportLonLat(page, DEST);
|
||||
const queueLL = await airportLonLat(page, QUEUE);
|
||||
const near = (a: [number, number], b: [number, number]) =>
|
||||
Math.abs(a[0] - b[0]) <= QUEUE_ROUTE_EPS_DEG && Math.abs(a[1] - b[1]) <= QUEUE_ROUTE_EPS_DEG;
|
||||
|
||||
await expect.poll(async () => {
|
||||
const feats = (await renderedFeatures(page, ['queue-routes'])) as Array<{
|
||||
properties?: Record<string, unknown>;
|
||||
geometry?: { type: string; coordinates: [number, number][] };
|
||||
}>;
|
||||
// Filter to real LineString segments — keeps this poll callback total. "None yet" → keep polling.
|
||||
const segs = feats.filter(
|
||||
(f): f is { properties?: Record<string, unknown>; geometry: { type: 'LineString'; coordinates: [number, number][] } } =>
|
||||
!!f.geometry && f.geometry.type === 'LineString' && Array.isArray(f.geometry.coordinates) && f.geometry.coordinates.length >= 2,
|
||||
);
|
||||
if (!segs.length) return false;
|
||||
// The chain HUB → DEST → QUEUE may be clipped into per-tile segments — check across all of them:
|
||||
// some segment STARTS within EPS of the hub (the plane's airport, NOT a flight destination)…
|
||||
const startsAtHub = segs.some((f) => near(f.geometry.coordinates[0], hubLL));
|
||||
// …some ENDS within EPS of the last queued airport…
|
||||
const endsAtQueue = segs.some((f) => {
|
||||
const c = f.geometry.coordinates;
|
||||
return near(c[c.length - 1], queueLL);
|
||||
});
|
||||
// …and the chain passes THROUGH the first queued airport — an interior joint point survives in
|
||||
// whichever tile contains it (not guaranteed to be a segment endpoint after clipping).
|
||||
const passesThroughDest = segs.some((f) => f.geometry.coordinates.some((c) => near(c, destLL)));
|
||||
// Every rendered feature belongs to plane-starter.
|
||||
const ownedByStarter = feats.every((f) => f.properties?.planeId === 'plane-starter');
|
||||
return startsAtHub && endsAtQueue && passesThroughDest && ownedByStarter;
|
||||
}, { timeout: 15_000, message: `waiting for the dotted queue line ${HUB} → ${DEST} → ${QUEUE}` }).toBe(true);
|
||||
|
||||
// Cleanup: cancel both queued legs (index 0 twice — each cancel shifts the rest down) so the
|
||||
// starter stays grounded at HUB with an empty queue. No flight happened, nothing else to reset.
|
||||
const c1 = await page.request.post('/api/planes/plane-starter/queue/cancel', { data: { index: 0 } });
|
||||
expect(c1.status()).toBe(200);
|
||||
const c2 = await page.request.post('/api/planes/plane-starter/queue/cancel', { data: { index: 0 } });
|
||||
expect(c2.status()).toBe(200);
|
||||
});
|
||||
|
||||
test('landing auto-starts the next queued leg (ISS-043 chain end-to-end)', async ({ page }) => {
|
||||
test.setTimeout(300_000); // self-heal + two chained legs at scale 1000
|
||||
await flyHomeViaApi(page);
|
||||
|
||||
Reference in New Issue
Block a user