mirror of
https://github.com/GoodStartLabs/AI_Diplomacy.git
synced 2026-04-19 12:58:09 +00:00
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:
parent
e81f41fc57
commit
eebfba0a1b
15 changed files with 848 additions and 9 deletions
110
ai_animation/tests/e2e/README.md
Normal file
110
ai_animation/tests/e2e/README.md
Normal 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
|
||||
166
ai_animation/tests/e2e/game-playthrough.spec.ts
Normal file
166
ai_animation/tests/e2e/game-playthrough.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
281
ai_animation/tests/e2e/test-helpers.ts
Normal file
281
ai_animation/tests/e2e/test-helpers.ts
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue