Files
Flight-Ops/e2e/auto-slow.spec.ts
T
2026-09-19 18:07:42 +00:00

131 lines
6.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { Page } from '@playwright/test';
import { expect, test } from '@playwright/test';
// v0.19.0 — ISS-011: when the LAST plane lands (no plane remains in flight) the sim auto-slows
// to 1×, server-side and persisted on the sim_state row so the UI slider reflects it on the next
// snapshot. Arrival-triggered by design: a grounded fleet at high scale is left alone, there is no
// auto-restore — the player speeds up manually again (asserted below).
const SCALE = 1000; // fastest sim speed — top of the v0.14.0 ladder (same convention as e2e/sim.spec.ts)
const HUB = 'EDDF'; // Frankfurt Main — the seeded starter plane's home base (lib/game/seed.ts)
interface SimState {
clockMs: number;
timeScale: number;
paused: boolean;
}
interface StateBody {
simState: SimState;
planes: Array<{ id: string; status: string; locationIcao: string; destIcao: string | null }>;
}
async function getState(page: Page): Promise<StateBody> {
const res = await page.request.get('/api/state');
expect(res.ok()).toBeTruthy();
return (await res.json()) as StateBody;
}
async function postSim(page: Page, body: Record<string, unknown>): Promise<void> {
const res = await page.request.post('/api/sim', { data: body });
expect(res.status()).toBe(200);
}
// Set the time scale and VERIFY from the response that it stuck — if something external ever
// resets the live instance's controls mid-test, fail loudly here instead of timing out later.
async function setScale(page: Page, scale: number): Promise<void> {
const res = await page.request.post('/api/sim', { data: { timeScale: scale } });
expect(res.status()).toBe(200);
const body = (await res.json()) as SimState;
expect(body.timeScale, `timeScale should be ${scale} after POST /api/sim`).toBe(scale);
}
// Closest airport to the hub from /api/airports → shortest possible leg (fastest e2e flight).
async function nearestToHub(page: Page): Promise<{ icao: string }> {
const res = await page.request.get('/api/airports');
expect(res.ok()).toBeTruthy();
const fc = (await res.json()) as { features: Array<{ properties: { icao: string; lat: number; lon: number } }> };
const hub = fc.features.find((f) => f.properties.icao === HUB)!.properties;
let nearest = fc.features.filter((f) => f.properties.icao !== HUB)[0].properties;
for (const f of fc.features) {
if (f.properties.icao === HUB) continue;
const d = Math.hypot(f.properties.lat - hub.lat, f.properties.lon - hub.lon);
const dn = Math.hypot(nearest.lat - hub.lat, nearest.lon - hub.lon);
if (d < dn) nearest = f.properties;
}
return { icao: nearest.icao };
}
// Self-heal helper (same pattern as e2e/relocation.spec.ts): let any in-flight leg finish and
// reposition plane-starter home via the API so every test starts from the canonical state even
// after an interrupted previous run.
async function flyHomeViaApi(page: Page): Promise<void> {
let s = await getState(page);
let plane = s.planes.find((p) => p.id === 'plane-starter')!;
if (plane.status === 'flying' || plane.locationIcao !== HUB) {
// Fast-forward any leftover leg / repositioning flight (~20 s real time worst case at 1000×).
await setScale(page, SCALE);
}
if (plane.status === 'flying') {
// Let the in-flight leg finish before repositioning. While flying, locationIcao stays at the
// DEPARTURE airport — re-read state after landing before deciding whether a repositioning
// flight is needed.
await expect.poll(async () => ((await getState(page)).planes.find((p) => p.id === 'plane-starter')!.status), { timeout: 240_000 }).toBe('available');
s = await getState(page);
plane = s.planes.find((p) => p.id === 'plane-starter')!;
}
if (plane.locationIcao !== HUB) {
// ISS-011: the landing above auto-slown the sim to 1× — re-set SCALE so the return leg
// fast-forwards instead of taking real-time hours.
await setScale(page, SCALE);
const res = await page.request.post(`/api/planes/${plane.id}/fly`, { data: { destIcao: HUB } });
expect(res.ok()).toBeTruthy(); // repositioning needs no package (v0.12.0 manual flight)
await expect.poll(async () => ((await getState(page)).planes.find((p) => p.id === 'plane-starter')!.locationIcao), { timeout: 240_000 }).toBe(HUB);
}
}
// Self-heal in CLEANUP (afterEach): the test deliberately leaves plane-starter at the far end, so
// fast-forward + reposition home here; then restore scale=1 / unpaused for later specs.
test.afterEach(async ({ page }) => {
await flyHomeViaApi(page);
await postSim(page, { timeScale: 1, paused: false });
});
test.describe.configure({ mode: 'serial' }); // shared live DB — order matters within this file
test('auto-slow to 1× when the last plane lands; manual speed-up still works (ISS-011)', async ({ page }) => {
test.setTimeout(300_000); // self-heal (worst case ~20 s at 1000×) + one short leg
await flyHomeViaApi(page); // deterministic: the starter sits grounded at EDDF
const nearest = await nearestToHub(page);
// Set 1000× via the API and verify it stuck.
await setScale(page, SCALE);
// Dispatch plane-starter on the shortest leg from its hub (repositioning — no package needed).
const flyRes = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: nearest.icao } });
expect(flyRes.status()).toBe(200);
// Wait for the landing…
let landedBody: StateBody | undefined;
await expect
.poll(async () => {
const b = await getState(page);
const p = b.planes.find((x) => x.id === 'plane-starter')!;
if (p.status === 'available' && p.locationIcao === nearest.icao) landedBody = b;
return !!landedBody;
}, { timeout: 120_000, message: `waiting for plane-starter to land at ${nearest.icao}` })
.toBe(true);
// …and the server has auto-slown to 1× — persisted on sim_state, so this very snapshot shows it.
expect(landedBody!.simState.timeScale).toBe(1);
// Manual override still works: POST /api/sim {timeScale: 1000} → confirmed by the response…
const post = await page.request.post('/api/sim', { data: { timeScale: SCALE } });
expect(post.status()).toBe(200);
expect(((await post.json()) as SimState).timeScale).toBe(SCALE);
// …and by a fresh snapshot (no auto-restore of the previous scale — and no re-slow either,
// since nothing is in flight: the trigger is arrival-based, not idle-enforcement).
expect((await getState(page)).simState.timeScale).toBe(SCALE);
});