feat: v0.22.0 — ISS-022/025/027 hover route trace on job list, on-board rows and relocation panel
This commit is contained in:
@@ -6,6 +6,9 @@ import { test, expect, type Page } from '@playwright/test';
|
||||
// highlight layers (planes-highlight / routes-highlight / airports-highlight), each filtered
|
||||
// to match nothing until a hover sets the filter — asserted here via window.__flightMap's
|
||||
// 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
|
||||
// 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.
|
||||
|
||||
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
|
||||
@@ -160,6 +163,35 @@ async function fitToAirports(page: Page, icaos: string[]): Promise<void> {
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
// [lon, lat] of an airport from the same /api/airports dataset the map draws its traces from.
|
||||
async function airportLonLat(page: Page, icao: string): Promise<[number, number]> {
|
||||
const fc = (await (await page.request.get('/api/airports')).json()) as {
|
||||
features: Array<{ properties: { icao: string; lat: number; lon: number } }>;
|
||||
};
|
||||
const f = fc.features.find((x) => x.properties.icao === icao);
|
||||
if (!f) throw new Error(`airport ${icao} not found in /api/airports`);
|
||||
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.
|
||||
async function expectHoverRoute(page: Page, fromIcao: string, toIcao: string): Promise<void> {
|
||||
const [from, to] = await Promise.all([airportLonLat(page, fromIcao), airportLonLat(page, toIcao)]);
|
||||
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]
|
||||
);
|
||||
}, { timeout: 15_000, message: `waiting for the hover-route line ${fromIcao} → ${toIcao}` }).toBe(true);
|
||||
}
|
||||
|
||||
test('hovering a fleet row highlights the plane on the map', async ({ page }) => {
|
||||
test.setTimeout(300_000); // self-heal can take minutes after an interrupted run
|
||||
await flyHomeViaApi(page); // deterministic: the starter sits at EDDF, inside the initial view
|
||||
@@ -238,11 +270,107 @@ test('hovering an open package highlights its endpoint airports', async ({ page
|
||||
}, { timeout: 15_000, message: 'waiting for the airports-highlight layer to show both endpoints' })
|
||||
.toBe(JSON.stringify([HUB, DEST].sort()));
|
||||
|
||||
// ISS-022 (v0.22.0): the ENTIRE trace is drawn too — a great-circle line from origin to dest,
|
||||
// not just the endpoint rings.
|
||||
await expectHoverRoute(page, HUB, DEST);
|
||||
|
||||
await unhover(page);
|
||||
await expect.poll(async () => (await renderedFeatures(page, ['airports-highlight'])).length, {
|
||||
timeout: 15_000,
|
||||
message: 'waiting for the highlight to clear after mouseleave',
|
||||
}).toBe(0);
|
||||
await expect.poll(async () => (await renderedFeatures(page, ['hover-route'])).length, {
|
||||
timeout: 15_000,
|
||||
message: 'waiting for the hover route trace to clear after mouseleave',
|
||||
}).toBe(0);
|
||||
});
|
||||
|
||||
test('hovering an on-board cargo row draws its trace and keeps the plane halo', async ({ page }) => {
|
||||
test.setTimeout(300_000); // self-heal can take minutes after an interrupted run
|
||||
await flyHomeViaApi(page); // deterministic: the starter sits grounded at EDDF
|
||||
|
||||
// Load a package onto the grounded starter so it shows up in the On board section.
|
||||
const pkg = await createPackage(page, HUB, DEST);
|
||||
const load = await page.request.post(`/api/packages/${pkg.id}/load`, { data: { planeId: 'plane-starter' } });
|
||||
expect(load.status()).toBe(200);
|
||||
|
||||
await page.goto('/');
|
||||
await waitForAirportCount(page);
|
||||
|
||||
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]);
|
||||
|
||||
await row.hover();
|
||||
// ISS-025 (v0.22.0): same full origin→dest trace as job-list rows…
|
||||
await expectHoverRoute(page, HUB, DEST);
|
||||
// …and the package's plane keeps its halo lit (the hover carries planeId).
|
||||
await expect.poll(async () => {
|
||||
const feats = (await renderedFeatures(page, ['planes-highlight'])) as Array<{ properties: Record<string, unknown> }>;
|
||||
return feats.length > 0 && feats[0].properties.id === 'plane-starter';
|
||||
}, { timeout: 15_000, message: 'waiting for the planes-highlight layer to show plane-starter' }).toBe(true);
|
||||
|
||||
await unhover(page);
|
||||
await expect.poll(async () => (await renderedFeatures(page, ['hover-route'])).length, {
|
||||
timeout: 15_000,
|
||||
message: 'waiting for the hover route trace to clear after mouseleave',
|
||||
}).toBe(0);
|
||||
await expect.poll(async () => (await renderedFeatures(page, ['planes-highlight'])).length, {
|
||||
timeout: 15_000,
|
||||
message: 'waiting for the highlight to clear after mouseleave',
|
||||
}).toBe(0);
|
||||
|
||||
// Leave the instance clean: abandon the loaded package so later specs start with an empty starter.
|
||||
const res = await page.request.post(`/api/packages/${pkg.id}/abandon`);
|
||||
expect(res.status()).toBe(200);
|
||||
});
|
||||
|
||||
test('hovering the Relocate section previews the planned route and target airport', async ({ page }) => {
|
||||
test.setTimeout(300_000); // self-heal can take minutes after an interrupted run
|
||||
await flyHomeViaApi(page); // deterministic: the starter sits grounded at EDDF
|
||||
|
||||
await page.goto('/');
|
||||
await waitForAirportCount(page);
|
||||
|
||||
const section = page.getByTestId('relocate-section');
|
||||
await expect(section, 'the Relocate section is visible').toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// LeftPane fetches /api/airports on its own (separate from the map header count) — wait for
|
||||
// the destination options to populate before selecting.
|
||||
await expect(page.getByTestId('relocate-dest').locator(`option[value="${DEST}"]`)).toHaveCount(1, { timeout: 15_000 });
|
||||
|
||||
// Pick the starter + a known destination explicitly (the effective-selection defaults already do this).
|
||||
await page.getByTestId('relocate-plane').selectOption('plane-starter');
|
||||
await page.getByTestId('relocate-dest').selectOption(DEST);
|
||||
|
||||
// Both endpoints must be in-viewport for queryRenderedFeatures to see the trace.
|
||||
await fitToAirports(page, [HUB, DEST]);
|
||||
|
||||
expect(await renderedFeatures(page, ['hover-route'])).toHaveLength(0);
|
||||
expect(await renderedFeatures(page, ['airports-highlight'])).toHaveLength(0);
|
||||
|
||||
// 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…
|
||||
await section.hover();
|
||||
await expectHoverRoute(page, HUB, DEST);
|
||||
// …and rings ONLY the target airport (the origin stays unmarked).
|
||||
await expect.poll(async () => {
|
||||
const feats = (await renderedFeatures(page, ['airports-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-highlight layer to ring only the target' })
|
||||
.toBe(JSON.stringify([DEST]));
|
||||
|
||||
await unhover(page);
|
||||
await expect.poll(async () => (await renderedFeatures(page, ['hover-route'])).length, {
|
||||
timeout: 15_000,
|
||||
message: 'waiting for the hover route trace to clear after mouseleave',
|
||||
}).toBe(0);
|
||||
await expect.poll(async () => (await renderedFeatures(page, ['airports-highlight'])).length, {
|
||||
timeout: 15_000,
|
||||
message: 'waiting for the highlight to clear after mouseleave',
|
||||
}).toBe(0);
|
||||
});
|
||||
|
||||
test('hovering an on-board block of a flying plane highlights its route', async ({ page }) => {
|
||||
|
||||
Reference in New Issue
Block a user