Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db5ad639fb | ||
|
|
7f28666f2e | ||
|
|
5093fba82e | ||
|
|
cde4334946 | ||
|
|
127d8f97f0 | ||
|
|
479c5448f7 |
@@ -1,5 +1,17 @@
|
||||
# Changelog
|
||||
|
||||
## v1.11.0 — 2026-09-27 22:55
|
||||
- **Do not show jobs for planes that are in air (ISS-159).** The right pane's open-package list now hides a row while no GROUNDED plane sits at the package's origin (`components/RightPane.tsx`): new exported `visibleOpenPackages(state)` replaces the bare `status === 'open'` filter — a job whose only claimant is still en route disappears from the list and reappears on landing. Payload-blocked rows (a grounded plane at the origin with no room) stay visible with their disabled Load button, exactly as before. Display-only: `poolAirports` and the spawner are untouched, so world state — jobs waiting at a flying plane's destination — is unchanged; this only stops rendering rows the player cannot dispatch yet. Known pre-existing gap (NOT fixed here): `loadEligiblePlanes` ignores plane ownership per the v1.4.0 shared-market design, so another player's grounded plane at an origin can make Load enabled for you → 403 on click.
|
||||
- **Tests.** Unit: new `visibleOpenPackages` suite in `components/right-pane.test.ts` — grounded plane at origin → visible; only a flying plane there → hidden; grounded elsewhere + one flying to the origin → hidden; payload-blocked grounded plane at origin → still visible with Load disabled. E2E: new test in `e2e/right-pane.spec.ts` dispatches the only plane toward an airport, creates a job AT that destination (in the pool while airborne, so it reaches `/api/state`), and asserts no row renders until landing — pinning the client-side filter rather than the v0.65.0 server-side pool visibility.
|
||||
|
||||
## v1.10.0 — 2026-09-27 22:31
|
||||
- **Top-bar Log out (ISS-157).** New `Log out` button as the last child of the top bar (`components/Map.tsx`) — flex row puts it top-right, no CSS. Same three steps as the admin panel's in-modal button: best-effort `POST /api/logout` with the session header, clear the stored session id, drop back to the sign-in screen via the existing `onLogout`. The two in-modal Log out buttons are kept (zero e2e churn).
|
||||
- **Admin button only for admins (ISS-158).** The top-bar `Admin` button now renders only when `/api/me` reports `admin: true` — hidden entirely, not disabled. No server change: the endpoint already returns `{username, admin}`; `app/page.tsx` now parses that body on its mount probe and passes `isAdmin` down AppShell → Map. The probe also re-runs after a form sign-in (bumped `probeKey`) so `isAdmin` tracks the user who just signed in, not whoever was probed before — an admin logging out followed by a non-admin signing in no longer sees the Admin button.
|
||||
- **E2E.** New test in `e2e/map.spec.ts`: top-bar Log out → sign-in form visible and app header gone (mirrors the existing admin-panel logout assertion).
|
||||
|
||||
## v1.9.0 — 2026-09-27 20:57
|
||||
- **E2E speedup follow-up (test-only, no game code changed).** `e2e/relocation.spec.ts`'s self-heal helper `flyHomeViaApi` now fast-forwards its cleanup legs at the v1.8.0 `FAST = 10_000` rate instead of a blanket `SCALE = 1000` — every scale-dependent wait in that file is a pure arrival wait (status `'available'` / `locationIcao === HUB`) with no mid-leg observation, so the higher rate is safe and the now-unused `SCALE` constant was dropped. Expected CI wall time for the file drops from ~40 s to ~25 s (~2–3 s off test 1, ~10–11 s each off tests 2 and 3). Test 3's deliberate `setScale(page, 1)` guard is untouched; `map.spec.ts` wheel-zoom (real-time-bound) and `multiplayer.spec.ts`'s N-player clock (measures sim-vs-real elapsed time) stay as-is.
|
||||
|
||||
## v1.8.0 — 2026-09-27 20:13
|
||||
- **E2E speedup (test-only, no game code changed).** Every Playwright spec that fast-forwards legs now uses the fastest rate its assertions allow instead of a blanket `SCALE = 1000`: pure arrival waits (no mid-leg observation) run at `FAST` — a lagged tick can only land a plane sooner, never skip an observed state. Expected CI wall time for the affected tests drops ~5–6× overall (e.g. the antimeridian test's relocation chains go from ~3 min to ~15 s per direction; two-planes' parallel-flight test loses its ~24 s afterEach heal leg).
|
||||
- **Per-test rates.** `actions.spec.ts` + `hover.spec.ts` + `two-planes.spec.ts`: new local `FAST = 10_000` for expiry fast-forwards, landing waits and fly-home cleanups. `queue.spec.ts`: same `FAST`, plus `CHAIN = 4_000` for the auto-chain observation (max per-tick advance 2 s × S stays below the LEMD-length leg's ~1e7 sim-ms duration so one lagged tick can't complete it before a poll sees the chained state); its refuel-before-chained-leg test moves to an explicit `2_000` with the fuel lower bound relaxed from `> 1.3×required` to `> required` (at 2_000× the snapshot can land ~0.4×required into leg 2's burn under dind load; a wrong top-up level still lands far below the bound, and the exact level stays pinned by the unchanged cash assertion). `map-interactions.spec.ts`: `FAST = 50_000` for chain hops (≤3400 km each) and arrival waits.
|
||||
|
||||
+14
-6
@@ -19,6 +19,11 @@ export default function Home() {
|
||||
// the sign-in screen or the game shell. The shell only mounts once authenticated, so its state
|
||||
// feed never polls an unauthenticated /api/state.
|
||||
const [authed, setAuthed] = useState<boolean | null>(null);
|
||||
// v1.10.0 (ISS-158): /api/me's `admin` flag — gates the top-bar Admin button in Map.
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
// Bumped after a form sign-in so the probe below re-runs for the NEW session — isAdmin must
|
||||
// track whoever just signed in, not the user probed on mount (or before the last logout).
|
||||
const [probeKey, setProbeKey] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const sid = storedSession();
|
||||
@@ -28,9 +33,10 @@ export default function Home() {
|
||||
}
|
||||
let cancelled = false;
|
||||
fetch('/api/me', { headers: sessionHeaders(sid), cache: 'no-store' })
|
||||
.then((res) => {
|
||||
.then(async (res) => {
|
||||
if (cancelled) return;
|
||||
if (!res.ok) clearStoredSession(); // stale/expired id — drop it and show sign-in
|
||||
else setIsAdmin(Boolean(((await res.json()) as { admin?: boolean }).admin)); // v1.10.0 (ISS-158)
|
||||
setAuthed(res.ok);
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -39,7 +45,7 @@ export default function Home() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
}, [probeKey]);
|
||||
|
||||
if (authed === null) {
|
||||
return (
|
||||
@@ -51,14 +57,16 @@ export default function Home() {
|
||||
);
|
||||
}
|
||||
if (!authed) {
|
||||
return <SignInScreen onAuthed={() => setAuthed(true)} />;
|
||||
// v1.10.0 (ISS-158): re-probe /api/me for the new session instead of blindly flipping authed —
|
||||
// isAdmin has to come from the user who just signed in.
|
||||
return <SignInScreen onAuthed={() => setProbeKey((k) => k + 1)} />;
|
||||
}
|
||||
|
||||
// The admin panel's Log out cleared the session — drop back to sign-in (ISS-107).
|
||||
return <AppShell onLogout={() => setAuthed(false)} />;
|
||||
return <AppShell onLogout={() => setAuthed(false)} isAdmin={isAdmin} />;
|
||||
}
|
||||
|
||||
function AppShell({ onLogout }: { onLogout: () => void }) {
|
||||
function AppShell({ onLogout, isAdmin }: { onLogout: () => void; isAdmin: boolean }) {
|
||||
// Single polling source for the whole shell (v0.9.0): one /api/state loop, snapshot passed
|
||||
// down as props so later panes can share it without extra poll loops. applySnapshot (v0.10.0)
|
||||
// lets a pane push an action endpoint's full response snapshot in immediately.
|
||||
@@ -95,7 +103,7 @@ function AppShell({ onLogout }: { onLogout: () => void }) {
|
||||
{/* v0.11.0 — the same polled snapshot drives the map's plane markers + route lines;
|
||||
ISS-010 (v0.18.0) — applySnapshot lets the airport popup's Relocate row push its
|
||||
action response into the shared state, exactly like RightPane does */}
|
||||
<Map state={state} hovered={hovered} applySnapshot={applySnapshot} onLogout={onLogout} />
|
||||
<Map state={state} hovered={hovered} applySnapshot={applySnapshot} onLogout={onLogout} isAdmin={isAdmin} />
|
||||
<RightPane state={state} loading={loading} applySnapshot={applySnapshot} onHover={setHovered} onError={onError} />
|
||||
<ToastStack toasts={toasts} onDismiss={dismiss} />
|
||||
</div>
|
||||
|
||||
+22
-5
@@ -14,7 +14,7 @@ import { approxLocalTime } from '../lib/localTime';
|
||||
import Modal from './Modal';
|
||||
import AdminPanel from './AdminPanel';
|
||||
import Leaderboard from './Leaderboard';
|
||||
import { sessionHeaders } from '../lib/authClient';
|
||||
import { clearStoredSession, sessionHeaders } from '../lib/authClient';
|
||||
|
||||
// v0.70.0 default view: null island [0,0] at zoom 2 — the world is 2048 px wide there and the
|
||||
// Africa+Europe bounding box (lat −35..+71, lon −18..+52) spans ~800 px vertically in Web Mercator,
|
||||
@@ -74,11 +74,13 @@ interface MapProps {
|
||||
// v1.1.0 (ISS-107): the admin panel's Log out cleared the session — tell the shell to drop back
|
||||
// to the sign-in screen instead of leaving a signed-out user staring at a live dashboard.
|
||||
onLogout?: () => void;
|
||||
// v1.10.0 (ISS-158): /api/me says this session is an admin → show the top-bar Admin button.
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
// Named FlightMap (not Map): a component called `Map` would shadow the global
|
||||
// Map constructor inside its own body, breaking `new Map(...)` for the airport index.
|
||||
export default function FlightMap({ state, hovered, applySnapshot, onLogout }: MapProps) {
|
||||
export default function FlightMap({ state, hovered, applySnapshot, onLogout, isAdmin = false }: MapProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const mapRef = useRef<maplibregl.Map | null>(null);
|
||||
// /api/airports is fetched at most once per page life: every styledata event
|
||||
@@ -569,6 +571,14 @@ export default function FlightMap({ state, hovered, applySnapshot, onLogout }: M
|
||||
void localAssetsAvailable().then((available) => mapRef.current?.setStyle(styleUrlFor(id, available)));
|
||||
};
|
||||
|
||||
// v1.10.0 (ISS-157): top-bar Log out — same three steps as the admin panel's button: best-effort
|
||||
// server-side session kill, drop the stored id, tell the shell to show sign-in again.
|
||||
const onTopBarLogout = async () => {
|
||||
await fetch('/api/logout', { method: 'POST', headers: sessionHeaders() }).catch(() => undefined);
|
||||
clearStoredSession();
|
||||
onLogout?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="map-wrap">
|
||||
<header data-testid="app-header" className="app-header">
|
||||
@@ -596,9 +606,16 @@ export default function FlightMap({ state, hovered, applySnapshot, onLogout }: M
|
||||
<button type="button" data-testid="leaderboard-btn" className="pane-btn" onClick={() => setLeaderboardOpen(true)}>
|
||||
Leaderboard
|
||||
</button>
|
||||
{/* v0.71.0: admin panel entry point — right side of the top bar, opens a modal (no URL). */}
|
||||
<button type="button" data-testid="admin-btn" className="pane-btn" onClick={() => setAdminOpen(true)}>
|
||||
Admin
|
||||
{/* v0.71.0: admin panel entry point — right side of the top bar, opens a modal (no URL).
|
||||
v1.10.0 (ISS-158): only for admins (/api/me `admin` flag) — hidden entirely otherwise. */}
|
||||
{isAdmin && (
|
||||
<button type="button" data-testid="admin-btn" className="pane-btn" onClick={() => setAdminOpen(true)}>
|
||||
Admin
|
||||
</button>
|
||||
)}
|
||||
{/* v1.10.0 (ISS-157): Log out — last child, so the flex row puts it top-right of the bar. */}
|
||||
<button type="button" data-testid="logout-btn" className="pane-btn" onClick={() => void onTopBarLogout()}>
|
||||
Log out
|
||||
</button>
|
||||
</header>
|
||||
<div ref={containerRef} data-testid="map" className="map-container" />
|
||||
|
||||
@@ -46,6 +46,15 @@ export function loadEligiblePlanes(state: StateSnapshot, pkg: PackageState): Pla
|
||||
});
|
||||
}
|
||||
|
||||
// v1.11.0 (ISS-159): the open packages the market list shows — hidden while no GROUNDED plane sits at
|
||||
// their origin ("jobs for planes that are in air" disappear until landing). Payload-blocked rows stay:
|
||||
// a grounded plane at the origin keeps the row visible with its disabled Load button.
|
||||
export function visibleOpenPackages(state: StateSnapshot): PackageState[] {
|
||||
return state.packages.filter(
|
||||
(p) => p.status === 'open' && state.planes.some((pl) => pl.status === 'available' && pl.locationIcao === p.originIcao),
|
||||
);
|
||||
}
|
||||
|
||||
// Load controls for one open package: a single Load button when exactly one plane
|
||||
// is eligible (it acts on that plane directly), a small plane picker + Load when
|
||||
// several are, and a disabled button with the reason otherwise. Each row owns its
|
||||
@@ -141,7 +150,7 @@ export default function RightPane({ state, loading, applySnapshot, onHover, onEr
|
||||
if (!state) return null;
|
||||
|
||||
const clockMs = state.simState.clockMs;
|
||||
const open = state.packages.filter((p) => p.status === 'open');
|
||||
const open = visibleOpenPackages(state);
|
||||
|
||||
const catalog = state.catalog;
|
||||
const buyEntry = catalog.find((e) => e.model === buyModelId) ?? catalog[0] ?? null;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { cargoWeightFor, loadEligiblePlanes } from './RightPane';
|
||||
import { cargoWeightFor, loadEligiblePlanes, visibleOpenPackages } from './RightPane';
|
||||
import type { PackageState, PlaneState, StateSnapshot } from '@/lib/game/state';
|
||||
|
||||
// v0.12.0 (weight model v0.27.0) — the package-model picker rules: a plane can LOAD a
|
||||
@@ -134,3 +134,39 @@ describe('loadEligiblePlanes (Load button eligibility)', () => {
|
||||
expect(loadEligiblePlanes(s, pkg({ id: 'x' }))).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('visibleOpenPackages (market list row filter, ISS-159)', () => {
|
||||
it('shows an open package when a grounded plane sits at its origin', () => {
|
||||
const s = snap(
|
||||
[plane({ id: 'at-origin' })],
|
||||
[pkg({ id: 'x' }), pkg({ id: 'onboard', status: 'loaded', planeId: 'at-origin' })], // non-open statuses never listed
|
||||
);
|
||||
expect(visibleOpenPackages(s).map((p) => p.id)).toEqual(['x']);
|
||||
});
|
||||
|
||||
it('hides an open package when only a flying plane is at its origin (in air, not grounded)', () => {
|
||||
const s = snap([plane({ id: 'en-route', status: 'flying', destIcao: 'LEMD' })], [pkg({ id: 'x' })]);
|
||||
expect(visibleOpenPackages(s)).toEqual([]);
|
||||
});
|
||||
|
||||
it('hides an open package when the grounded plane is elsewhere and another flies to its origin', () => {
|
||||
const s = snap(
|
||||
[
|
||||
plane({ id: 'grounded-elsewhere', locationIcao: 'LEMD' }),
|
||||
plane({ id: 'en-route', status: 'flying', destIcao: 'EDDF' }), // flying TO the package's origin — not grounded there yet
|
||||
],
|
||||
[pkg({ id: 'x' })],
|
||||
);
|
||||
expect(visibleOpenPackages(s)).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps a payload-blocked row visible when a grounded plane at the origin has no room (disabled Load path preserved)', () => {
|
||||
const packages = [
|
||||
...Array.from({ length: 4 }, (_, i) => pkg({ id: `fill-${i}`, status: 'loaded', planeId: 'full', weightKg: 1_200 })), // 4 × 1200 = 4800 — full
|
||||
pkg({ id: 'x' }),
|
||||
];
|
||||
const s = snap([plane({ id: 'full' })], packages);
|
||||
expect(visibleOpenPackages(s).map((p) => p.id)).toEqual(['x']); // row stays…
|
||||
expect(loadEligiblePlanes(s, s.packages.find((p) => p.id === 'x')!)).toEqual([]); // …with Load disabled
|
||||
});
|
||||
});
|
||||
|
||||
@@ -78,6 +78,17 @@ test('header shows the app version sourced from package.json', async ({ page })
|
||||
);
|
||||
});
|
||||
|
||||
// v1.10.0 (ISS-157): top-bar Log out — same drop-to-sign-in as the admin panel's button
|
||||
// (mirrors e2e/admin.spec.ts' logout assertion).
|
||||
test('top bar Log out drops back to the sign-in screen', async ({ page }) => {
|
||||
await page.goto(APP_URL);
|
||||
const header = page.getByTestId('app-header');
|
||||
await expect(header).toBeVisible({ timeout: 30_000 });
|
||||
await page.getByTestId('logout-btn').click();
|
||||
await expect(page.getByTestId('sign-in-form')).toBeVisible({ timeout: 10_000 });
|
||||
await expect(header).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('clicking an airport marker opens a popup with name and ICAO', async ({ page }) => {
|
||||
await page.goto(APP_URL);
|
||||
await waitForAirportCount(page);
|
||||
|
||||
@@ -14,7 +14,7 @@ test.beforeEach(async ({ page }) => { await authenticate(page); });
|
||||
// works for FLYING planes — it then appends to POST /api/planes/{id}/queue instead of flying now.
|
||||
// Marker-size regression is covered by the existing e2e/map.spec.ts click tests, not here.
|
||||
|
||||
const SCALE = 1000; // test rate for fast-forwarding legs — keeps cleanup flights to seconds of real time
|
||||
const FAST = 10_000; // pure arrival waits (no mid-leg observation) — cleanup legs land in ~1 s
|
||||
const WORLD = 24; // the fixed world rate (ISS-117) — must match lib/worldClock.ts
|
||||
const HUB = 'EDDF'; // Frankfurt Main — the seeded starter plane's home base (lib/game/seed.ts)
|
||||
// LFRS Nantes Atlantique — same isolated marker as e2e/map.spec.ts. The v0.70.0 default view ([0,0] z2)
|
||||
@@ -93,8 +93,8 @@ 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);
|
||||
// Fast-forward any leftover leg / repositioning flight (~2 s real time worst case at FAST).
|
||||
await setScale(page, FAST);
|
||||
}
|
||||
if (plane.status === 'flying') {
|
||||
// While flying, locationIcao stays at the DEPARTURE airport — re-read state after landing
|
||||
@@ -104,8 +104,8 @@ async function flyHomeViaApi(page: Page): Promise<void> {
|
||||
plane = s.planes.find((p) => p.id === 'plane-starter')!;
|
||||
}
|
||||
if (plane.locationIcao !== HUB) {
|
||||
// Make sure the world rate is back at SCALE so the return leg fast-forwards.
|
||||
await setScale(page, SCALE);
|
||||
// Make sure the world rate is back at FAST so the return leg fast-forwards.
|
||||
await setScale(page, FAST);
|
||||
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);
|
||||
@@ -122,7 +122,7 @@ test.afterEach(async ({ page }) => {
|
||||
test.describe.configure({ mode: 'serial' }); // shared live DB — order matters within this file
|
||||
|
||||
test('left pane Relocate section relocates a grounded plane', async ({ page }) => {
|
||||
test.setTimeout(300_000); // self-heal (worst case ~20 s at 1000×) + one short leg
|
||||
test.setTimeout(300_000); // self-heal (worst case ~2 s at FAST) + one short leg
|
||||
await flyHomeViaApi(page);
|
||||
|
||||
// Full tank before departure — deterministic baseline (ISS-082 hook; dispatch would auto-refuel anyway).
|
||||
@@ -154,7 +154,7 @@ test('left pane Relocate section relocates a grounded plane', async ({ page }) =
|
||||
});
|
||||
|
||||
test('airport popup offers Relocate options for grounded planes', async ({ page }) => {
|
||||
test.setTimeout(300_000); // self-heal (worst case ~20 s at 1000×) + one leg to LFRS
|
||||
test.setTimeout(300_000); // self-heal (worst case ~2 s at FAST) + one leg to LFRS
|
||||
await flyHomeViaApi(page);
|
||||
|
||||
// Full tank before departure — deterministic baseline (ISS-082 hook; dispatch would auto-refuel anyway).
|
||||
@@ -221,7 +221,7 @@ test('airport popup offers Relocate options for grounded planes', async ({ page
|
||||
});
|
||||
|
||||
test('airport popup Relocate queues a leg for a flying plane', async ({ page }) => {
|
||||
test.setTimeout(300_000); // self-heal (worst case ~20 s at 1000×) + one short leg, fast-forwarded in cleanup
|
||||
test.setTimeout(300_000); // self-heal (worst case ~2 s at FAST) + one short leg, fast-forwarded in cleanup
|
||||
await flyHomeViaApi(page);
|
||||
|
||||
// Full tank before departure — deterministic baseline (ISS-082 hook; dispatch would auto-refuel anyway).
|
||||
|
||||
@@ -270,6 +270,26 @@ test('open jobs only appear where a plane can take them — foreign-origin jobs
|
||||
await expect(page.locator(`li[data-testid="open-package-${pkg.id}"]`)).toHaveCount(0, { timeout: 15_000 });
|
||||
});
|
||||
|
||||
test('jobs at an airport with no grounded plane stay hidden while a plane is still en route (ISS-159)', async ({ page }) => {
|
||||
test.setTimeout(300_000); // self-heal can take minutes after an interrupted run
|
||||
await flyHomeViaApi(page); // starter grounded at EDDF
|
||||
|
||||
const nearest = await nearestToHub(page);
|
||||
// Dispatch the only plane toward `nearest` — while airborne its DESTINATION joins the pool (v0.65.0), so a job there
|
||||
// reaches /api/state, but no GROUNDED plane sits at that airport yet: ISS-159 hides it until landing.
|
||||
const flyRes = await page.request.post('/api/planes/plane-starter/fly', { data: { destIcao: nearest.icao } });
|
||||
expect(flyRes.status()).toBe(200);
|
||||
|
||||
const pkg = await createPackage(page, nearest.icao, HUB);
|
||||
// The server keeps the job (its origin is in the pool)…
|
||||
await expect.poll(async () => (await getState(page)).packages.some((p) => p.id === pkg.id), { timeout: 15_000 }).toBe(true);
|
||||
|
||||
await page.goto('/');
|
||||
// …but no row renders while the plane is still en route — no grounded plane at its origin. No landing wait needed;
|
||||
// the next test's flyHomeViaApi self-heal finishes this leg.
|
||||
await expect(page.locator(`li[data-testid="open-package-${pkg.id}"]`)).toHaveCount(0, { timeout: 15_000 });
|
||||
});
|
||||
|
||||
test('on-board list: fly-to-destination + abandon with penalty preview; flying planes show their leg', async ({ page }) => {
|
||||
test.setTimeout(240_000); // one short flight at scale 1000 (seconds of real time) plus UI steps
|
||||
await flyHomeViaApi(page);
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "flight-ops",
|
||||
"version": "1.8.0",
|
||||
"version": "1.11.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3000",
|
||||
|
||||
Reference in New Issue
Block a user