diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000..3eb1314 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/ai_animation/.github/workflows/test.yml b/.github/workflows/test.yml similarity index 100% rename from ai_animation/.github/workflows/test.yml rename to .github/workflows/test.yml diff --git a/.gitignore b/.gitignore index da8f132..324b7b6 100644 --- a/.gitignore +++ b/.gitignore @@ -149,3 +149,7 @@ ai_diplomacy/alt_implementations game_moments.json game_moments_report.md /game_moments + +# Playwrite test results +**/test-results/ +**/playwright-report/ diff --git a/ai_animation/.gitignore b/ai_animation/.gitignore index 639ee0d..43da1db 100644 --- a/ai_animation/.gitignore +++ b/ai_animation/.gitignore @@ -27,3 +27,9 @@ dist-ssr .claude/ public/games/ + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/ai_animation/CLAUDE.md b/ai_animation/CLAUDE.md index c71d4ee..3df6d2d 100644 --- a/ai_animation/CLAUDE.md +++ b/ai_animation/CLAUDE.md @@ -8,28 +8,59 @@ ## Common Commands - `npm run dev` - Start the development server - `npm run build` - Build for production -- `npm run lint` - Run TypeScript linting +- `npm run lint` - Run TypeScript linting (note: many false negatives due to JS->TS conversion) - `npm test` - Run unit tests with Vitest - `npm run test:ui` - Run tests with UI interface +- `npm run test:e2e` - Run end-to-end tests with Playwright +- `npm run test:e2e:ui` - Run e2e tests with visual test runner +- `npm run test:e2e:debug` - Run e2e tests in debug mode ## Project Structure - `src/` - Source code - `main.ts` - Main entry point, handles game loop and UI events - `gameState.ts` - Central state management for the application - `config.ts` - Global configuration settings + - `phase.ts` - Phase progression logic, game completion, and victory handling + - `domElements.ts` - Core DOM element references and utilities - `domElements/` - DOM manipulation and UI components + - `chatWindows.ts` - Message display and news banner management + - `standingsBoard.ts` - Leaderboard and standings display + - `relationshipPopup.ts` - Power relationship visualization - `map/` - Map rendering and manipulation - `units/` - Unit creation and animation + - `components/` - Reusable UI components + - `rotatingDisplay.ts` - Dynamic information display + - `twoPowerConversation.ts` - Two-power conversation overlays + - `utils/` - Utility functions + - `powerNames.ts` - Power name display resolution - `types/` - TypeScript type definitions + - `debug/` - Debug tools and menu system +- `tests/` - Test files + - `e2e/` - End-to-end Playwright tests + - `integration/` - Integration tests (empty) + - `fixtures/` - Test fixtures (empty) +- `public/` - Static assets + - `games/` - Game data files (JSON format) + - `maps/` - Map data and SVG files + - `sounds/` - Audio files for speech + - `fonts/` - Three.js font files ## Game Flow -1. Load game data from JSON -2. Display initial phase +1. Load game data from JSON files located in `public/games/{gameId}/` + - `game.json` - Main game data with phases, units, orders, messages + - `moments.json` - High-interest moments and power model mappings +2. Display initial phase with units and supply centers 3. When Play is clicked: - Show messages sequentially, one word at a time - When all messages are displayed, animate unit movements - - When animations complete, show phase summary (if available) + - When animations complete, show phase summary (if available) via speech + - Check for high-interest moments (score ≥8.0) and display two-power conversations - Advance to next phase and repeat +4. Game completion: + - When final phase is reached, `displayFinalPhase()` is called + - Victory message appears in news banner with winner and supply center count + - `gameState.loadNextGame()` is called to transition to next game + - Game ID increments and new game loads automatically ## Power Name Display System The application now includes a dynamic power name display system: @@ -116,6 +147,81 @@ Phase names follow the format: `[Season][Year][Phase]` 4. Register the tool in the DebugMenu's `initTools()` method 5. Add any update functions to `updateTools()` method if needed +## End-to-End Testing +The project includes comprehensive Playwright tests to verify game functionality: + +### Test Coverage +- **Complete Game Playthrough**: Verifies games play through to victory and transition to next game +- **Victory Message Timing**: Ensures victory popups appear and stay visible for appropriate duration +- **Manual Phase Advancement with Conversation Detection**: Clicks through entire game manually while tracking two-power conversations +- **UI Element Loading**: Smoke tests for essential interface components +- **Manual Navigation**: Tests basic phase advancement controls + +### Key DOM Elements +Tests rely on these element IDs: +- `#play-btn` - Play/Pause button +- `#next-btn`, `#prev-btn` - Manual phase navigation +- `#news-banner-content` - Victory messages and news updates +- `#phase-display` - Current phase/era information +- `#game-id-display` - Current game identifier +- `canvas` - Three.js rendering surface +- `.dialogue-overlay` - Two-power conversation dialog overlay + +### Victory Detection +Tests monitor for victory patterns in the news banner: +- "GAME OVER.*WINS", "VICTORIOUS", "🏆.*WINS" +- Victory messages should appear when games complete +- Messages should remain visible until next game loads +- Game ID should increment when transitioning to next game + +### Two-Power Conversation Detection +Tests can detect and track two-power conversation overlays: +- Conversations appear when moments have interest scores ≥8.0 and involve ≥2 powers +- Tests monitor for `.dialogue-overlay` elements during phase advancement +- Conversations auto-close after timeout or can be manually closed +- Manual advancement test tracks which phases trigger conversations + +### Test Helpers +Located in `tests/e2e/test-helpers.ts`: +- `waitForGameReady()` - Ensures app loads completely and enables instant mode +- `measureVictoryTiming()` - Comprehensive victory detection and timing +- `advanceGameManually()` - Manual phase progression with optional two-power conversation tracking +- `isTwoPowerConversationOpen()` - Detects when two-power conversation dialogs are displayed +- `waitForTwoPowerConversationToClose()` - Waits for conversation dialogs to close +- `getCurrentPhaseName()` - Gets current phase name for tracking purposes + +### Running Tests +```bash +npm run test:e2e # Run all e2e tests +npm run test:e2e:ui # Visual test runner +npm run test:e2e:debug # Debug mode +``` + +### Configuration Notes +- Tests automatically enable instant mode (`VITE_INSTANT_MODE=true`) for faster execution +- Tests automatically enable debug mode (`VITE_DEBUG_MODE=true`) for auto-loading games +- Dev server starts automatically on `http://localhost:5173` +- Timeouts set appropriately (1-2 minutes for full playthroughs with instant mode) +- Cross-browser testing on Chromium, Firefox, and WebKit + +## Game State Management +Central state is managed in `gameState.ts` with key properties: +- `gameData` - Current game's JSON data +- `momentsData` - High-interest moments and metadata +- `phaseIndex` - Current phase being displayed +- `currentPower` - Player's assigned power +- `isPlaying` - Automatic playback state +- `messagesPlaying` - Message animation state +- `unitAnimations` - Active unit movement animations + +## Game Completion Flow +1. Final phase detection in `phase.ts:_setPhase()` +2. `displayFinalPhase()` calculates winner by supply center count +3. Victory message added to news banner via `addToNewsBanner()` +4. `gameState.loadNextGame()` increments game ID +5. New game file loaded from `public/games/{newGameId}/game.json` +6. Application resets to initial state with new game + ## Code Style Preferences - Use descriptive function and variable names - Add JSDoc comments for all exported functions diff --git a/ai_animation/package-lock.json b/ai_animation/package-lock.json index 23cdf75..4d673f6 100644 --- a/ai_animation/package-lock.json +++ b/ai_animation/package-lock.json @@ -15,6 +15,8 @@ "zod": "^3.24.2" }, "devDependencies": { + "@playwright/test": "^1.52.0", + "@types/node": "^22.15.29", "@vitest/ui": "^3.1.4", "jsdom": "^26.1.0", "typescript": "~5.7.2", @@ -589,6 +591,22 @@ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, + "node_modules/@playwright/test": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", + "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -875,6 +893,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.15.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", + "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@types/stats.js": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.3.tgz", @@ -1528,6 +1556,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.52.0.tgz", + "integrity": "sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.52.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.52.0.tgz", + "integrity": "sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -1823,6 +1898,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", diff --git a/ai_animation/package.json b/ai_animation/package.json index dd10e31..444f568 100644 --- a/ai_animation/package.json +++ b/ai_animation/package.json @@ -10,9 +10,14 @@ "preview": "vite preview --host 0.0.0.0", "test": "vitest", "test:ui": "vitest --ui", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui", + "test:e2e:debug": "playwright test --debug", "lint": "tsc --noEmit" }, "devDependencies": { + "@playwright/test": "^1.52.0", + "@types/node": "^22.15.29", "@vitest/ui": "^3.1.4", "jsdom": "^26.1.0", "typescript": "~5.7.2", diff --git a/ai_animation/playwright.config.ts b/ai_animation/playwright.config.ts new file mode 100644 index 0000000..a5772fd --- /dev/null +++ b/ai_animation/playwright.config.ts @@ -0,0 +1,46 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * @see https://playwright.dev/docs/test-configuration + */ +export default defineConfig({ + testDir: './tests/e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:5173', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npm run dev', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + env: { + INSTANT_MODE: 'true', // Enable instant mode for faster testing + }, + }, +}); diff --git a/ai_animation/src/config.ts b/ai_animation/src/config.ts index 5366614..a76cef7 100644 --- a/ai_animation/src/config.ts +++ b/ai_animation/src/config.ts @@ -19,11 +19,16 @@ export const config = { // Webhook URL for phase change notifications (optional) webhookUrl: import.meta.env.VITE_WEBHOOK_URL || '', + get isTestingMode(): boolean { + // have playwrite inject a marker saying that it's testing to brower + return import.meta.env.VITE_TESTING_MODE == 'True' || window.isUnderTest; + }, + _isTestingMode: false, // Whether instant mode is enabled (makes all animations instant) // Can be enabled via VITE_INSTANT_MODE env variable or debug menu get isInstantMode(): boolean { - return import.meta.env.VITE_INSTANT_MODE === 'true' || this._instantModeOverride; + return import.meta.env.VITE_INSTANT_MODE === 'True' || this._instantModeOverride; }, // Internal flag to allow runtime toggling of instant mode diff --git a/ai_animation/src/debug/debugMenu.ts b/ai_animation/src/debug/debugMenu.ts index 5a1c445..7ff5a3a 100644 --- a/ai_animation/src/debug/debugMenu.ts +++ b/ai_animation/src/debug/debugMenu.ts @@ -5,7 +5,7 @@ import { updateNextMomentDisplay, initNextMomentTool } from "./nextMoment"; import { initDebugProvinceHighlighting } from "./provinceHighlight"; -import { initInstantChatTool } from "./instantChat"; +import { initInstantChatTool as initInstantModeTool } from "./instantMode"; import { initSpeechToggleTool } from "./speechToggle"; import { initShowRandomMomentTool, updateMomentStatus } from "./showRandomMoment"; @@ -175,7 +175,7 @@ export class DebugMenu { private initTools(): void { initSpeechToggleTool(this); - initInstantChatTool(this); + initInstantModeTool(this); initNextMomentTool(this); initShowRandomMomentTool(this); initDebugProvinceHighlighting() diff --git a/ai_animation/src/debug/instantChat.ts b/ai_animation/src/debug/instantMode.ts similarity index 100% rename from ai_animation/src/debug/instantChat.ts rename to ai_animation/src/debug/instantMode.ts diff --git a/ai_animation/src/domElements/chatWindows.ts b/ai_animation/src/domElements/chatWindows.ts index e832cf2..ad4f383 100644 --- a/ai_animation/src/domElements/chatWindows.ts +++ b/ai_animation/src/domElements/chatWindows.ts @@ -4,7 +4,7 @@ import { config } from "../config"; import { advanceToNextPhase } from "../phase"; import { getPowerDisplayName, getAllPowerDisplayNames } from '../utils/powerNames'; import { PowerENUM } from '../types/map'; -import { isInstantChatEnabled } from '../debug/instantChat'; +import { isInstantChatEnabled } from '../debug/instantMode'; //TODO: Sometimes the LLMs use lists, and they don't work in the chats. The just appear as bullets within a single line. @@ -168,7 +168,7 @@ export function updateChatWindows(phase: any, stepMessages = false) { console.log(`Found ${relevantMessages.length} messages for player ${gameState.currentPower} in phase ${phase.name}`); } - if (!stepMessages || isInstantChatEnabled()) { + if (!stepMessages || config.isInstantMode) { // Normal mode or instant chat mode: show all messages at once relevantMessages.forEach(msg => { const isNew = addMessageToChat(msg, phase.name); @@ -761,6 +761,7 @@ function playRandomSoundEffect() { // Create an