129 lines
6.8 KiB
TypeScript
129 lines
6.8 KiB
TypeScript
import { expect, test } from '@playwright/test';
|
|
|
|
// 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 }
|
|
interface PlanetDto { id: number; name: string; type: string; hazard: boolean; explored: boolean; parent: number | null }
|
|
interface InventoryItemDto { kind: string; name: string; qty: number }
|
|
interface SystemState { systemId: number; systemName: string; planets: PlanetDto[]; inventory: InventoryItemDto[]; turn: number }
|
|
interface FoundItem { kind: string; name: string; qty: number }
|
|
interface ExploreResult extends SystemState { outcome: FoundItem[] }
|
|
|
|
test('landing page loads with the SpaceWanderer title', async ({ page }) => {
|
|
await page.goto('/');
|
|
await expect(page.getByRole('heading', { name: 'SpaceWanderer' })).toBeVisible();
|
|
});
|
|
|
|
test('game template tabs render (scificn-ui component layer wired in)', async ({ page }) => {
|
|
await page.goto('/');
|
|
// Stat card + footer badge prove the layout shell; Radix Tabs.Trigger renders role="tab".
|
|
await expect(page.getByText('NEXT ENCOUNTER')).toBeVisible();
|
|
await expect(page.getByRole('tab', { name: 'WORLD MAP' })).toBeVisible();
|
|
await expect(page.getByRole('tab', { name: 'CREW' })).toBeVisible();
|
|
await expect(page.getByRole('tab', { name: 'SKILL TREE' })).toBeVisible();
|
|
await expect(page.getByText('SYS ONLINE')).toBeVisible();
|
|
});
|
|
|
|
test('tabs switch panels client-side (event log terminal)', async ({ page }) => {
|
|
await page.goto('/');
|
|
await page.getByRole('tab', { name: 'EVENT LOG' }).click();
|
|
await expect(page.getByText('SHIP LOG // PLACEHOLDER FEED')).toBeVisible();
|
|
});
|
|
|
|
test('world map shows the generated starting system and its neighbours', async ({ page }) => {
|
|
await page.goto('/');
|
|
// Live chart title carries the turn counter; a fresh save sits at the start with six jump routes.
|
|
await expect(page.getByText(/GALACTIC CHART \/\/ JUMP ROUTES — TURN \d+/)).toBeVisible();
|
|
const jumpButtons = page.getByRole('button', { name: /\/\/ JUMP$/ });
|
|
await expect(jumpButtons.first()).toBeVisible();
|
|
await expect(jumpButtons).toHaveCount(6);
|
|
});
|
|
|
|
test('jumping to an adjacent system updates the world state and turn counter', async ({ page }) => {
|
|
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)!;
|
|
const targetId = edge[0] === currentId ? edge[1] : edge[0];
|
|
const targetName = before.systems.find((s) => s.id === targetId)!.name;
|
|
|
|
await page.goto('/');
|
|
await page.getByRole('button', { name: `${targetName.toUpperCase()} // JUMP` }).click();
|
|
|
|
const after = (await (await page.request.get('/api/world')).json()) as WorldState;
|
|
expect(after.turn).toBe(before.turn + 1);
|
|
expect(after.systems.find((s) => s.current)?.id).toBe(targetId);
|
|
});
|
|
|
|
test('local system shows the current system bodies with staggered moons and explore buttons', async ({ page }) => {
|
|
const sys = (await (await page.request.get('/api/system')).json()) as SystemState;
|
|
|
|
await page.goto('/');
|
|
await page.getByRole('tab', { name: 'LOCAL SYSTEM' }).click();
|
|
|
|
// Header carries the real system name + turn counter.
|
|
await expect(page.getByText(`EXPLORABLE WORLDS // ${sys.systemName.toUpperCase()} — TURN ${sys.turn}`)).toBeVisible();
|
|
// One EXPLORE button per unexplored body (planets and moons alike).
|
|
const exploreButtons = page.getByRole('button', { name: /\/\/ EXPLORE$/ });
|
|
await expect(exploreButtons.first()).toBeVisible();
|
|
await expect(exploreButtons).toHaveCount(sys.planets.filter((p) => !p.explored).length);
|
|
});
|
|
|
|
test('exploring a body flips its row state, bumps the turn and shows the scan result', async ({ page }) => {
|
|
const before = (await (await page.request.get('/api/system')).json()) as SystemState;
|
|
const target = before.planets.find((p) => !p.explored)!;
|
|
|
|
await page.goto('/');
|
|
await page.getByRole('tab', { name: 'LOCAL SYSTEM' }).click();
|
|
await page.getByRole('button', { name: `${target.name.toUpperCase()} // EXPLORE` }).click();
|
|
|
|
const after = (await (await page.request.get('/api/system')).json()) as SystemState;
|
|
expect(after.turn).toBe(before.turn + 1);
|
|
expect(after.planets.find((p) => p.id === target.id)?.explored).toBe(true);
|
|
|
|
// UI reflects it: the row badge flipped to SCANNED and its EXPLORE button is gone.
|
|
await expect(page.getByRole('button', { name: `${target.name.toUpperCase()} // EXPLORE` })).toHaveCount(0);
|
|
await expect(page.getByText(/SCAN RESULT \/\/ /)).toBeVisible();
|
|
});
|
|
|
|
test('exploration yields finds that accumulate in the inventory', async ({ page }) => {
|
|
// 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
|
|
let iterations = 0;
|
|
while (found.size === 0 && 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) {
|
|
const res = await page.request.post('/api/explore', { data: { planetId: unexplored[0].id } });
|
|
expect(res.status()).toBe(200);
|
|
for (const item of ((await res.json()) as ExploreResult).outcome) {
|
|
found.set(`${item.kind}|${item.name}`, item.qty);
|
|
}
|
|
} else {
|
|
// Current system fully scanned — jump somewhere and keep scanning.
|
|
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);
|
|
}
|
|
}
|
|
expect(found.size).toBeGreaterThan(0);
|
|
|
|
// Every find landed in the inventory with at least that qty.
|
|
const sys = (await (await page.request.get('/api/system')).json()) as SystemState;
|
|
for (const [key, qty] of found) {
|
|
const [kind, name] = key.split('|');
|
|
expect(sys.inventory.find((i) => i.kind === kind && i.name === name)?.qty).toBeGreaterThanOrEqual(qty);
|
|
}
|
|
});
|
|
|
|
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);
|
|
expect(res.headers()['content-type']).toContain('application/json');
|
|
const body = (await res.json()) as { version: string };
|
|
expect(body.version).toMatch(/^\d+\.\d+\.\d+$/);
|
|
});
|