103 lines
4.5 KiB
TypeScript
103 lines
4.5 KiB
TypeScript
import { test, expect, type Page } from '@playwright/test';
|
||
|
||
// v0.9.0 — single-screen 3-pane shell + left Assets pane against the live origin.
|
||
// The panes render from the shell's single /api/state poll (lib/useGameState.ts); time
|
||
// controls POST to /api/sim and refetch immediately, so UI assertions settle within a tick.
|
||
|
||
interface SimState {
|
||
clockMs: number;
|
||
timeScale: number;
|
||
paused: boolean;
|
||
}
|
||
|
||
interface StateBody {
|
||
account: { cash: number; reputation: number };
|
||
simState: SimState;
|
||
planes: Array<Record<string, unknown>>;
|
||
packages: Array<Record<string, unknown>>;
|
||
}
|
||
|
||
async function getState(page: Page): Promise<StateBody> {
|
||
const res = await page.request.get('/api/state');
|
||
expect(res.status()).toBe(200);
|
||
return (await res.json()) as StateBody;
|
||
}
|
||
|
||
// Leave the live instance in a sane state for the other specs (scale 1, running) — same
|
||
// pattern as e2e/sim.spec.ts.
|
||
test.afterEach(async ({ page }) => {
|
||
const res = await page.request.post('/api/sim', { data: { timeScale: 1, paused: false } });
|
||
expect(res.status()).toBe(200);
|
||
});
|
||
|
||
test('left pane renders wallet, reputation and the starter fleet at Frankfurt', async ({ page }) => {
|
||
await page.goto('/');
|
||
await expect(page.getByTestId('left-pane')).toBeVisible();
|
||
|
||
// Wallet: cash > 0 (formatted EUR) — poll until the first snapshot lands.
|
||
const cashEl = page.getByTestId('wallet-cash');
|
||
await expect(cashEl).toBeVisible({ timeout: 15_000 });
|
||
const cashText = (await cashEl.textContent()) ?? '';
|
||
expect(Number(cashText.replace(/[^0-9.]/g, ''))).toBeGreaterThan(0);
|
||
|
||
// Reputation present and numeric.
|
||
await expect(page.getByTestId('reputation')).toBeVisible();
|
||
expect(Number((await page.getByTestId('reputation').textContent()) ?? 'NaN')).toBeGreaterThanOrEqual(0);
|
||
|
||
// Fleet: the starter ATR 72-600 at Frankfurt (EDDF).
|
||
const row = page.getByTestId('fleet-list').locator('.plane-row').first();
|
||
await expect(row, 'starter plane row appears once the first snapshot lands').toBeVisible({ timeout: 15_000 });
|
||
await expect(row).toContainText('ATR 72-600');
|
||
await expect(row).toContainText('Frankfurt');
|
||
|
||
// v0.12.0 loadout: "n/capacity pkgs" (capacity from the snapshot catalog — ATR 72-600 = 6).
|
||
const loadout = page.getByTestId('plane-loadout-plane-starter');
|
||
await expect(loadout).toBeVisible();
|
||
expect((await loadout.textContent()) ?? '').toMatch(/^\d+\/6 pkgs$/);
|
||
|
||
// Sim clock renders (UTC wall-clock of the game time).
|
||
await expect(page.getByTestId('sim-clock')).toContainText(/UTC/);
|
||
});
|
||
|
||
test('time controls: pause freezes the sim and the power-of-two slider sets timeScale', async ({ page }) => {
|
||
await page.goto('/');
|
||
const pauseBtn = page.getByTestId('pause-toggle');
|
||
await expect(pauseBtn).toBeVisible({ timeout: 15_000 });
|
||
|
||
// Pause via the UI button — assert against the authoritative /api/state…
|
||
await pauseBtn.click();
|
||
await expect.poll(async () => (await getState(page)).simState.paused, { timeout: 10_000 }).toBe(true);
|
||
// …and in the pane itself (the refetch after POST flips the button to Resume).
|
||
await expect(pauseBtn).toHaveText('Resume');
|
||
|
||
// Scale via the power-of-two slider. End jumps to max (t=1 → 64×): the label updates
|
||
// optimistically on input, and release flushes the debounced POST immediately.
|
||
const slider = page.getByTestId('time-scale-slider');
|
||
await slider.focus();
|
||
await page.keyboard.press('End');
|
||
await expect(page.getByTestId('time-scale-value')).toHaveText('64×', { timeout: 10_000 });
|
||
await expect.poll(async () => (await getState(page)).simState.timeScale, { timeout: 10_000 }).toBe(64);
|
||
|
||
// Home jumps to min (t=0 → 1×).
|
||
await page.keyboard.press('Home');
|
||
await expect(page.getByTestId('time-scale-value')).toHaveText('1×', { timeout: 10_000 });
|
||
await expect.poll(async () => (await getState(page)).simState.timeScale, { timeout: 10_000 }).toBe(1);
|
||
|
||
// Game clock shows seconds: YYYY-MM-DD HH:MM:SS UTC.
|
||
await expect(page.getByTestId('sim-clock')).toContainText(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC/);
|
||
|
||
// Restore the live instance to scale 1 + unpaused (afterEach enforces this too).
|
||
const restore = await page.request.post('/api/sim', { data: { timeScale: 1, paused: false } });
|
||
expect(restore.status()).toBe(200);
|
||
const after = await getState(page);
|
||
expect(after.simState.timeScale).toBe(1);
|
||
expect(after.simState.paused).toBe(false);
|
||
});
|
||
|
||
test('right pane placeholder exists with the Jobs & Market heading', async ({ page }) => {
|
||
await page.goto('/');
|
||
const right = page.getByTestId('right-pane');
|
||
await expect(right).toBeVisible();
|
||
await expect(right).toContainText('Jobs & Market');
|
||
});
|