test(e2e): event log entries + restart persistence
smoke: jump (shared helper) + scan, then assert both events are in GET /api/events with the newer one first, and that the EVENT LOG terminal renders both T<turn> // <MESSAGE> lines. Idempotent across retries — asserts on this run's own entries by turn/message, never absolute counts. state: the latest five log lines come back identical (ids included) after the mid-run container restart.
This commit is contained in:
@@ -13,6 +13,7 @@ interface CrewDto { id: number; name: string; role: string; traitGood: string; t
|
||||
interface ShipSystemDto { id: number; type: string; level: number; capacity: number; built: boolean }
|
||||
interface ShipState { systems: ShipSystemDto[]; inventory: InventoryItemDto[]; turn: number }
|
||||
interface MissionDto { id: number; type: string; description: string; targetSystemId: number | null; conditionKind: string | null; progressTarget: number; progress: number; rewardKind: string; rewardQty: number; state: string }
|
||||
interface EventDto { id: number; turn: number; type: string; message: string; createdAt: string }
|
||||
|
||||
const totalQty = (items: InventoryItemDto[], kind: string) => items.filter((i) => i.kind === kind).reduce((sum, i) => sum + i.qty, 0);
|
||||
|
||||
@@ -362,6 +363,40 @@ test('MISSIONS tab accepts a contract and completing it grants the reward', asyn
|
||||
expect(totalQty(shipAfter.inventory, picked.rewardKind)).toBeGreaterThanOrEqual(baseline + picked.rewardQty);
|
||||
});
|
||||
|
||||
test('EVENT LOG tab shows real game events, newest first', async ({ page }) => {
|
||||
// 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.
|
||||
const afterJump = await jumpToFreshSystem(page);
|
||||
const jumpedName = afterJump.systems.find((s) => s.current)!.name;
|
||||
const jumpTurn = afterJump.turn;
|
||||
|
||||
// Scan one body — if the current system is exhausted (leftover from a previous attempt), hop again first.
|
||||
let sys = (await (await page.request.get('/api/system')).json()) as SystemState;
|
||||
if (!sys.planets.some((p) => !p.explored)) {
|
||||
await jumpToFreshSystem(page); // fresh systems always have unscanned bodies
|
||||
sys = (await (await page.request.get('/api/system')).json()) as SystemState;
|
||||
}
|
||||
const planet = sys.planets.find((p) => !p.explored)!;
|
||||
const exploreRes = await page.request.post('/api/explore', { data: { planetId: planet.id } });
|
||||
expect(exploreRes.status()).toBe(200);
|
||||
const explored = (await exploreRes.json()) as ExploreResult;
|
||||
|
||||
// API: this run's jump + scan are in the log, and the newer one (the scan) comes first.
|
||||
const events = (await (await page.request.get('/api/events')).json()) as EventDto[];
|
||||
const exploreEvent = events.find((e) => e.turn === explored.turn && e.type === 'explore');
|
||||
expect(exploreEvent?.message).toMatch(new RegExp(`^Explored ${planet.name} — found`));
|
||||
const jumpEvent = events.find((e) => e.type === 'jump' && e.message === `Jumped to ${jumpedName}`);
|
||||
expect(jumpEvent).toBeDefined(); // system names are unique, so the message pins this run's jump
|
||||
expect(events.indexOf(exploreEvent!)).toBeLessThan(events.indexOf(jumpEvent!));
|
||||
|
||||
// UI: the terminal renders both lines (T<turn> // <MESSAGE>, uppercased).
|
||||
await page.goto('/');
|
||||
await page.getByRole('tab', { name: 'EVENT LOG' }).click();
|
||||
const panel = page.getByRole('tabpanel');
|
||||
await expect(panel.getByText(new RegExp(`T${explored.turn} // EXPLORED ${planet.name.toUpperCase()}`))).toBeVisible();
|
||||
await expect(panel.getByText(new RegExp(`T${jumpTurn} // JUMPED TO ${jumpedName.toUpperCase()}`))).toBeVisible();
|
||||
});
|
||||
|
||||
test('GET /api/version reports the app version as JSON', async ({ page }) => {
|
||||
const res = await page.request.get('/api/version');
|
||||
expect(res.status()).toBe(200);
|
||||
|
||||
@@ -17,6 +17,7 @@ interface ShipSystemDto { id: number; type: string; level: number; capacity: num
|
||||
interface ShipState { systems: ShipSystemDto[]; inventory: { kind: string; name: string; qty: number }[]; turn: number }
|
||||
interface TechDto { id: number; slug: string; name: string; costKind: string; costQty: number; prereqs: string[]; effect: string; state: string; missingPrereqs: string[] }
|
||||
interface MissionDto { id: number; type: string; description: string; targetSystemId: number | null; conditionKind: string | null; progressTarget: number; progress: number; rewardKind: string; rewardQty: number; state: string }
|
||||
interface EventDto { id: number; turn: number; type: string; message: string; createdAt: string }
|
||||
|
||||
test('app survives a container restart (stack + volume persistence)', async ({ page }) => {
|
||||
test.setTimeout(180_000);
|
||||
@@ -59,6 +60,11 @@ test('app survives a container restart (stack + volume persistence)', async ({ p
|
||||
const missionsBefore = (await (await page.request.get('/api/missions')).json()) as MissionDto[];
|
||||
expect(missionsBefore.length).toBeGreaterThan(0); // the board is always seeded on new game
|
||||
|
||||
// Event log baseline: append-only, and nothing writes between this snapshot and the restart — so the
|
||||
// latest lines must come back byte-identical (ids included) after it.
|
||||
const eventsBefore = (await (await page.request.get('/api/events?limit=5')).json()) as EventDto[];
|
||||
expect(eventsBefore.length).toBeGreaterThan(0);
|
||||
|
||||
// Restart the app container mid-run — the named DB volume must survive it.
|
||||
execSync(`docker compose ${composeArgs} restart space-wanderer`, { cwd: repoRoot, stdio: 'pipe' });
|
||||
|
||||
@@ -105,6 +111,10 @@ test('app survives a container restart (stack + volume persistence)', async ({ p
|
||||
const missionsAfter = (await (await page.request.get('/api/missions')).json()) as MissionDto[];
|
||||
expect(missionsAfter).toEqual(missionsBefore);
|
||||
|
||||
// Event log survived too: same latest lines, in the same order.
|
||||
const eventsAfter = (await (await page.request.get('/api/events?limit=5')).json()) as EventDto[];
|
||||
expect(eventsAfter).toEqual(eventsBefore);
|
||||
|
||||
// The page itself serves again with the landing title.
|
||||
await page.goto('/');
|
||||
await expect(page.getByRole('heading', { name: 'SpaceWanderer' })).toBeVisible();
|
||||
|
||||
Reference in New Issue
Block a user