From 80885c8ad7203c14f7feeba75780c3731287b139 Mon Sep 17 00:00:00 2001 From: Kamil Krzywda Date: Tue, 29 Sep 2026 00:56:28 +0000 Subject: [PATCH] =?UTF-8?q?test:=20e2e=20jump=20helper=20prefers=20undisco?= =?UTF-8?q?vered=20systems=20=E2=80=94=20fixes=20exhausted-territory=20pin?= =?UTF-8?q?g-pong?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- e2e/helpers.ts | 29 +++++++++++++++++++++++++++++ e2e/smoke.spec.ts | 7 ++----- e2e/state.spec.ts | 20 +++++--------------- 3 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 e2e/helpers.ts diff --git a/e2e/helpers.ts b/e2e/helpers.ts new file mode 100644 index 0000000..6afb625 --- /dev/null +++ b/e2e/helpers.ts @@ -0,0 +1,29 @@ +import { expect, type Page } from '@playwright/test'; + +// Shared e2e helpers — kept out of the specs so both files can reuse them. + +interface WorldSystemDto { id: number; name: string; x: number; y: number; discovered: boolean; current: boolean } +interface WorldState { systems: WorldSystemDto[]; edges: [number, number][]; turn: number } + +// Jump to an adjacent system, preferring one that is still undiscovered. Every jump into an +// undiscovered system generates 4 fresh neighbours (lib/game/world.ts), so fresh territory is +// always available — picking the FIRST edge instead ping-pongs between exhausted systems, because +// edges come back in insertion order and the oldest route out of a system is usually the one that +// led here. Falls back to any non-current adjacent system if none is undiscovered. +export async function jumpToFreshSystem(page: Page): Promise { + const world = (await (await page.request.get('/api/world')).json()) as WorldState; + const currentId = world.systems.find((s) => s.current)!.id; + + const adjacentIds = new Set(); + for (const [a, b] of world.edges) { + if (a === currentId) adjacentIds.add(b); + else if (b === currentId) adjacentIds.add(a); + } + const candidates = [...adjacentIds].map((id) => world.systems.find((s) => s.id === id)!); + const target = candidates.find((s) => !s.discovered) ?? candidates[0]; + expect(target).toBeDefined(); // every system has at least one route out (ring generation) + + const res = await page.request.post('/api/jump', { data: { systemId: target.id } }); + expect(res.status()).toBe(200); + return (await res.json()) as WorldState; +} diff --git a/e2e/smoke.spec.ts b/e2e/smoke.spec.ts index dc7403f..5ab2610 100644 --- a/e2e/smoke.spec.ts +++ b/e2e/smoke.spec.ts @@ -1,4 +1,5 @@ import { expect, test } from '@playwright/test'; +import { jumpToFreshSystem } from './helpers'; // Mirrors the /api/world + /api/system response shapes (lib/game/save.ts) — kept local so the spec stays self-contained. interface WorldSystemDto { id: number; name: string; x: number; y: number; discovered: boolean; current: boolean } @@ -218,11 +219,7 @@ test('SKILL TREE tab researches WEAPONS SYSTEMS and unlocks the gated WEAPONS bu if (unexplored.length > 0) { expect((await page.request.post('/api/explore', { data: { planetId: unexplored[0].id } })).status()).toBe(200); } else { - const world = (await (await page.request.get('/api/world')).json()) as WorldState; - const currentId = world.systems.find((s) => s.current)!.id; - const edge = world.edges.find(([a, b]) => a === currentId || b === currentId)!; - const targetId = edge[0] === currentId ? edge[1] : edge[0]; - expect((await page.request.post('/api/jump', { data: { systemId: targetId } })).status()).toBe(200); + await jumpToFreshSystem(page); // prefer undiscovered territory — first-edge ping-pongs between exhausted systems } ship = (await (await page.request.get('/api/ship')).json()) as ShipState; } diff --git a/e2e/state.spec.ts b/e2e/state.spec.ts index 4c7b94f..af817cf 100644 --- a/e2e/state.spec.ts +++ b/e2e/state.spec.ts @@ -1,6 +1,7 @@ import { execSync } from 'node:child_process'; import path from 'node:path'; import { expect, test } from '@playwright/test'; +import { jumpToFreshSystem } from './helpers'; // Repo root — docker-compose.yml lives here; `docker compose restart` runs on // this host (the test runner), browsers stay remote. CI points E2E_COMPOSE_ARGS at the @@ -8,9 +9,7 @@ import { expect, test } from '@playwright/test'; const repoRoot = path.join(__dirname, '..'); const composeArgs = process.env.E2E_COMPOSE_ARGS ?? ''; -// Mirrors the /api/world + /api/system response shapes (lib/game/save.ts) — kept local so the spec stays self-contained. -interface WorldSystemDto { id: number; name: string; x: number; y: number; discovered: boolean; current: boolean } -interface WorldState { systems: WorldSystemDto[]; edges: [number, number][]; turn: number } +// Mirrors the /api/system response shape (lib/game/save.ts) — kept local so the spec stays self-contained. interface PlanetDto { id: number; name: string; type: string; hazard: boolean; explored: boolean; parent: number | null } interface SystemState { systemId: number; systemName: string; planets: PlanetDto[]; inventory: { kind: string; name: string; qty: number }[]; turn: number } interface CrewDto { id: number; name: string; role: string; traitGood: string; traitBad: string; origin: string } @@ -28,22 +27,13 @@ test('app survives a container restart (stack + volume persistence)', async ({ p expect(before.version).toMatch(/^\d+\.\d+\.\d+$/); // Game-state baseline: jump once so there is real progress that must survive the restart. - const worldBefore = (await (await page.request.get('/api/world')).json()) as WorldState; - const currentId = worldBefore.systems.find((s) => s.current)!.id; - const edge = worldBefore.edges.find(([a, b]) => a === currentId || b === currentId)!; - const targetId = edge[0] === currentId ? edge[1] : edge[0]; - const jumpRes = await page.request.post('/api/jump', { data: { systemId: targetId } }); - expect(jumpRes.status()).toBe(200); + await jumpToFreshSystem(page); // Exploration baseline: scan one body so explored state + inventory must survive too. // (Earlier specs may have exhausted the current system, so jump until an unscanned body shows up.) let sysBefore = (await (await page.request.get('/api/system')).json()) as SystemState; for (let i = 0; !sysBefore.planets.some((p) => !p.explored) && i < 20; i++) { - const world = (await (await page.request.get('/api/world')).json()) as WorldState; - const curId = world.systems.find((s) => s.current)!.id; - const e = world.edges.find(([a, b]) => a === curId || b === curId)!; - const tId = e[0] === curId ? e[1] : e[0]; - expect((await page.request.post('/api/jump', { data: { systemId: tId } })).status()).toBe(200); + await jumpToFreshSystem(page); // prefer undiscovered territory — first-edge ping-pongs between exhausted systems sysBefore = (await (await page.request.get('/api/system')).json()) as SystemState; } const targetPlanet = sysBefore.planets.find((p) => !p.explored)!; @@ -84,7 +74,7 @@ test('app survives a container restart (stack + volume persistence)', async ({ p expect(after!.version).toBe(before.version); // same build, not a re-seeded stack // Game state survived: same turn counter and current system as before the restart. - const worldAfter = (await (await page.request.get('/api/world')).json()) as WorldState; + const worldAfter = (await (await page.request.get('/api/world')).json()) as { turn: number; systems: { id: number; current: boolean }[] }; expect(worldAfter.turn).toBe(sysExplored.turn); expect(worldAfter.systems.find((s) => s.current)?.id).toBe(sysExplored.systemId);