Files
Flight-Ops/e2e/panes.spec.ts
T

144 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 — location asserted against /api/state truth, because a
// human player (or an earlier spec) may have moved it anywhere by now.
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');
const truth = (await getState(page)).planes.find((p) => p.id === 'plane-starter') as unknown as {
status: string;
locationName: string;
destName: string | null;
};
if (truth.status === 'flying' && truth.destName) {
await expect(row).toContainText(`${truth.locationName} → ${truth.destName}`); // in-flight leg
} else {
await expect(row).toContainText(truth.locationName);
}
// 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('slider commit keeps the chosen scale on screen until confirmed — no snap-back (ISS-001)', async ({ page }) => {
test.setTimeout(60_000);
await page.goto('/');
const slider = page.getByTestId('time-scale-slider');
await expect(slider).toBeVisible({ timeout: 15_000 });
// Commit to max via keyboard (input + change → immediate commitNow POST)…
await slider.focus();
await page.keyboard.press('End');
// …and sample the displayed value rapidly across the whole commit→confirm window. With the
// ISS-001 fix the optimistic value stays until a snapshot confirms it, so EVERY sample must
// be '64×' — never snapping back to the previous scale for one poll cycle (the old bug).
const seen: string[] = [];
for (let i = 0; i < 30; i++) {
seen.push(((await page.getByTestId('time-scale-value').textContent()) ?? '').trim());
await new Promise((r) => setTimeout(r, 25));
}
expect(
seen.every((v) => v === '64×'),
`displayed scale must stay 64× during the commit window (saw: ${[...new Set(seen)].join(', ')})`,
).toBe(true);
// …and the server confirms it.
await expect.poll(async () => (await getState(page)).simState.timeScale, { timeout: 10_000 }).toBe(64);
// Restore to min for later tests (afterEach restores scale anyway).
await page.keyboard.press('Home');
await expect.poll(async () => (await getState(page)).simState.timeScale, { timeout: 10_000 }).toBe(1);
});
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');
});