164 lines
7.7 KiB
TypeScript
164 lines
7.7 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.
|
||
|
||
const SCALE = 1000; // fastest sim speed — top of the v0.14.0 ladder (same convention as e2e/sim.spec.ts)
|
||
|
||
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.27.0 loadout: "X.Xt / Y.Yt" tonnage (used/max payload from the snapshot catalog — ATR 72-600 = 4.8t).
|
||
const loadout = page.getByTestId('plane-loadout-plane-starter');
|
||
await expect(loadout).toBeVisible();
|
||
expect((await loadout.textContent()) ?? '').toMatch(/^\d+\.\dt \/ \d+\.\dt$/);
|
||
|
||
// Sim clock renders (UTC wall-clock of the game time).
|
||
await expect(page.getByTestId('sim-clock')).toContainText(/UTC/);
|
||
|
||
// ISS-021 (v0.20.0): the On board section moved to the left pane (between Fleet and Time) —
|
||
// its heading renders here…
|
||
await expect(page.getByTestId('left-pane').getByRole('heading', { name: 'On board', exact: true })).toBeVisible();
|
||
// …and no longer in the right pane.
|
||
await expect(page.getByTestId('right-pane').getByRole('heading', { name: 'On board', exact: true })).toHaveCount(0);
|
||
|
||
// ISS-035 (v0.23.0): the Refuel-to-full row moved to the left pane next to Relocate — its section
|
||
// heading and button render here…
|
||
await expect(page.getByTestId('left-pane').getByRole('heading', { name: 'Refuel', exact: true })).toBeVisible();
|
||
await expect(page.getByTestId('left-pane').getByTestId('market-refuel')).toBeVisible();
|
||
// …and neither the heading nor the row remains in the right pane.
|
||
await expect(page.getByTestId('right-pane').getByRole('heading', { name: 'Refuel', exact: true })).toHaveCount(0);
|
||
await expect(page.getByTestId('right-pane').getByTestId('market-refuel')).toHaveCount(0);
|
||
});
|
||
|
||
test('sim clock ticks smoothly between polls — seconds advance without waiting for a snapshot (ISS-007)', async ({ page }) => {
|
||
await page.goto('/');
|
||
const clock = page.getByTestId('sim-clock');
|
||
await expect(clock).toBeVisible({ timeout: 15_000 });
|
||
|
||
// Fastest scale so game seconds move visibly inside the sampling window.
|
||
const post = await page.request.post('/api/sim', { data: { timeScale: SCALE, paused: false } });
|
||
expect(post.status()).toBe(200);
|
||
|
||
// "YYYY-MM-DD HH:MM:SS UTC" → epoch ms (UTC). While running there is no "(paused)" suffix.
|
||
const sample = async () => {
|
||
const text = ((await clock.textContent()) ?? '').trim();
|
||
expect(text, `clock format: ${text}`).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC$/);
|
||
return Date.parse(text.replace(' ', 'T').replace(' UTC', 'Z'));
|
||
};
|
||
|
||
// Three samples ~1.3 s apart real time: at scale 1000 each gap is ≈1300 game seconds, so the
|
||
// displayed clock must strictly advance between CONSECUTIVE samples — with the old poll-only
|
||
// rendering a sample pair could straddle zero snapshot updates and show no change at all.
|
||
const t0 = await sample();
|
||
await new Promise((r) => setTimeout(r, 1_300));
|
||
const t1 = await sample();
|
||
await new Promise((r) => setTimeout(r, 1_300));
|
||
const t2 = await sample();
|
||
|
||
expect(t1).toBeGreaterThan(t0);
|
||
expect(t2).toBeGreaterThan(t1);
|
||
// Sanity: ~2.6 s real × 1000 ≈ 2.6 M game ms — generous bounds keep slow CI boxes safe while
|
||
// still catching a frozen or wildly mis-scaled clock.
|
||
const total = t2 - t0;
|
||
expect(total).toBeGreaterThanOrEqual(1_000_000);
|
||
expect(total).toBeLessThanOrEqual(6_000_000);
|
||
});
|
||
|
||
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 '1000×' — 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 === '1000×'),
|
||
`displayed scale must stay 1000× 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(1000);
|
||
|
||
// 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');
|
||
|
||
// v0.28.0 (ISS-047): the Jobs / Planes market tab bar renders once a snapshot lands; Jobs is default-active.
|
||
await expect(page.getByTestId('tab-jobs'), 'the Jobs tab button appears').toBeVisible({ timeout: 15_000 });
|
||
await expect(page.getByTestId('tab-planes-market')).toBeVisible();
|
||
await expect(page.getByTestId('tab-jobs')).toHaveAttribute('aria-selected', 'true');
|
||
});
|