Initial attempt at some tests that help us determine if the game has basic functionality

Signed-off-by: Tyler Marques <me@tylermarques.com>
This commit is contained in:
Tyler Marques 2025-05-30 20:17:33 -07:00
parent e81f41fc57
commit eebfba0a1b
No known key found for this signature in database
GPG key ID: CB99EDCF41D3016F
15 changed files with 848 additions and 9 deletions

View file

@ -0,0 +1,110 @@
# End-to-End Tests for AI Diplomacy Animation
This directory contains Playwright end-to-end tests for the AI Diplomacy Animation application.
## Test Overview
The tests verify that:
1. **Complete Game Playthrough** - Games play all the way through to completion, show victory messages, and transition to the next game
2. **Victory Message Timing** - The victory popup/message appears when games end and stays visible for an appropriate duration
3. **Next Game Transition** - After a victory message is shown, the application automatically loads and starts the next game
4. **Basic UI Functionality** - Core UI elements load and function correctly
5. **Manual Phase Navigation** - Users can manually advance through game phases
## Test Files
- `game-playthrough.spec.ts` - Main test suite containing all game flow tests
- `test-helpers.ts` - Utility functions for common test operations
- `README.md` - This documentation file
## Running Tests
### Prerequisites
Ensure the development server can start and that there are test games available in debug mode.
### Commands
```bash
# Run all e2e tests
npm run test:e2e
# Run tests with UI (visual test runner)
npm run test:e2e:ui
# Run tests in debug mode
npm run test:e2e:debug
# Run only the basic smoke test
npx playwright test "game loads and basic UI elements are present"
# Run only the complete playthrough test
npx playwright test "complete game playthrough"
```
## Test Configuration
Tests are configured to:
- Start the dev server automatically on `http://localhost:5173`
- Run across Chromium, Firefox, and WebKit browsers
- Have appropriate timeouts for game completion (up to 3 minutes for full playthroughs)
- Wait for the app to fully load before starting tests
## Key Test Scenarios
### 1. Complete Game Playthrough
- Starts automatic game playback
- Monitors for victory messages in the news banner
- Measures how long victory messages are visible
- Detects when the next game starts (via game ID changes or message replacement)
### 2. Manual Advancement
- Stops automatic playback
- Uses the "Next" button to advance through phases manually
- Provides more control over game progression for testing
### 3. Victory Message Detection
- Looks for patterns like "GAME OVER", "WINS", "VICTORIOUS", or trophy emojis (🏆)
- Monitors the `#news-banner-content` element for these messages
- Tracks timing from when victory is detected until the message disappears
## Important DOM Elements
The tests rely on these DOM element IDs:
- `#play-btn` - Play/Pause button
- `#next-btn` - Manual next phase button
- `#prev-btn` - Manual previous phase button
- `#news-banner-content` - News banner where victory messages appear
- `#phase-display` - Current phase/era display
- `#game-id-display` - Current game ID display
- `canvas` - Three.js rendering canvas
## Test Helpers
The `test-helpers.ts` file provides reusable functions:
- `waitForGameReady()` - Waits for app to load and game to be ready
- `startGamePlayback()` / `stopGamePlayback()` - Control game playback
- `measureVictoryTiming()` - Comprehensive victory detection and timing measurement
- `checkForVictoryMessage()` - Simple victory message detection
- `advanceGameManually()` - Manual game progression
- `getCurrentGameId()` - Get current game ID
- `isGamePlaying()` - Check if game is currently playing
## Expected Game Flow
1. Game loads with initial phase displayed
2. When "Play" is clicked, game begins automatic progression
3. Messages appear and disappear, units animate between phases
4. When the final phase is reached, a victory message appears in the news banner
5. The victory message should remain visible for some time
6. After the victory message, the next game should automatically load
7. The game ID should increment, and the new game should be ready to play
## Troubleshooting
- If tests fail to start, ensure the dev server starts correctly with `npm run dev`
- If games don't auto-load, check that debug mode is enabled in the configuration
- If victory messages aren't detected, verify the game files contain complete games that reach victory conditions
- For timing issues, check the `config.ts` file for debug mode and instant mode settings that affect display duration

View file

@ -0,0 +1,166 @@
import { test, expect } from '@playwright/test';
import {
waitForGameReady,
startGamePlayback,
stopGamePlayback,
measureVictoryTiming,
advanceGameManually,
checkForVictoryMessage,
isGamePlaying,
type ManualAdvancementResult
} from './test-helpers';
test.describe('Game Playthrough Tests', () => {
test.beforeEach(async ({ page, context }) => {
// Navigate to the app
await page.goto('/');
await context.addInitScript(() => window.isUnderTest = true);
// Wait for the game to be ready and loaded
await waitForGameReady(page);
});
test('complete game playthrough with victory screen and next game transition', async ({ page }) => {
// Start playing the game
await startGamePlayback(page);
// Wait for the game to complete and measure victory timing
const result = await measureVictoryTiming(page, 90000); // 1.5 minutes max wait
// Verify that we saw the victory message
expect(result.victoryDetected).toBe(true);
expect(result.victoryMessage).toBeTruthy();
// Log the results
console.log(`Victory message: "${result.victoryMessage}"`);
console.log(`Victory message was visible for ${result.displayDuration}ms`);
console.log(`Next game started: ${result.nextGameStarted}`);
console.log(`Game ID changed: ${result.gameIdChanged}`);
// Verify that victory message was displayed for a reasonable amount of time
expect(result.displayDuration).toBeGreaterThan(50); // At least 50ms (reduced for instant mode)
// The victory message should still be visible when we detect it
if (result.victoryDetected) {
const currentVictoryMessage = await checkForVictoryMessage(page);
if (currentVictoryMessage) {
await expect(page.locator('#news-banner-content')).toContainText(/GAME OVER|VICTORIOUS|🏆/);
}
}
});
test('victory popup stays visible for expected duration via manual advancement', async ({ page }) => {
// This test focuses on the timing by manually advancing through the game
test.setTimeout(60000); // 1 minute
// Stop automatic playback and advance manually for more control
if (await isGamePlaying(page)) {
await stopGamePlayback(page);
}
// Manually advance through the game to reach victory
const victoryReached = await advanceGameManually(page, 100, false);
if (victoryReached) {
// Now measure victory timing
const result = await measureVictoryTiming(page, 30000); // 30 seconds max wait
expect(result.victoryDetected).toBe(true);
expect(result.displayDuration).toBeGreaterThan(50); // At least 50ms (reduced for instant mode)
console.log(`Manual advancement: Victory message visible for ${result.displayDuration}ms`);
console.log(`Manual advancement: Next game started: ${result.nextGameStarted}`);
} else {
console.log('Could not reach victory through manual advancement - test skipped');
test.skip();
}
});
test('game loads and basic UI elements are present', async ({ page }) => {
// Basic smoke test to ensure the game loads properly
// Check that essential UI elements are present
await expect(page.locator('#play-btn')).toBeVisible();
await expect(page.locator('#prev-btn')).toBeVisible();
await expect(page.locator('#next-btn')).toBeVisible();
await expect(page.locator('canvas')).toBeVisible();
await expect(page.locator('#news-banner-content')).toBeVisible();
await expect(page.locator('#phase-display')).toBeVisible();
await expect(page.locator('#game-id-display')).toBeVisible();
// Check that the Three.js scene has loaded
const canvas = page.locator('canvas');
await expect(canvas).toHaveAttribute('width');
await expect(canvas).toHaveAttribute('height');
// Verify that we can start and stop playback
await startGamePlayback(page);
await stopGamePlayback(page);
});
test('manual phase advancement with two-power conversation detection', async ({ page }) => {
// Test comprehensive manual advancement with conversation tracking
test.setTimeout(120000); // 2 minutes
// Stop automatic playback to control advancement manually
if (await isGamePlaying(page)) {
await stopGamePlayback(page);
}
// Manually advance through the entire game while tracking conversations
const result = await advanceGameManually(page, 150, true) as ManualAdvancementResult;
// Log comprehensive results
console.log(`Manual advancement results:`);
console.log(`- Victory reached: ${result.victoryReached}`);
console.log(`- Phases advanced: ${result.phasesAdvanced}`);
console.log(`- Two-power conversations found: ${result.twoPowerConversationsFound}`);
console.log(`- Conversation phases: ${result.conversationPhases.join(', ')}`);
console.log(`- Final phase: ${result.finalPhaseName}`);
// Verify the game completed successfully
expect(result.victoryReached).toBe(true);
// Verify that we advanced through a reasonable number of phases
expect(result.phasesAdvanced).toBeGreaterThan(5);
// Two-power conversations should occur (though exact number depends on game data)
// Just verify the tracking worked - some games might have 0 conversations
expect(result.twoPowerConversationsFound).toBeGreaterThanOrEqual(0);
// If conversations were found, verify they were properly tracked
if (result.twoPowerConversationsFound > 0) {
expect(result.conversationPhases).toHaveLength(result.twoPowerConversationsFound);
console.log('Two-power conversations detected at phases:', result.conversationPhases);
} else {
console.log('No two-power conversations found in this game');
}
// After victory, check that victory message is present
const victoryMessage = await checkForVictoryMessage(page);
expect(victoryMessage).toBeTruthy();
});
test('game advances phases manually', async ({ page }) => {
// Test basic manual phase advancement
const initialPhaseText = await page.locator('#phase-display').textContent();
// Click next button
await page.click('#next-btn');
// Wait for phase to update
await page.waitForTimeout(500);
// Verify phase changed
const newPhaseText = await page.locator('#phase-display').textContent();
expect(newPhaseText).not.toBe(initialPhaseText);
// Test previous button
await page.click('#prev-btn');
await page.waitForTimeout(500);
const backPhaseText = await page.locator('#phase-display').textContent();
expect(backPhaseText).toBe(initialPhaseText);
});
});

View file

@ -0,0 +1,281 @@
import { Page, expect } from '@playwright/test';
/**
* Helper function to wait for the game to be ready and loaded
*/
export async function waitForGameReady(page: Page, timeout = 15000): Promise<void> {
// Wait for the app to fully load
await page.waitForLoadState('networkidle');
// Wait for Three.js scene to initialize
await page.waitForSelector('canvas', { timeout });
// Wait for essential UI elements to be present
await expect(page.locator('#play-btn')).toBeVisible({ timeout });
await expect(page.locator('#prev-btn')).toBeVisible({ timeout });
await expect(page.locator('#next-btn')).toBeVisible({ timeout });
// Ensure play button is enabled (indicating game is loaded)
await expect(page.locator('#play-btn')).toBeEnabled({ timeout });
}
/**
* Helper function to start game playback and verify it started
*/
export async function startGamePlayback(page: Page): Promise<void> {
await page.click('#play-btn');
await expect(page.locator('#play-btn')).toHaveText(/⏸ Pause/);
}
/**
* Helper function to stop game playback and verify it stopped
*/
export async function stopGamePlayback(page: Page): Promise<void> {
await page.click('#play-btn');
await expect(page.locator('#play-btn')).toHaveText(/▶ Play/);
}
/**
* Helper function to check if a victory message is present
*/
export async function checkForVictoryMessage(page: Page): Promise<string | null> {
try {
const newsText = await page.locator('#news-banner-content').textContent();
const victoryPattern = /GAME OVER.*WINS|VICTORIOUS|🏆.*WINS/i;
if (newsText && victoryPattern.test(newsText)) {
return newsText;
}
return null;
} catch {
return null;
}
}
/**
* Helper function to get current game ID
*/
export async function getCurrentGameId(page: Page): Promise<string | null> {
try {
return await page.locator('#game-id-display').textContent();
} catch {
return null;
}
}
/**
* Helper function to check if game is still playing
*/
export async function isGamePlaying(page: Page): Promise<boolean> {
try {
const playButtonText = await page.locator('#play-btn').textContent();
return playButtonText?.includes('⏸') || false;
} catch {
return false;
}
}
/**
* Interface for victory timing measurement result
*/
export interface VictoryTimingResult {
victoryDetected: boolean;
victoryMessage: string | null;
displayDuration: number;
nextGameStarted: boolean;
gameIdChanged: boolean;
}
/**
* Helper function to measure victory message timing and next game transition
*/
export async function measureVictoryTiming(
page: Page,
maxWaitTime = 60000
): Promise<VictoryTimingResult> {
const startTime = Date.now();
let victoryMessage: string | null = null;
let victoryDetected = false;
let victoryStartTime = 0;
let nextGameStarted = false;
let gameIdChanged = false;
let initialGameId: string | null = null;
// Get initial game ID
initialGameId = await getCurrentGameId(page);
while ((Date.now() - startTime) < maxWaitTime) {
// Check for victory message
if (!victoryDetected) {
victoryMessage = await checkForVictoryMessage(page);
if (victoryMessage) {
victoryDetected = true;
victoryStartTime = Date.now();
console.log('Victory message detected:', victoryMessage);
}
}
// If victory was detected, monitor for next game transition
if (victoryDetected) {
// Check if game ID changed
const currentGameId = await getCurrentGameId(page);
if (currentGameId && currentGameId !== initialGameId) {
gameIdChanged = true;
nextGameStarted = true;
console.log('Game ID changed from', initialGameId, 'to', currentGameId);
break;
}
// Check if victory message disappeared (indicating new game started)
const currentVictoryMessage = await checkForVictoryMessage(page);
if (!currentVictoryMessage) {
nextGameStarted = true;
console.log('Victory message disappeared, indicating new game started');
break;
}
}
await page.waitForTimeout(500);
}
const displayDuration = victoryDetected ? Date.now() - victoryStartTime : 0;
return {
victoryDetected,
victoryMessage,
displayDuration,
nextGameStarted,
gameIdChanged
};
}
/**
* Helper function to check if a two-power conversation is currently displayed
*/
export async function isTwoPowerConversationOpen(page: Page): Promise<boolean> {
try {
// Look for the dialogue overlay that appears when a two-power conversation is shown
const overlay = page.locator('.dialogue-overlay');
return await overlay.isVisible();
} catch {
return false;
}
}
/**
* Helper function to wait for a two-power conversation to close
*/
export async function waitForTwoPowerConversationToClose(page: Page, timeout = 5000): Promise<void> {
const startTime = Date.now();
while ((Date.now() - startTime) < timeout) {
const isOpen = await isTwoPowerConversationOpen(page);
if (!isOpen) {
return;
}
await page.waitForTimeout(100);
}
}
/**
* Helper function to get current phase name
*/
export async function getCurrentPhaseName(page: Page): Promise<string | null> {
try {
const phaseText = await page.locator('#phase-display').textContent();
// Extract phase name from "Era: {phaseName}" format
return phaseText?.replace('Era: ', '') || null;
} catch {
return null;
}
}
/**
* Interface for manual advancement result
*/
export interface ManualAdvancementResult {
victoryReached: boolean;
phasesAdvanced: number;
twoPowerConversationsFound: number;
conversationPhases: string[];
finalPhaseName: string | null;
}
/**
* Helper function to advance game manually by clicking next button with conversation tracking
*/
export async function advanceGameManually(
page: Page,
maxClicks = 50,
trackConversations = false
): Promise<boolean | ManualAdvancementResult> {
let clicks = 0;
let twoPowerConversationsFound = 0;
const conversationPhases: string[] = [];
while (clicks < maxClicks) {
try {
// Check for victory message first
const victoryMessage = await checkForVictoryMessage(page);
if (victoryMessage) {
if (trackConversations) {
return {
victoryReached: true,
phasesAdvanced: clicks,
twoPowerConversationsFound,
conversationPhases,
finalPhaseName: await getCurrentPhaseName(page)
};
}
return true; // Victory reached
}
// Get current phase name for tracking
const currentPhase = await getCurrentPhaseName(page);
// Check if we can advance
const nextButton = page.locator('#next-btn');
if (await nextButton.isEnabled()) {
// If tracking conversations, check if one opened after clicking next
if (trackConversations) {
await page.waitForTimeout(300); // Give time for conversation to appear
const conversationOpen = await isTwoPowerConversationOpen(page);
if (conversationOpen) {
twoPowerConversationsFound++;
if (currentPhase) {
conversationPhases.push(currentPhase);
}
console.log(`Two-power conversation detected at phase: ${currentPhase}`);
// Wait for conversation to close automatically or close it manually
await waitForTwoPowerConversationToClose(page, 35000); // 35 seconds max
}
await nextButton.click();
await page.waitForTimeout(100);
}
} else {
// Can't advance anymore, might be at end
break;
}
clicks++;
} catch (error) {
console.log('Error during manual advance:', error);
break;
}
}
if (trackConversations) {
return {
victoryReached: false,
phasesAdvanced: clicks,
twoPowerConversationsFound,
conversationPhases,
finalPhaseName: await getCurrentPhaseName(page)
};
}
return false; // Didn't reach victory
}