Auto-refuel on dispatch; remove manual refueling UI/API/action (ISS-082, v0.42.0)
CI / build + unit + e2e (pull_request) Successful in 17m25s
CI / build + unit + e2e (pull_request) Successful in 17m25s
This commit is contained in:
+19
-72
@@ -5,15 +5,13 @@ import { expect, test } from '@playwright/test';
|
||||
import { initialBearingDeg } from '../lib/geo';
|
||||
import { weatherFor } from '../lib/weather';
|
||||
import { windBurnFactor } from '../lib/sim/movement';
|
||||
// v0.38.0 (ISS-072) — reputation discounts: expectations computed from the LIVE snapshot's rep with the real helpers.
|
||||
import { FUEL_DISCOUNT_PER_REP, MAX_FUEL_DISCOUNT, repRate } from '../lib/game/catalog';
|
||||
|
||||
// v0.31.0 — ISS-055: gameplay with TWO planes in the fleet. The reported bug ("can't load cargo
|
||||
// if both planes are on the same airport") was UX-only: LoadControls' multi-plane path rendered a
|
||||
// greyed-out Load button until a plane was picked from the dropdown, while the single-plane path
|
||||
// is one-click. v0.31.0 preselects the first eligible plane (components/RightPane.tsx), and this
|
||||
// spec covers every two-plane flow: load (preselect + picker), parallel flights with the ISS-057
|
||||
// slow/resume semantics, refuel, relocate, per-plane queues (scoped testids) and sell.
|
||||
// slow/resume semantics, relocate, per-plane queues (scoped testids) and sell.
|
||||
//
|
||||
// Each test buys its own second plane via the UI (after a cash grant from the new
|
||||
// POST /api/test/add-cash hook — ATR €200k vs the €20k starting balance); beforeEach/afterEach
|
||||
@@ -171,6 +169,14 @@ async function addCash(page: Page, amount?: number): Promise<StateBody> {
|
||||
return (await res.json()) as StateBody;
|
||||
}
|
||||
|
||||
// Test hook (FLIGHT_TEST_HOOKS=1): top a plane's tank back to full — replaces the removed /refuel
|
||||
// endpoint (ISS-082). Reads maxFuel from the snapshot so no catalog mirror is needed.
|
||||
async function setFuelFull(page: Page, planeId: string): Promise<void> {
|
||||
const s = await getState(page);
|
||||
const p = s.planes.find((x) => x.id === planeId)!;
|
||||
expect((await page.request.post('/api/test/set-fuel', { data: { planeId, fuel: p.maxFuel } })).status()).toBe(200);
|
||||
}
|
||||
|
||||
// Buy a second ATR through the UI (Planes market tab) and return its id — discovered in /api/state
|
||||
// as the plane that isn't plane-starter (bought planes get random UUIDs, no stable name).
|
||||
async function buySecondPlaneViaUi(page: Page): Promise<string> {
|
||||
@@ -244,13 +250,10 @@ async function healPlaneHome(page: Page, planeId: string): Promise<void> {
|
||||
}
|
||||
if (p.locationIcao !== HUB) {
|
||||
// ISS-057: the landing above slowed the sim to 1× — setScale restores 1000× for the return leg.
|
||||
// ISS-082: dispatch auto-refuels, so a grounded plane can always start any full-tank-flyable
|
||||
// leg when cash allows — no top-up retry needed.
|
||||
await setScale(page, SCALE);
|
||||
let flyHome = await page.request.post(`/api/planes/${planeId}/fly`, { data: { destIcao: HUB } });
|
||||
if (flyHome.status() === 409) {
|
||||
// A long outbound leg can leave the tank too empty for the return — top up and retry.
|
||||
expect((await page.request.post(`/api/planes/${planeId}/refuel`)).status()).toBe(200);
|
||||
flyHome = await page.request.post(`/api/planes/${planeId}/fly`, { data: { destIcao: HUB } });
|
||||
}
|
||||
const flyHome = await page.request.post(`/api/planes/${planeId}/fly`, { data: { destIcao: HUB } });
|
||||
expect(flyHome.status()).toBe(200);
|
||||
await expect
|
||||
.poll(async () => ((await getState(page)).planes.find((x) => x.id === planeId)?.locationIcao ?? ''), {
|
||||
@@ -350,8 +353,8 @@ test('two planes fly in parallel; the last landing slows to 1× and the next dis
|
||||
const secondId = await buySecondPlaneViaUi(page);
|
||||
|
||||
// Full tanks for both — the starter's may be partial from an earlier run.
|
||||
expect((await page.request.post(`/api/planes/${STARTER}/refuel`)).status()).toBe(200);
|
||||
expect((await page.request.post(`/api/planes/${secondId}/refuel`)).status()).toBe(200);
|
||||
await setFuelFull(page, STARTER);
|
||||
await setFuelFull(page, secondId);
|
||||
|
||||
// Two very different leg lengths: the starter takes the SHORTEST hop, the second plane the longest
|
||||
// full-tank-flyable one — their landings are separated by hours of game time and can never fall on
|
||||
@@ -402,8 +405,8 @@ test('two planes fly in parallel; the last landing slows to 1× and the next dis
|
||||
expect(landed.packages.find((p) => p.id === pkgA.id)?.status).toBe('delivered');
|
||||
expect(landed.packages.find((p) => p.id === pkgB.id)?.status).toBe('delivered');
|
||||
|
||||
// The outbound to the range edge left the tank nearly empty — top up for the return leg.
|
||||
expect((await page.request.post(`/api/planes/${secondId}/refuel`)).status()).toBe(200);
|
||||
// The outbound to the range edge left the tank nearly empty — top it back up (ISS-082 hook).
|
||||
await setFuelFull(page, secondId);
|
||||
|
||||
// Resume semantics: with the starter still grounded, dispatching the second plane does NOT resume…
|
||||
const flyB2 = await page.request.post(`/api/planes/${secondId}/fly`, { data: { destIcao: HUB } });
|
||||
@@ -418,64 +421,8 @@ test('two planes fly in parallel; the last landing slows to 1× and the next dis
|
||||
expect(body.simState.timeScale, 'the last grounded plane resumes lastSelectedTimeScale').toBe(SCALE); // setScale posted 1000
|
||||
});
|
||||
|
||||
test('refuel section lists both planes; refueling one tops only its tank', async ({ page }) => {
|
||||
test.setTimeout(300_000); // self-heal + two short hops at scale 1000
|
||||
const secondId = await buySecondPlaneViaUi(page);
|
||||
|
||||
// Partial tanks: a short hop each, to different destinations.
|
||||
const [a, b] = await twoNearestToHub(page);
|
||||
expect((await page.request.post(`/api/planes/${STARTER}/refuel`)).status()).toBe(200);
|
||||
expect((await page.request.post(`/api/planes/${secondId}/refuel`)).status()).toBe(200);
|
||||
expect((await page.request.post(`/api/planes/${STARTER}/fly`, { data: { destIcao: a.icao } })).status()).toBe(200);
|
||||
expect((await page.request.post(`/api/planes/${secondId}/fly`, { data: { destIcao: b.icao } })).status()).toBe(200);
|
||||
|
||||
// Fast-forward to the first landing, restore speed (ISS-057 slows on any landing), then wait for both.
|
||||
await setScale(page, SCALE);
|
||||
await expect
|
||||
.poll(async () => ((await getState(page)).planes.some((p) => p.status === 'available')), {
|
||||
timeout: 240_000,
|
||||
message: 'waiting for the first plane to land',
|
||||
})
|
||||
.toBe(true);
|
||||
await setScale(page, SCALE);
|
||||
await expect
|
||||
.poll(async () => ((await getState(page)).planes.every((p) => p.status === 'available')), {
|
||||
timeout: 240_000,
|
||||
message: 'waiting for both planes to land',
|
||||
})
|
||||
.toBe(true);
|
||||
|
||||
const pre = await getState(page);
|
||||
const starter = pre.planes.find((p) => p.id === STARTER)!;
|
||||
const second = pre.planes.find((p) => p.id === secondId)!;
|
||||
expect(starter.fuel, 'the hop burned fuel').toBeLessThan(starter.maxFuel);
|
||||
expect(second.fuel, 'the hop burned fuel').toBeLessThan(second.maxFuel);
|
||||
|
||||
// The left-pane Refuel section lists BOTH grounded planes with partial tanks…
|
||||
const refuelSelect = page.getByTestId('market-refuel-plane');
|
||||
await expect(refuelSelect).toBeVisible({ timeout: 15_000 });
|
||||
await expect(refuelSelect.locator(`option[value="${STARTER}"]`)).toHaveCount(1);
|
||||
await expect(refuelSelect.locator(`option[value="${secondId}"]`)).toHaveCount(1);
|
||||
|
||||
// …select the SECOND plane — the preview is its exact fill-up cost (mirrors refuelPlane 'full'),
|
||||
// rep-scaled (v0.38.0, ISS-072) from the live snapshot's reputation.
|
||||
const fuelRate = repRate(pre.account.reputation, FUEL_DISCOUNT_PER_REP, MAX_FUEL_DISCOUNT);
|
||||
const expectedCost = Math.round((second.maxFuel - second.fuel) * pre.fuelPricePerL * (1 - fuelRate) * 100) / 100;
|
||||
await refuelSelect.selectOption(secondId);
|
||||
await expect(page.getByTestId('market-refuel-cost')).toContainText(eur(expectedCost));
|
||||
|
||||
await page.getByTestId('market-refuel').click();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const s = await getState(page);
|
||||
return Math.abs(s.account.cash - (pre.account.cash - expectedCost)) < 0.001;
|
||||
}, { timeout: 15_000, message: 'waiting for the refuel cost to land in /api/state' })
|
||||
.toBe(true);
|
||||
|
||||
const post = await getState(page);
|
||||
expect(post.planes.find((p) => p.id === secondId)!.fuel, 'that tank is full').toBe(second.maxFuel);
|
||||
expect(post.planes.find((p) => p.id === STARTER)!.fuel, 'the other tank is untouched').toBe(starter.fuel);
|
||||
});
|
||||
// ISS-082 (v0.42.0): the "refuel section lists both planes" test is gone with the manual refuel UI —
|
||||
// its absence is pinned in e2e/panes.spec.ts instead.
|
||||
|
||||
test('relocate section moves one plane without touching the other', async ({ page }) => {
|
||||
test.setTimeout(300_000); // self-heal + one short hop at scale 1000
|
||||
@@ -527,7 +474,7 @@ test('per-plane queues are independent; landing auto-chains the next queued leg'
|
||||
const [a, b] = await twoNearestToHub(page);
|
||||
|
||||
// Full tank for the starter — it flies both legs (EDDF→a then the auto-chained a→b).
|
||||
expect((await page.request.post(`/api/planes/${STARTER}/refuel`)).status()).toBe(200);
|
||||
await setFuelFull(page, STARTER);
|
||||
|
||||
// Seed one queued leg per plane via the API so BOTH on-board blocks render…
|
||||
expect((await page.request.post(`/api/planes/${STARTER}/queue`, { data: { destIcao: a.icao } })).status()).toBe(200);
|
||||
|
||||
Reference in New Issue
Block a user