import { test, expect, type Page } from '@playwright/test'; /** * Fully deterministic end-to-end test. The Playwright webServer boots the real * stack (Vite :5173 → Express :4321) with LLM_STUB=1, so the backend uses the * deterministic turn simulator in server/stub.ts: the player nation's `military` * stat is set to (current + 5, clamped) each turn and exactly one event is * emitted (action → military, chat → diplomatic, jump → political). There is no * live LLM and no flakiness — every assertion below is an exact, predicted value. * * Beyond the core simulation loop it also asserts the polished command-center * UI: the dossier's animated stat bars, the map legend (generated from the * region map), and the new event cards (type icon + label + colored outcome * badge). The map itself is a canvas, so we assert on its DOM legend, not pixels. */ test.describe('History — deterministic simulation (LLM_STUB=1)', () => { test('scenario → nation → action → jump, with predicted stat changes', async ({ page }) => { await page.goto('/'); // Home: the scenario grid loads with all 5 presets. const presetCards = page.getByTestId('preset-card'); await expect(presetCards.first()).toBeVisible(); await expect(presetCards).toHaveCount(5); // Start the first scenario (World War II, 1939). await presetCards.first().getByRole('button', { name: 'Play' }).click(); // Nation select: the world brief is shown, then we lead the United Kingdom. await expect(page.getByTestId('world-background')).toBeVisible(); await page .getByTestId('nation-card-uk') .getByRole('button', { name: 'Play as United Kingdom' }) .click(); // Game screen mounts: map, command deck, and event feed all present; turn 1. await expect(page.getByTestId('world-map')).toBeVisible(); await expect(page.getByTestId('action-panel')).toBeVisible(); await expect(page.getByTestId('event-feed-panel')).toBeVisible(); await expect(page.getByTestId('turn-count')).toHaveText('1'); // The dossier shows the player's four stat bars (value-colored, animated). await expect(page.getByTestId('stat-military')).toBeVisible(); await expect(page.getByTestId('stat-economy')).toBeVisible(); await expect(page.getByTestId('stat-stability')).toBeVisible(); await expect(page.getByTestId('stat-population')).toBeVisible(); // The player (UK) starts at military 80, with no events yet. const military = page.getByTestId('stat-military'); await expect(military).toHaveText('80'); await expect(page.getByTestId('event-item')).toHaveCount(0); // The map legend is auto-generated from the region map: all 7 nations of the // scenario own at least one region, including the player (United Kingdom). await expect(page.getByTestId('map-legend-item')).toHaveCount(7, { timeout: 15000 }); await expect(page.getByTestId('map-legend-item').filter({ hasText: 'United Kingdom' })).toBeVisible(); // Take an action: the stub bumps UK military to 85 and emits a military event. await page.getByTestId('action-textarea').fill('Mobilize the home guard and fortify Dover'); await page.getByTestId('action-submit').click(); await expect(page.getByTestId('turn-count')).toHaveText('2', { timeout: 20000 }); await expect(military).toHaveText('85', { timeout: 20000 }); await expect(page.getByTestId('event-item')).toHaveCount(1); await expect( page.getByTestId('event-feed').getByText('Mobilize the home guard and fortify Dover').first(), ).toBeVisible(); // The new event card carries its type (icon + label) and a colored outcome badge. await expect(page.getByTestId('event-item').first().getByTestId('event-type-icon')).toBeVisible(); await expect(page.getByTestId('event-item').first().getByTestId('event-type')).toHaveText('military'); await expect(page.getByTestId('event-item').first().getByTestId('event-outcome')).toHaveText('success'); // Jump forward (no action): a "world moves on" political event, military → 90. await page.getByTestId('jump-forward').click(); await expect(page.getByTestId('turn-count')).toHaveText('3', { timeout: 20000 }); await expect(military).toHaveText('90', { timeout: 20000 }); await expect(page.getByTestId('event-item')).toHaveCount(2); await expect(page.getByTestId('event-feed').getByText('The world moves on').first()).toBeVisible(); // Newest first: the jump event (political / neutral) sits on top, and the // action event (military / success) is second — both with their outcome badges. await expect(page.getByTestId('event-item').first().getByTestId('event-type')).toHaveText('political'); await expect(page.getByTestId('event-item').first().getByTestId('event-outcome')).toHaveText('neutral'); await expect(page.getByTestId('event-item').nth(1).getByTestId('event-outcome')).toHaveText('success'); }); }); /** * Save / resume across a page refresh. The active game's id is persisted in * localStorage, so a reload (or the home screen's "Resume last game" button) * must drop the user straight back into the same game — same turn, same stats, * same map — rather than resetting to the home screen. Deterministic under the * stub (UK military starts at 80 and +5 per turn). */ test.describe('History — save / resume across a page refresh', () => { async function createUkGameAtTurn2(page: Page): Promise { await page.goto('/'); await page.getByTestId('preset-card').first().waitFor({ timeout: 15000 }); await page.getByTestId('preset-card').first().getByRole('button', { name: 'Play' }).click(); await page .getByTestId('nation-card-uk') .getByRole('button', { name: 'Play as United Kingdom' }) .click(); // Game screen mounts at turn 1 (UK military starts at 80). await expect(page.getByTestId('world-map')).toBeVisible(); await expect(page.getByTestId('turn-count')).toHaveText('1'); await expect(page.getByTestId('stat-military')).toHaveText('80'); // One action: the stub bumps UK military to 85 and advances to turn 2. await page.getByTestId('action-textarea').fill('Reinforce the Atlantic convoys'); await page.getByTestId('action-submit').click(); await expect(page.getByTestId('turn-count')).toHaveText('2', { timeout: 20000 }); await expect(page.getByTestId('stat-military')).toHaveText('85', { timeout: 20000 }); } test('auto-resumes into the same game after a page reload', async ({ page }) => { await createUkGameAtTurn2(page); // Reload: the persisted active game id must restore the game — not home. await page.reload(); await expect(page.getByTestId('world-map')).toBeVisible(); await expect(page.getByTestId('turn-count')).toHaveText('2', { timeout: 20000 }); await expect(page.getByTestId('stat-military')).toHaveText('85', { timeout: 20000 }); // The player's identity is restored in the header… await expect(page.getByTestId('player-nation')).toHaveText('United Kingdom'); // …and the map legend is regenerated from the persisted region map. await expect(page.getByTestId('map-legend-item').first()).toBeVisible({ timeout: 15000 }); }); test('explicitly resumes a saved game from the home screen', async ({ page }) => { await createUkGameAtTurn2(page); // Back to the home screen: the "Resume last game" affordance is present. await page.getByTestId('back-to-scenarios').click(); await expect(page.getByTestId('resume-last')).toBeVisible(); // Resuming drops back into the same game, at the saved turn. await page.getByTestId('resume-last').click(); await expect(page.getByTestId('world-map')).toBeVisible(); await expect(page.getByTestId('turn-count')).toHaveText('2', { timeout: 20000 }); await expect(page.getByTestId('stat-military')).toHaveText('85', { timeout: 20000 }); await expect(page.getByTestId('player-nation')).toHaveText('United Kingdom'); }); });