fix(e2e): task-10 review fixes — Nth tick, retry-safe preambles, queue-DTO shape (#44)
CI / build + unit + e2e (pull_request) Failing after 34m10s
CI / build + unit + e2e (pull_request) Failing after 34m10s
This commit is contained in:
+18
-15
@@ -1,5 +1,5 @@
|
||||
import { expect, test, type Page } from '@playwright/test';
|
||||
import { jumpToFreshSystem, resolveAction } from './helpers';
|
||||
import { drainQueue, jumpToFreshSystem, resolveAction } from './helpers';
|
||||
|
||||
// Mirrors the /api/world + /api/system response shapes (lib/game/save.ts) — kept local so the spec stays self-contained.
|
||||
// Since Task 6 both snapshots also carry the save's task queue (ACTIVE block first, then PENDING).
|
||||
@@ -172,6 +172,7 @@ test('world map shows the generated starting system and its neighbours', async (
|
||||
});
|
||||
|
||||
test('jumping to an adjacent system updates the world state and turn counter', async ({ page }) => {
|
||||
await drainQueue(page); // retry-safety: a failed earlier attempt may have left a row queued (an in-transit JUMP would 409 every new queue)
|
||||
const before = (await (await page.request.get('/api/world')).json()) as WorldState;
|
||||
const currentId = before.systems.find((s) => s.current)!.id;
|
||||
const edge = before.edges.find(([a, b]) => a === currentId || b === currentId)!;
|
||||
@@ -199,6 +200,7 @@ test('jumping to an adjacent system updates the world state and turn counter', a
|
||||
});
|
||||
|
||||
test('local system shows the new current system after a jump, without a page reload (#30)', async ({ page }) => {
|
||||
await drainQueue(page); // retry-safety: a failed earlier attempt may have left a row queued (an in-transit JUMP would 409 every new queue)
|
||||
const before = (await (await page.request.get('/api/world')).json()) as WorldState;
|
||||
const currentId = before.systems.find((s) => s.current)!.id;
|
||||
const edge = before.edges.find(([a, b]) => a === currentId || b === currentId)!;
|
||||
@@ -247,6 +249,7 @@ test('local system shows the current system bodies with staggered moons and expl
|
||||
});
|
||||
|
||||
test('exploring a body flips its row state, bumps the turn and shows the scan result', async ({ page }) => {
|
||||
await drainQueue(page); // retry-safety: a failed earlier attempt may have left a row queued (an in-transit JUMP would 409 every new queue)
|
||||
const before = (await (await page.request.get('/api/system')).json()) as SystemState;
|
||||
const target = before.planets.find((p) => !p.explored)!;
|
||||
|
||||
@@ -280,6 +283,7 @@ test('exploring a body flips its row state, bumps the turn and shows the scan re
|
||||
});
|
||||
|
||||
test('exploration yields finds that accumulate in the inventory', async ({ page }) => {
|
||||
await drainQueue(page); // retry-safety: a failed earlier attempt may have left a row queued (an in-transit JUMP would 409 every new queue)
|
||||
// Scan bodies until one yields something (most scans do; jumping keeps it going if a whole
|
||||
// system is exhausted). P(no find across this many scans) ≈ 0.21^30 — negligible.
|
||||
const found = new Map<string, number>(); // kind|name -> qty
|
||||
@@ -338,6 +342,7 @@ test('CREW tab renders real roster cards, each showing both traits and an origin
|
||||
});
|
||||
|
||||
test('SHIP SYSTEMS tab performs an affordable action and reflects it in the card and left column', async ({ page }) => {
|
||||
await drainQueue(page); // retry-safety: a failed earlier attempt may have left a row queued (an in-transit JUMP would 409 every new queue)
|
||||
// Scan bodies until something is affordable: an upgrade costs `level` resources (at least one),
|
||||
// a build costs three parts. Every scan/jump is queued with a default crew and ticked to
|
||||
// completion (resolveAction) — the hold only changes once the task resolves on a tick. Jumping
|
||||
@@ -433,6 +438,7 @@ test('SHIP SYSTEMS tab performs an affordable action and reflects it in the card
|
||||
});
|
||||
|
||||
test('SKILL TREE tab researches WEAPONS SYSTEMS and unlocks the gated WEAPONS build', async ({ page }) => {
|
||||
await drainQueue(page); // retry-safety: a failed earlier attempt may have left a row queued (an in-transit JUMP would 409 every new queue)
|
||||
// Research costs two parts, building WEAPONS three — scan until the hold has five. Every
|
||||
// scan/jump is queued with a default crew and ticked to completion (resolveAction). Parts drop on
|
||||
// ~30% of scans, so P(<5 in 80 scans) is negligible; jumping keeps it going if a system exhausts.
|
||||
@@ -508,6 +514,7 @@ test('SKILL TREE tab researches WEAPONS SYSTEMS and unlocks the gated WEAPONS bu
|
||||
});
|
||||
|
||||
test('NAVIGATION AI research unlocks two-hop jumps on the world map (#35)', async ({ page }) => {
|
||||
await drainQueue(page); // retry-safety: a failed earlier attempt may have left a row queued (an in-transit JUMP would 409 every new queue)
|
||||
// Research costs two resources (70% of scans drop one), so a short scan loop makes it affordable;
|
||||
// jumping keeps it going if a system exhausts. Idempotent: a retry finds it already RESEARCHED.
|
||||
let techs = (await (await page.request.get('/api/tech')).json()) as { id: number; slug: string; state: string }[];
|
||||
@@ -601,6 +608,7 @@ test('NAVIGATION AI research unlocks two-hop jumps on the world map (#35)', asyn
|
||||
});
|
||||
|
||||
test('MISSIONS tab accepts a contract and completing it grants the reward', async ({ page }) => {
|
||||
await drainQueue(page); // retry-safety: a failed earlier attempt may have left a row queued (an in-transit JUMP would 409 every new queue)
|
||||
const board = (await (await page.request.get('/api/missions')).json()) as MissionDto[];
|
||||
expect(board.filter((m) => m.state === 'AVAILABLE').length).toBeGreaterThanOrEqual(3); // the board is always topped up
|
||||
|
||||
@@ -684,6 +692,7 @@ test('MISSIONS tab accepts a contract and completing it grants the reward', asyn
|
||||
});
|
||||
|
||||
test('EVENT LOG tab shows real game events, newest first', async ({ page }) => {
|
||||
await drainQueue(page); // retry-safety: a failed earlier attempt may have left a row queued (an in-transit JUMP would 409 every new queue)
|
||||
// Idempotent across retries sharing a DB: assert on THIS run's own entries (identified by turn and
|
||||
// message), never on absolute counts — earlier attempts leave their events behind.
|
||||
// The jump is queued + ticked to completion: the event it writes carries the tick it RESOLVED
|
||||
@@ -748,18 +757,6 @@ const tickOnce = async (page: Page) => {
|
||||
expect((await page.request.post('/api/turn')).status()).toBe(200);
|
||||
};
|
||||
|
||||
// Tick the save's task queue until it is empty. Free skips are legal 200s and every tick
|
||||
// decrements every ACTIVE row (PENDING rows auto-start), so a non-empty queue always shrinks —
|
||||
// the bound only fails a pathological state, not a slow one.
|
||||
async function drainQueue(page: Page): Promise<void> {
|
||||
for (let ticks = 0; ; ticks++) {
|
||||
const { tasks } = (await (await page.request.get('/api/crew')).json()) as { crew: CrewDto[]; tasks: QueueTaskDto[] };
|
||||
if (tasks.length === 0) return;
|
||||
expect(ticks).toBeLessThan(60);
|
||||
await tickOnce(page);
|
||||
}
|
||||
}
|
||||
|
||||
test('queueing a one-person JUMP leaves the ship in the origin until its Nth tick (#44)', async ({ page }) => {
|
||||
await drainQueue(page);
|
||||
const world = (await (await page.request.get('/api/world')).json()) as WorldStateQ;
|
||||
@@ -799,13 +796,16 @@ test('a JUMP and a disjoint-crew EXPLORE run in parallel and resolve at their ow
|
||||
const { crew } = (await (await page.request.get('/api/crew')).json()) as { crew: CrewDto[]; tasks: QueueTaskDto[] };
|
||||
expect(crew.length).toBeGreaterThanOrEqual(2); // disjoint one-person teams
|
||||
|
||||
// The explore's body: an unexplored planet in the current system (jump first if it is exhausted).
|
||||
// The explore's body: an unexplored planet in the current system. A jump's fallback target can
|
||||
// be a fully-explored system (jumpToFreshSystem prefers undiscovered, not unexplored), so loop
|
||||
// like the other preambles instead of a single if.
|
||||
let sys = (await (await page.request.get('/api/system')).json()) as SystemStateQ;
|
||||
if (!sys.planets.some((p) => !p.explored)) {
|
||||
for (let guard = 0; !sys.planets.some((p) => !p.explored) && guard < 10; guard++) {
|
||||
await jumpToFreshSystem(page);
|
||||
sys = (await (await page.request.get('/api/system')).json()) as SystemStateQ;
|
||||
}
|
||||
const planet = sys.planets.find((p) => !p.explored)!;
|
||||
expect(planet).toBeDefined(); // the world keeps producing fresh unscanned bodies
|
||||
|
||||
const world = (await (await page.request.get('/api/world')).json()) as WorldStateQ;
|
||||
const currentId = world.systems.find((s) => s.current)!.id;
|
||||
@@ -874,6 +874,9 @@ test('a JUMP and a disjoint-crew EXPLORE run in parallel and resolve at their ow
|
||||
expect(midWorld.tasks.find((t) => t.type === 'EXPLORE')!.remaining).toBe(nExp - k);
|
||||
expect(midWorld.tasks.find((t) => t.type === 'JUMP')!.remaining).toBe(nExp - k);
|
||||
}
|
||||
// The Nth tick resolves both — the loop above covered ticks 1..N-1 only (mirrors how the other
|
||||
// scenarios end on their resolving tick).
|
||||
await tickOnce(page);
|
||||
}
|
||||
|
||||
// Both resolved at their own durations: the jump landed, each event is stamped on its task's
|
||||
|
||||
Reference in New Issue
Block a user