v0.15.0 — ISS-002 hovering a plane or package in the side panels highlights it on the map #15
@@ -5,6 +5,7 @@
|
||||
- **Three top-most highlight layers** (`components/Map.tsx`, added inside idempotent `ensureLayers()` so they survive every `setStyle`): `planes-highlight` (circle over both plane layers, filter `['==',['get','id'],<planeId>]`, r=10, #38bdf8 fill + white stroke), `routes-highlight` (line over `routes-legs`, filter on the leg's `planeId`, width 6, #ef4444) and `airports-highlight` (circle ring over `airports-circles`, filter matching the hovered package's two ICAOs via `['any', …]`, r=8 stroke-only). All start filtered to match nothing — an empty-string sentinel no feature can have. Consumption follows the existing ref + effect pattern: a `[hovered]` effect calls `map.setFilter(...)` on the three layers (guarded by `getLayer`), and `ensureLayers` re-applies the current filter at its end so a hover outliving a style switch stays highlighted
|
||||
- e2e — new `e2e/hover.spec.ts` (3 tests, same SCALE/self-heal/`window.__flightMap` conventions as the other specs): fleet-row hover → `planes-highlight` renders exactly plane-starter, clears on mouseleave; open-package hover → `airports-highlight` shows both endpoint ICAOs (hook-created EDDF→LEMD package), clears on mouseleave; on-board block of a flying plane → `routes-highlight` shows the leg's planeId (sim paused mid-flight so the ~10 s leg can't land under the assertions, then resumed + fly-home cleanup)
|
||||
- Verified: `tsc --noEmit` clean; unit suite **238/238** green locally (no lib changes); e2e not run locally by design — CI verifies against an isolated stack
|
||||
- **E2e hover spec mechanics fixed after CI run #32** (`e2e/hover.spec.ts`, no app changes): the fleet-row wait now polls up to 30 s with a diagnostic that distinguishes "server has no plane-starter" from "DOM didn't render" (the browser's /api/state poll can lag node-side requests right after page load, leaving the pane in its loading state); the open-package test fits the map to both endpoint airports before asserting — `queryRenderedFeatures` only returns features inside the current viewport and the default Frankfurt-centered view leaves LEMD off-screen
|
||||
|
||||
## v0.14.0 — 2026-09-19 08:45
|
||||
- **Time-scale slider reworked to a 10-step ladder up to 1000× (ISS-006).** The old power-of-two ladder `1/2/4/8/16/32/64` is replaced by the discrete values `1, 2, 5, 10, 25, 50, 100, 250, 500, 1000` (each step roughly doubles the speed). The server clamp range widens from `[1, 64]` to `[1, 1000]` (`MAX_TIME_SCALE` in `lib/sim/engine.ts`, enforced by `clampTimeScale`; out-of-range values are still clamped, never rejected), and the OpenAPI spec's `timeScale` bounds + description follow. The slider mapping helpers in `lib/timeScale.ts` now place off-ladder scales at the nearest ladder step **by log distance** (the old `log2` formula assumed a pure power-of-two ladder); round-trip, monotonicity and endpoint-exactness are preserved
|
||||
|
||||
+48
-1
@@ -126,6 +126,36 @@ async function unhover(page: Page): Promise<void> {
|
||||
await page.getByTestId('wallet-cash').hover();
|
||||
}
|
||||
|
||||
// Fit the map so all given airports are in-viewport — queryRenderedFeatures only returns
|
||||
// features rendered inside the CURRENT viewport, and the default Frankfurt-centered view leaves
|
||||
// far endpoints (LEMD) off-screen even when they match a layer filter.
|
||||
async function fitToAirports(page: Page, icaos: string[]): Promise<void> {
|
||||
const fc = (await (await page.request.get('/api/airports')).json()) as {
|
||||
features: Array<{ properties: { icao: string; lat: number; lon: number } }>;
|
||||
};
|
||||
const points = icaos.map((icao) => {
|
||||
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] as [number, number];
|
||||
});
|
||||
await page.evaluate((pts: [number, number][]) => {
|
||||
const m = (window as any).__flightMap;
|
||||
if (!m) throw new Error('map not ready');
|
||||
const lons = pts.map((p) => p[0]);
|
||||
const lats = pts.map((p) => p[1]);
|
||||
// duration 0 → instant camera jump, no animation to wait out.
|
||||
m.fitBounds(
|
||||
[
|
||||
[Math.min(...lons), Math.min(...lats)],
|
||||
[Math.max(...lons), Math.max(...lats)],
|
||||
],
|
||||
{ padding: 40, duration: 0 },
|
||||
);
|
||||
}, points);
|
||||
// Give the renderer a beat after the camera jump before asserting on rendered features.
|
||||
await page.waitForTimeout(300);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -134,7 +164,20 @@ test('hovering a fleet row highlights the plane on the map', async ({ page }) =>
|
||||
await waitForAirportCount(page);
|
||||
|
||||
const row = page.locator('li[data-testid="plane-starter"]');
|
||||
await expect(row).toBeVisible({ timeout: 15_000 });
|
||||
// The browser's /api/state polls can lag node-side requests right after a fresh page load
|
||||
// (useGameState keeps loading=true until the first success → no fleet rows yet), so poll with
|
||||
// diagnostics: on timeout the message distinguishes "server has no plane-starter" from
|
||||
// "DOM didn't render". (The value is returned, not thrown — expect.poll fails immediately
|
||||
// on a callback throw instead of retrying.)
|
||||
await expect
|
||||
.poll(async () => {
|
||||
if ((await row.count()) > 0 && (await row.isVisible())) return 'visible';
|
||||
const body = await getState(page);
|
||||
const serverHasStarter = body.planes.some((p) => p.id === 'plane-starter');
|
||||
const paneHtml = (await page.locator('[data-testid="left-pane"]').first().innerHTML()).slice(0, 200);
|
||||
return `not visible — /api/state has plane-starter: ${serverHasStarter}; left-pane HTML: ${paneHtml}`;
|
||||
}, { timeout: 30_000 })
|
||||
.toBe('visible');
|
||||
|
||||
// No highlight before hovering (the layers start filtered to match nothing).
|
||||
expect(await renderedFeatures(page, ['planes-highlight'])).toHaveLength(0);
|
||||
@@ -164,6 +207,10 @@ test('hovering an open package highlights its endpoint airports', async ({ page
|
||||
const row = page.locator(`li[data-testid="open-package-${pkg.id}"]`);
|
||||
await expect(row, 'the hook-created package appears in the list').toBeVisible({ timeout: 15_000 });
|
||||
|
||||
// Both endpoints must be in-viewport for queryRenderedFeatures to see them (LEMD is off-screen
|
||||
// at the default Frankfurt-centered view).
|
||||
await fitToAirports(page, [HUB, DEST]);
|
||||
|
||||
expect(await renderedFeatures(page, ['airports-highlight'])).toHaveLength(0);
|
||||
|
||||
await row.hover();
|
||||
|
||||
Reference in New Issue
Block a user