e2e: ship action via UI (bounded scan until affordable) + ship state survives container restart
This commit is contained in:
@@ -9,6 +9,10 @@ interface SystemState { systemId: number; systemName: string; planets: PlanetDto
|
||||
interface FoundItem { kind: string; name: string; qty: number }
|
||||
interface ExploreResult extends SystemState { outcome: FoundItem[]; crewFound: unknown }
|
||||
interface CrewDto { id: number; name: string; role: string; traitGood: string; traitBad: string; origin: string }
|
||||
interface ShipSystemDto { id: number; type: string; level: number; capacity: number; built: boolean }
|
||||
interface ShipState { systems: ShipSystemDto[]; inventory: InventoryItemDto[]; turn: number }
|
||||
|
||||
const totalQty = (items: InventoryItemDto[], kind: string) => items.filter((i) => i.kind === kind).reduce((sum, i) => sum + i.qty, 0);
|
||||
|
||||
test('landing page loads with the SpaceWanderer title', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
@@ -139,6 +143,67 @@ 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 }) => {
|
||||
// Scan bodies until something is affordable: an upgrade costs `level` resources (at least one),
|
||||
// a build costs three parts. Jumping keeps it going if a whole system is exhausted.
|
||||
let ship = (await (await page.request.get('/api/ship')).json()) as ShipState;
|
||||
const upgradeTarget = () => ship.systems.find((s) => s.built && s.level < 5 && totalQty(ship.inventory, 'resource') >= s.level);
|
||||
const buildTarget = () => (totalQty(ship.inventory, 'part') >= 3 ? ship.systems.find((s) => !s.built) : undefined);
|
||||
|
||||
let iterations = 0;
|
||||
while (!upgradeTarget() && !buildTarget() && iterations < 60) {
|
||||
iterations++;
|
||||
const sys = (await (await page.request.get('/api/system')).json()) as SystemState;
|
||||
const unexplored = sys.planets.filter((p) => !p.explored);
|
||||
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);
|
||||
}
|
||||
ship = (await (await page.request.get('/api/ship')).json()) as ShipState;
|
||||
}
|
||||
|
||||
const upgrade = upgradeTarget();
|
||||
const build = !upgrade ? buildTarget() : undefined;
|
||||
expect(upgrade ?? build).toBeDefined(); // the bounded scan always finds something to spend on
|
||||
|
||||
await page.goto('/');
|
||||
await page.getByRole('tab', { name: 'SHIP SYSTEMS' }).click();
|
||||
|
||||
if (upgrade) {
|
||||
const card = page.getByRole('heading', { name: upgrade.type }).locator('xpath=ancestor::div[3]');
|
||||
await expect(card.getByRole('button', { name: 'UPGRADE' })).toBeEnabled();
|
||||
await card.getByRole('button', { name: 'UPGRADE' }).click();
|
||||
|
||||
const after = (await (await page.request.get('/api/ship')).json()) as ShipState;
|
||||
const row = after.systems.find((s) => s.id === upgrade.id)!;
|
||||
expect(row.level).toBe(upgrade.level + 1);
|
||||
expect(after.turn).toBe(ship.turn + 1);
|
||||
|
||||
// The card badge flipped to the new level and the left column mirrors it.
|
||||
await expect(card.getByText(`LV ${upgrade.level + 1}`)).toBeVisible();
|
||||
await expect(page.getByText(`LV ${row.level} · CAP ${row.capacity}`)).toBeVisible();
|
||||
} else {
|
||||
const target = build!;
|
||||
const card = page.getByRole('heading', { name: target.type }).locator('xpath=ancestor::div[3]');
|
||||
await expect(card.getByRole('button', { name: 'BUILD NEW' })).toBeEnabled();
|
||||
await card.getByRole('button', { name: 'BUILD NEW' }).click();
|
||||
|
||||
const after = (await (await page.request.get('/api/ship')).json()) as ShipState;
|
||||
const row = after.systems.find((s) => s.id === target.id)!;
|
||||
expect(row.built).toBe(true);
|
||||
expect({ level: row.level, capacity: row.capacity }).toEqual({ level: 1, capacity: 2 });
|
||||
|
||||
// The card badge flipped OFFLINE → LV 1 and the left column's online count went up.
|
||||
await expect(card.getByText('LV 1')).toBeVisible();
|
||||
await expect(page.getByText(`${after.systems.filter((s) => s.built).length} ONLINE`)).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);
|
||||
|
||||
@@ -14,6 +14,8 @@ interface WorldState { systems: WorldSystemDto[]; edges: [number, number][]; tur
|
||||
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 }
|
||||
interface ShipSystemDto { id: number; type: string; level: number; capacity: number; built: boolean }
|
||||
interface ShipState { systems: ShipSystemDto[]; inventory: { kind: string; name: string; qty: number }[]; turn: number }
|
||||
|
||||
test('app survives a container restart (stack + volume persistence)', async ({ page }) => {
|
||||
test.setTimeout(180_000);
|
||||
@@ -52,6 +54,10 @@ test('app survives a container restart (stack + volume persistence)', async ({ p
|
||||
const crewBefore = (await (await page.request.get('/api/crew')).json()) as CrewDto[];
|
||||
expect(crewBefore.length).toBeGreaterThanOrEqual(2); // starting crew is always seeded
|
||||
|
||||
// Ship baseline: the systems (levels/capacities) + cargo hold must survive too.
|
||||
const shipBefore = (await (await page.request.get('/api/ship')).json()) as ShipState;
|
||||
expect(shipBefore.systems.length).toBeGreaterThan(0); // starting ship is always seeded
|
||||
|
||||
// Restart the app container mid-run — the named DB volume must survive it.
|
||||
execSync(`docker compose ${composeArgs} restart space-wanderer`, { cwd: repoRoot, stdio: 'pipe' });
|
||||
|
||||
@@ -86,6 +92,10 @@ test('app survives a container restart (stack + volume persistence)', async ({ p
|
||||
const crewAfter = (await (await page.request.get('/api/crew')).json()) as CrewDto[];
|
||||
expect(crewAfter).toEqual(crewBefore);
|
||||
|
||||
// Ship state survived too: same systems, levels/capacities and cargo hold.
|
||||
const shipAfter = (await (await page.request.get('/api/ship')).json()) as ShipState;
|
||||
expect(shipAfter).toEqual(shipBefore);
|
||||
|
||||
// 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