Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
096a6bfe61 | ||
|
|
71ee63b38a | ||
|
|
a4f7947666 | ||
|
|
570ad52d47 | ||
|
|
59974d1b79 | ||
|
|
af5176354a | ||
|
|
f5468bb640 | ||
|
|
0970ee4710 |
@@ -1,18 +1,16 @@
|
||||
# Flight Simulator — Workflow Rules
|
||||
|
||||
## Change process (mandatory)
|
||||
1. **Small, incremental changes** — work is done one change at a time, in smaller parts; each part must be tested thoroughly with unit and e2e tests covering it before moving on.
|
||||
1. **Small, incremental changes** — work is done one change at a time, in smaller parts; each part must be tested thoroughly with unit and e2e tests covering it before moving on. Connected issues/tasks that reference the same part of code MAY be done together on one branch/PR with a single version bump (e.g. two UI tweaks to the same panel).
|
||||
2. **Feature branch + PR** — every change lands on a feature branch off `master` (no direct pushes to master). The branch carries the commits below; open a pull request to `master`.
|
||||
3. **Changelog entry** — every such change is listed in `CHANGELOG.md` with date, time and version (format: `## v<version> — YYYY-MM-DD HH:MM`, then bullet points of what changed).
|
||||
4. **Version bump** — the version is updated in `package.json`.
|
||||
5. **CI must pass** — the PR's pipeline (build → unit tests → e2e, `.gitea/workflows/ci.yml`) must be green before merging. CI never deploys to production; its e2e runs against an isolated stack inside dind.
|
||||
5. **CI must pass** — enforced by branch protection on `master` (required status check `CI / build + unit + e2e (pull request)`): the PR cannot be merged until CI is green. Merge via auto-merge (`POST /repos/{owner}/{repo}/pulls/{n}/merge` with body `{"do":"merge","merge_style":"merge","merge_when_checks_succeed":true}`) so it merges automatically once CI passes. CI never deploys to production; its e2e runs against an isolated stack inside dind.
|
||||
6. **Tag + manual deploy after merge** — once merged, tag the version on `master` (e.g. `v0.13.0`). Production (`https://flight.dev.kkhost.pl`) is updated manually afterwards: `docker compose up -d --build` on the host.
|
||||
|
||||
## E2E tests (mandatory)
|
||||
1. **Docker compose instance is the app under test** — e2e always runs against a docker-compose build, never a local `next dev` on :3000 (dev builds don't render for non-loopback clients, and a second instance splits state). Locally that means the Traefik-routed instance at `https://flight.dev.kkhost.pl`; in CI it is an isolated stack inside dind (`docker-compose.ci.yml`) — **CI never deploys to or modifies production**.
|
||||
2. **Update before testing (local)** — deploy current code first: `docker compose up -d --build`, then run the suite with plain `npx playwright test`. The config's webServer block is off by default; set `E2E_DEPLOY_CMD` to have Playwright itself run a deploy command and wait for readiness.
|
||||
3. **Remote browsers** — local runs drive browsers through the shared Playwright server at `192.168.1.4:3000` (Docker image tag must match this repo's @playwright/test version, pinned in devDependencies). Set `PW_REMOTE_BROWSER=0` to run local chromium instead — CI does, because the remote server cannot reach a stack inside dind.
|
||||
4. **CI env vars** (`playwright.config.ts`, all optional; defaults preserve the local flow) — `E2E_BASE_URL` (app under test), `E2E_DEPLOY_CMD` (webServer deploy command), `PW_REMOTE_BROWSER=0` (local browsers); `e2e/state.spec.ts` reads `E2E_COMPOSE_ARGS` for its mid-run container restart.
|
||||
1. **CI-only** — e2e runs ONLY in CI, against an isolated docker stack inside dind built from `docker-compose.ci.yml` (project name `flight-ci`, app container on a static IP; see `.gitea/workflows/ci.yml`). There are NO local e2e runs at all: locally you verify with `tsc --noEmit` + `npx vitest run` + `npx playwright test --list` (typecheck, unit suite, specs compile and list), and the full e2e suite is verified by CI on every PR. **CI never deploys to or modifies production.**
|
||||
2. **CI env vars** (`playwright.config.ts`, all optional) — `E2E_BASE_URL` (app under test; CI sets it to the in-network app host, so the config's default of the live Traefik instance at `https://flight.dev.kkhost.pl` is never used), `E2E_DEPLOY_CMD` (runs as Playwright webServer: deploys the stack and waits for readiness before tests start; CI uses build + `up -d` + `logs -f`), `PW_REMOTE_BROWSER=0` (CI runs local chromium because the shared remote browser server at 192.168.1.4 cannot reach a stack inside dind — when unset, the config defaults to that remote server and to the live instance as baseURL), and `E2E_COMPOSE_ARGS`, read by `e2e/state.spec.ts` for its mid-run `docker compose restart flight` (CI points it at `-f docker-compose.ci.yml -p flight-ci`).
|
||||
|
||||
## API
|
||||
1. **Live base URL** — `https://flight.dev.kkhost.pl` (the docker-compose build behind Traefik).
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## v0.21.0 — 2026-09-19 21:01
|
||||
- **Relocate panel moves to the left pane (ISS-023).** Flying an empty/grounded plane to another airport is "anything with our plane", so it belongs in the left pane, not as a market row on the right. The `.market-row.market-row-relocate` JSX (`relocate-plane` select of grounded planes labeled `{planeName} — {icao}`, `relocate-dest` name-sorted select, `relocate-go` button → `POST /api/planes/{id}/fly {destIcao}`) moved verbatim from `components/RightPane.tsx` into a new "Relocate" `<section>` in `components/LeftPane.tsx`, inserted between On board and Time. Its state (`relocatePlaneId` / `relocateDestIcao`) and the one-time `/api/airports` fetch (promise-ref pattern, GeoJSON → `{name, icao}[]` sorted by name) moved with it — the relocate-dest select was that fetch's only consumer in RightPane, so nothing else there changed. Busy gating rewired from RightPane's `busy` to LeftPane's own `useGameAction` instance (`actionsBusy`); a grounded-plane filter was added next to the existing `onboard` compute; plane labels reuse the expression already inlined in the Fleet rows. Testids kept byte-identical (`relocate-plane` / `relocate-dest` / `relocate-go`) and the row keeps its `.market-row market-row-relocate` classes (global CSS, works in the 360 px left pane); stale "280 px right pane" comment updated in `app/globals.css`. The map-popup relocation controls (`airport-popup-*`, Map.tsx) are a separate surface fed by the snapshot only — untouched
|
||||
- Tests: e2e — only `relocation.spec.ts` touches these testids and all locators are global, so no locator changes; the first test's title (which mentioned "right pane") and the file header comments now say left-pane Relocate section. No unit-test changes (baseline 283)
|
||||
- Verified: `tsc --noEmit` clean; unit suite green locally; e2e not run locally by design — CI verifies against an isolated stack
|
||||
|
||||
## v0.20.0 — 2026-09-19 19:21
|
||||
- **On board moved to the left pane (ISS-021).** The "On board" section (per-plane blocks with loaded/expired cargo rows, Refuel-for-leg / Fly-to / Abandon + penalty preview) now renders in `components/LeftPane.tsx` between Fleet and Time — wallet stays on top. The JSX moved verbatim (same testids: `onboard-{planeId}`, `onboard-package-{pkg.id}`, `refuel-for-*`, `fly-to-*`, `abandon-*` + penalty preview), along with the `onboard` computation (`planes.filter(flying || slotsUsedFor > 0)`) and per-plane cargo filter. All onboard e2e locators are global testids, so existing specs work unchanged
|
||||
- **Left pane widened: grid swapped to 360/1fr/280.** `.app-shell` in `app/globals.css` goes from `280px minmax(0, 1fr) 360px` to `360px minmax(0, 1fr) 280px` — total fixed width stays 640 px, so the map column is pixel-identical (map.spec.ts clicks via dynamic `map.project()`). Stale comments updated (on-board blocks now in the left pane; Relocate row comment reflects the 280 px right pane)
|
||||
@@ -8,6 +13,13 @@
|
||||
- Verified: `tsc --noEmit` clean; unit suite green locally; e2e not run locally by design — CI verifies against an isolated stack
|
||||
- **On-board cargo rows are two lines (ISS-028).** Each `onboard-package-*` row now stacks a full-width "Fly to <airport>" button on its own line above Refuel + Abandon side by side: `components/LeftPane.tsx` moves the fly-to button out of `.contract-main` into the row root and wraps refuel-for + abandon in a new `.onboard-actions` div — all testids, label text, penalty preview and disabled states unchanged. Long destination names in the fly-to label truncate with an ellipsis instead of overflowing: the label's inner span gets `min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap`, full name preserved via a `title` attribute (CSS in `app/globals.css`: `.onboard-block .contract-row` becomes flex-column, new `.onboard-fly-btn` / `.onboard-fly-label` / `.onboard-actions`). No e2e changes — specs locate everything by testid and assert no layout or button order; no version bump (part 2 of v0.20.0)
|
||||
|
||||
## v0.19.2 — 2026-09-19 20:06
|
||||
- **AGENTS.md "Change process" item 1** — connected issues/tasks that reference the same part of code may now be done together on one branch/PR with a single version bump (e.g. two UI tweaks to the same panel)
|
||||
- **AGENTS.md "E2E tests" section rewritten to match current reality** — e2e runs ONLY in CI, against an isolated docker stack inside dind (`docker-compose.ci.yml`); there are NO local e2e runs at all (local verification = `tsc --noEmit` + `npx vitest run` + `npx playwright test --list`; the full suite is verified by CI on every PR). Dropped the stale local-run instructions (deploying to the Traefik-routed instance before testing, driving browsers through the remote Playwright server at 192.168.1.4:3000); kept and reworded the CI env vars that actually exist in `playwright.config.ts` (`E2E_BASE_URL`, `E2E_DEPLOY_CMD`, `PW_REMOTE_BROWSER=0`, `E2E_COMPOSE_ARGS`)
|
||||
|
||||
## v0.19.1 — 2026-09-19 18:48
|
||||
- AGENTS.md: document that CI-green is enforced on master by branch protection (required status check) and that merges use auto-merge (`merge_when_checks_succeed`) so PRs merge automatically once CI passes
|
||||
|
||||
## v0.19.0 — 2026-09-19 18:05
|
||||
- **Auto-slow to 1× when all planes have landed (ISS-011).** When the last plane in flight lands, the sim slows down on its own so an idle fleet doesn't sit at 1000×. Server-side and authoritative: `tickOnce` (`lib/sim/engine.ts`) now remembers how many planes `deliverPackages` finalized during a tick, and when ≥1 plane landed this tick AND no plane remains with status `'flying'` AND the sim is not paused, it persists `timeScale = 1` on the same `sim_state` write as the clock — so the UI slider reflects it on the very next snapshot (no API or OpenAPI change: timeScale is still set via the existing `POST /api/sim`, and `clampTimeScale` semantics are untouched since 1 is a valid ladder value)
|
||||
- **Arrival-triggered, not idle-enforced.** The trigger fires only on the tick that processes a landing — a grounded fleet at high scale with no flight ever happening is left alone (this keeps the existing slider e2e in `panes.spec.ts` green: it sets 1000× and expects the server to confirm it while nothing is flying). Paused state is untouched: while paused no landings are processed, so a paused sim never auto-slows
|
||||
|
||||
+1
-1
@@ -364,7 +364,7 @@ body {
|
||||
}
|
||||
|
||||
/* ISS-010 (v0.18.0) — the Relocate row carries TWO selects, so let it wrap instead of
|
||||
squeezing both below their readable width in the 280 px right pane (ISS-021 v0.20.0). */
|
||||
squeezing both below their readable width in the 360 px left pane (moved there in ISS-023 v0.21.0). */
|
||||
.market-row-relocate {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useRef, useState, type ChangeEvent } from 'react';
|
||||
import type { GeoJsonFeatureCollection } from '@/lib/airports';
|
||||
import type { PlaneState, StateSnapshot } from '@/lib/game/state';
|
||||
import { PLANE_TYPES } from '@/lib/game/catalog';
|
||||
import { abandonPenalty } from '@/lib/game/penalty';
|
||||
@@ -95,6 +96,34 @@ export default function LeftPane({ state, loading, error, onRefresh, applySnapsh
|
||||
// ISS-021 (v0.20.0): shared action runner for the On board section (moved here from RightPane).
|
||||
const { runAction, busy: actionsBusy, actionError } = useGameAction(state, applySnapshot, onError);
|
||||
|
||||
// ISS-023 (v0.21.0): Relocate selections — moved here from the right-pane market row; same
|
||||
// "effective selection" pattern as there: the user's pick while it stays valid, else a sensible
|
||||
// default (first grounded plane / first airport).
|
||||
const [relocatePlaneId, setRelocatePlaneId] = useState<string | null>(null);
|
||||
const [relocateDestIcao, setRelocateDestIcao] = useState<string | null>(null);
|
||||
|
||||
// ISS-010 (v0.18.0): one-time /api/airports fetch per page life — destination options for the
|
||||
// Relocate section, sorted by name so e2e selection is stable. Same promise-ref pattern as Map.tsx;
|
||||
// on failure airportOptions stays null and the Relocate button simply remains disabled. Moved here
|
||||
// from RightPane in ISS-023 (v0.21.0) — the relocate-dest select was its only consumer there.
|
||||
const [airportOptions, setAirportOptions] = useState<Array<{ name: string; icao: string }> | null>(null);
|
||||
const airportsPromiseRef = useRef<Promise<Array<{ name: string; icao: string }>> | null>(null);
|
||||
useEffect(() => {
|
||||
if (!airportsPromiseRef.current) {
|
||||
airportsPromiseRef.current = fetch('/api/airports', { cache: 'no-store' })
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(`GET /api/airports → HTTP ${res.status}`);
|
||||
return res.json() as Promise<GeoJsonFeatureCollection>;
|
||||
})
|
||||
.then((fc) =>
|
||||
fc.features
|
||||
.map((f) => ({ name: f.properties.name, icao: f.properties.icao }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name, 'en')),
|
||||
);
|
||||
}
|
||||
void airportsPromiseRef.current.then(setAirportOptions).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// POST /api/sim (retried up to SIM_POST_MAX_ATTEMPTS× on failure — CI runs #44/#47), then
|
||||
// refetch the full snapshot immediately so the pane reflects the new controls without waiting
|
||||
// for the next poll tick. After the final attempt fails keep the stale UI and surface an error
|
||||
@@ -168,6 +197,14 @@ export default function LeftPane({ state, loading, error, onRefresh, applySnapsh
|
||||
const clockMs = state?.simState.clockMs ?? 0;
|
||||
const onboard = state ? state.planes.filter((p) => p.status === 'flying' || slotsUsedFor(state, p.id) > 0) : [];
|
||||
|
||||
// ISS-023 (v0.21.0): Relocate — any GROUNDED plane can be repositioned to any airport via the
|
||||
// existing fly endpoint (pure repositioning, no package needed since v0.12.0). Moved here from
|
||||
// the right-pane market row; with state null both fall back to null and the section isn't rendered.
|
||||
const grounded = state ? state.planes.filter((p) => p.status === 'available') : [];
|
||||
const relocatePlane = grounded.find((p) => p.id === relocatePlaneId) ?? grounded[0] ?? null;
|
||||
const relocateDest =
|
||||
airportOptions === null ? null : airportOptions.find((a) => a.icao === relocateDestIcao) ?? airportOptions[0] ?? null;
|
||||
|
||||
// Slider commit (v0.9.1): the displayed value follows the drag optimistically via
|
||||
// pendingScale; POST /api/sim fires after a 250 ms debounce, or immediately when the
|
||||
// user releases the slider (browsers fire a native 'change' on release). The server's
|
||||
@@ -399,6 +436,53 @@ export default function LeftPane({ state, loading, error, onRefresh, applySnapsh
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* ISS-023 (v0.21.0): moved here from the right-pane market row — it belongs with
|
||||
everything else about our planes, between On board and Time. */}
|
||||
<section className="pane-section">
|
||||
<h3 className="pane-heading">Relocate</h3>
|
||||
{/* ISS-010 (v0.18.0): relocate a grounded plane — the fly endpoint already allows pure
|
||||
repositioning, this row just exposes it for planes with nothing on board. */}
|
||||
<div className="market-row market-row-relocate">
|
||||
<select
|
||||
data-testid="relocate-plane"
|
||||
aria-label="Plane to relocate"
|
||||
value={relocatePlane?.id ?? ''}
|
||||
onChange={(e) => setRelocatePlaneId(e.target.value || null)}
|
||||
>
|
||||
{grounded.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{PLANE_TYPES[p.model]?.name ?? p.model} — {p.locationIcao}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
data-testid="relocate-dest"
|
||||
aria-label="Relocation destination airport"
|
||||
value={relocateDest?.icao ?? ''}
|
||||
onChange={(e) => setRelocateDestIcao(e.target.value || null)}
|
||||
>
|
||||
{airportOptions?.map((a) => (
|
||||
<option key={a.icao} value={a.icao}>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="relocate-go"
|
||||
className="pane-btn"
|
||||
disabled={actionsBusy || relocatePlane === null || relocateDest === null}
|
||||
onClick={() => {
|
||||
if (relocatePlane && relocateDest) {
|
||||
void runAction(`/api/planes/${relocatePlane.id}/fly`, { destIcao: relocateDest.icao });
|
||||
}
|
||||
}}
|
||||
>
|
||||
Relocate
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="pane-section">
|
||||
<h3 className="pane-heading">Time</h3>
|
||||
<p data-testid="sim-clock" className="sim-clock">
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { GeoJsonFeatureCollection } from '@/lib/airports';
|
||||
import { useState } from 'react';
|
||||
import type { PackageState, PlaneState, StateSnapshot } from '@/lib/game/state';
|
||||
import { PLANE_TYPES } from '@/lib/game/catalog';
|
||||
import { formatCash, formatDeadlineRelative } from '@/lib/format';
|
||||
@@ -119,31 +118,8 @@ export default function RightPane({ state, loading, applySnapshot, onHover, onEr
|
||||
const [sellPlaneId, setSellPlaneId] = useState<string | null>(null);
|
||||
const [refuelPlaneId, setRefuelPlaneId] = useState<string | null>(null);
|
||||
|
||||
// ISS-010 (v0.18.0): Relocate row selections — same "effective selection" pattern as the rows above:
|
||||
// the user's pick while it stays valid, else a sensible default (first grounded plane / first airport).
|
||||
const [relocatePlaneId, setRelocatePlaneId] = useState<string | null>(null);
|
||||
const [relocateDestIcao, setRelocateDestIcao] = useState<string | null>(null);
|
||||
|
||||
// ISS-010 (v0.18.0): one-time /api/airports fetch per page life — destination options for the
|
||||
// Relocate row, sorted by name so e2e selection is stable. Same promise-ref pattern as Map.tsx;
|
||||
// on failure airportOptions stays null and the Relocate button simply remains disabled.
|
||||
const [airportOptions, setAirportOptions] = useState<Array<{ name: string; icao: string }> | null>(null);
|
||||
const airportsPromiseRef = useRef<Promise<Array<{ name: string; icao: string }>> | null>(null);
|
||||
useEffect(() => {
|
||||
if (!airportsPromiseRef.current) {
|
||||
airportsPromiseRef.current = fetch('/api/airports', { cache: 'no-store' })
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(`GET /api/airports → HTTP ${res.status}`);
|
||||
return res.json() as Promise<GeoJsonFeatureCollection>;
|
||||
})
|
||||
.then((fc) =>
|
||||
fc.features
|
||||
.map((f) => ({ name: f.properties.name, icao: f.properties.icao }))
|
||||
.sort((a, b) => a.name.localeCompare(b.name, 'en')),
|
||||
);
|
||||
}
|
||||
void airportsPromiseRef.current.then(setAirportOptions).catch(() => {});
|
||||
}, []);
|
||||
// ISS-023 (v0.21.0): the Relocate row moved to the left pane — it belongs with everything else
|
||||
// about our planes, not in the market. Its state + /api/airports fetch went with it.
|
||||
|
||||
if (loading && !state) {
|
||||
return (
|
||||
@@ -168,12 +144,6 @@ export default function RightPane({ state, loading, applySnapshot, onHover, onEr
|
||||
state.planes.find((p) => p.id === refuelPlaneId && p.status === 'available' && p.fuel < p.maxFuel) ??
|
||||
refuelable[0] ??
|
||||
null;
|
||||
// ISS-010 (v0.18.0): Relocate — any GROUNDED plane can be repositioned to any airport via the
|
||||
// existing fly endpoint (pure repositioning, no package needed since v0.12.0).
|
||||
const relocatePlane = grounded.find((p) => p.id === relocatePlaneId) ?? grounded[0] ?? null;
|
||||
const relocateDest =
|
||||
airportOptions === null ? null : airportOptions.find((a) => a.icao === relocateDestIcao) ?? airportOptions[0] ?? null;
|
||||
|
||||
// Previews computed client-side from the snapshot's catalog + economy constants (v0.10.0).
|
||||
const sellEntry = sellPlane ? catalog.find((e) => e.model === sellPlane.model) : undefined;
|
||||
const sellPayout = sellEntry ? Math.round(sellEntry.price * state.sellFraction * 100) / 100 : null;
|
||||
@@ -329,47 +299,7 @@ export default function RightPane({ state, loading, applySnapshot, onHover, onEr
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ISS-010 (v0.18.0): relocate a grounded plane — the fly endpoint already allows pure
|
||||
repositioning, this row just exposes it for planes with nothing on board. */}
|
||||
<div className="market-row market-row-relocate">
|
||||
<select
|
||||
data-testid="relocate-plane"
|
||||
aria-label="Plane to relocate"
|
||||
value={relocatePlane?.id ?? ''}
|
||||
onChange={(e) => setRelocatePlaneId(e.target.value || null)}
|
||||
>
|
||||
{grounded.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{planeName(p)} — {p.locationIcao}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
data-testid="relocate-dest"
|
||||
aria-label="Relocation destination airport"
|
||||
value={relocateDest?.icao ?? ''}
|
||||
onChange={(e) => setRelocateDestIcao(e.target.value || null)}
|
||||
>
|
||||
{airportOptions?.map((a) => (
|
||||
<option key={a.icao} value={a.icao}>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="relocate-go"
|
||||
className="pane-btn"
|
||||
disabled={busy || relocatePlane === null || relocateDest === null}
|
||||
onClick={() => {
|
||||
if (relocatePlane && relocateDest) {
|
||||
void runAction(`/api/planes/${relocatePlane.id}/fly`, { destIcao: relocateDest.icao });
|
||||
}
|
||||
}}
|
||||
>
|
||||
Relocate
|
||||
</button>
|
||||
</div>
|
||||
{/* ISS-023 (v0.21.0): the Relocate row moved to the left pane's "Relocate" section. */}
|
||||
</section>
|
||||
|
||||
{actionError && <p data-testid="right-error" className="pane-error">{actionError}</p>}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { expect, test } from '@playwright/test';
|
||||
|
||||
// v0.18.0 — ISS-010: relocating a GROUNDED (empty) plane to any airport against the live origin.
|
||||
// Two UI surfaces drive the EXISTING POST /api/planes/{id}/fly endpoint (pure repositioning has
|
||||
// been allowed since v0.12.0 — no API change): the right-pane market row
|
||||
// been allowed since v0.12.0 — no API change): the left-pane Relocate section (ISS-023, v0.21.0)
|
||||
// (relocate-plane / relocate-dest / relocate-go) and the Relocate row inside the airport popup on
|
||||
// the map (airport-popup-plane / airport-popup-relocate). Marker-size regression is covered by
|
||||
// the existing e2e/map.spec.ts click tests, not here.
|
||||
@@ -90,7 +90,7 @@ test.afterEach(async ({ page }) => {
|
||||
|
||||
test.describe.configure({ mode: 'serial' }); // shared live DB — order matters within this file
|
||||
|
||||
test('market row relocates a grounded plane via the right pane', async ({ page }) => {
|
||||
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
|
||||
await flyHomeViaApi(page);
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "flight-ops",
|
||||
"version": "0.20.0",
|
||||
"version": "0.21.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3000",
|
||||
|
||||
Reference in New Issue
Block a user