diff --git a/CHANGELOG.md b/CHANGELOG.md index 60d1f9f..fc8fe22 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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'],]`, 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 diff --git a/e2e/hover.spec.ts b/e2e/hover.spec.ts index f626f5b..a5dcb68 100644 --- a/e2e/hover.spec.ts +++ b/e2e/hover.spec.ts @@ -126,6 +126,36 @@ async function unhover(page: Page): Promise { 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 { + 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();