Version 2.0.0: Animation fixes, timing improvements, and E2E test suite
Animation fixes: - Fix held card positioning bug (was appearing at bottom of page) - Fix discard pile blank/white flash on turn transitions - Fix blank card at round end by skipping animations during round_over/game_over - Set card content before triggering flip animation to prevent flash - Center suit symbol on 10 cards Timing improvements: - Reduce post-discard delay from 700ms to 500ms - Reduce post-swap delay from 1800ms to 1000ms - Speed up swap flip animation from 1150ms to 550ms - Reduce CPU initial thinking delay from 150-250ms to 80-150ms - Pause now happens after swap completes (showing result) instead of before E2E test suite: - Add Playwright-based test bot that plays full games - State parser extracts game state from DOM for validation - AI brain ports decision logic for automated play - Freeze detector monitors for UI hangs - Visual validator checks CSS states - Full game, stress, and visual test specs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
253
tests/e2e/specs/full-game.spec.ts
Normal file
253
tests/e2e/specs/full-game.spec.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Full game playthrough tests
|
||||
* Tests complete game sessions with the bot
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { GolfBot } from '../bot/golf-bot';
|
||||
import { FreezeDetector } from '../health/freeze-detector';
|
||||
import { ScreenshotValidator } from '../visual/screenshot-validator';
|
||||
|
||||
test.describe('Full Game Playthrough', () => {
|
||||
test('bot completes 3-hole game against CPU', async ({ page }) => {
|
||||
test.setTimeout(180000); // 3 minutes for 3-hole game
|
||||
|
||||
const bot = new GolfBot(page);
|
||||
const freezeDetector = new FreezeDetector(page);
|
||||
const validator = new ScreenshotValidator(page);
|
||||
|
||||
// Navigate to game
|
||||
await bot.goto();
|
||||
|
||||
// Create game and add CPU
|
||||
const roomCode = await bot.createGame('TestBot');
|
||||
expect(roomCode).toHaveLength(4);
|
||||
|
||||
await bot.addCPU('Sofia');
|
||||
|
||||
// Take screenshot of waiting room
|
||||
await validator.capture('waiting-room');
|
||||
|
||||
// Start game with 3 holes
|
||||
await bot.startGame({ holes: 3 });
|
||||
|
||||
// Verify game started
|
||||
const phase = await bot.getGamePhase();
|
||||
expect(['initial_flip', 'playing']).toContain(phase);
|
||||
|
||||
// Take screenshot of game start
|
||||
await validator.capture('game-start', phase);
|
||||
|
||||
// Play through the entire game
|
||||
const result = await bot.playGame(3);
|
||||
|
||||
// Take final screenshot
|
||||
await validator.capture('game-over', 'game_over');
|
||||
|
||||
// Verify game completed
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.rounds).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Check for errors
|
||||
const errors = bot.getConsoleErrors();
|
||||
expect(errors).toHaveLength(0);
|
||||
|
||||
// Verify no freezes occurred
|
||||
const health = await freezeDetector.runHealthCheck();
|
||||
expect(health.healthy).toBe(true);
|
||||
});
|
||||
|
||||
test('bot completes 9-hole game against CPU', async ({ page }) => {
|
||||
test.setTimeout(900000); // 15 minutes for 9-hole game
|
||||
|
||||
const bot = new GolfBot(page);
|
||||
const freezeDetector = new FreezeDetector(page);
|
||||
|
||||
await bot.goto();
|
||||
|
||||
const roomCode = await bot.createGame('TestBot');
|
||||
await bot.addCPU('Marcus');
|
||||
|
||||
await bot.startGame({ holes: 9 });
|
||||
|
||||
// Play full game
|
||||
const result = await bot.playGame(9);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.rounds).toBe(9);
|
||||
|
||||
// Verify game ended properly
|
||||
const finalPhase = await bot.getGamePhase();
|
||||
expect(finalPhase).toBe('game_over');
|
||||
|
||||
// Check health
|
||||
const health = await freezeDetector.runHealthCheck();
|
||||
expect(health.healthy).toBe(true);
|
||||
});
|
||||
|
||||
test('bot handles initial flip phase correctly', async ({ page }) => {
|
||||
const bot = new GolfBot(page);
|
||||
const validator = new ScreenshotValidator(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1, initialFlips: 2 });
|
||||
|
||||
// Wait for initial flip phase
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Take screenshot before flips
|
||||
await validator.capture('before-initial-flip');
|
||||
|
||||
// Complete initial flips
|
||||
await bot.completeInitialFlips();
|
||||
|
||||
// Take screenshot after flips
|
||||
await validator.capture('after-initial-flip');
|
||||
|
||||
// Verify 2 cards are face-up
|
||||
const state = await bot.getGameState();
|
||||
const faceUpCount = state.myPlayer?.cards.filter(c => c.faceUp).length || 0;
|
||||
expect(faceUpCount).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
|
||||
test('bot recovers from rapid turn changes', async ({ page }) => {
|
||||
test.setTimeout(90000); // 90 seconds
|
||||
|
||||
const bot = new GolfBot(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
|
||||
// Add multiple fast CPUs
|
||||
await bot.addCPU('Maya'); // Aggressive
|
||||
await bot.addCPU('Sage'); // Sneaky finisher
|
||||
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
// Play with health monitoring
|
||||
let frozenCount = 0;
|
||||
let turnCount = 0;
|
||||
|
||||
while (await bot.getGamePhase() !== 'round_over' && turnCount < 50) {
|
||||
if (await bot.isMyTurn()) {
|
||||
const result = await bot.playTurn();
|
||||
expect(result.success).toBe(true);
|
||||
turnCount++;
|
||||
}
|
||||
|
||||
// Check for freeze
|
||||
if (await bot.isFrozen(2000)) {
|
||||
frozenCount++;
|
||||
}
|
||||
|
||||
await page.waitForTimeout(100);
|
||||
}
|
||||
|
||||
// Should not have frozen
|
||||
expect(frozenCount).toBe(0);
|
||||
});
|
||||
|
||||
test('game handles all players finishing', async ({ page }) => {
|
||||
test.setTimeout(90000); // 90 seconds for single round
|
||||
|
||||
const bot = new GolfBot(page);
|
||||
const validator = new ScreenshotValidator(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
// Play until round over
|
||||
const roundResult = await bot.playRound(100);
|
||||
expect(roundResult.success).toBe(true);
|
||||
|
||||
// Take screenshot of round end
|
||||
await validator.capture('round-end');
|
||||
|
||||
// Verify all player cards are revealed
|
||||
const state = await bot.getGameState();
|
||||
const allRevealed = state.myPlayer?.cards.every(c => c.faceUp) ?? false;
|
||||
expect(allRevealed).toBe(true);
|
||||
|
||||
// Verify scoreboard is visible
|
||||
const scoreboardVisible = await validator.expectVisible('#game-buttons');
|
||||
expect(scoreboardVisible.passed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Game Settings', () => {
|
||||
test('Speed Golf mode (flip on discard)', async ({ page }) => {
|
||||
test.setTimeout(90000); // 90 seconds
|
||||
|
||||
const bot = new GolfBot(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
await bot.addCPU('Sofia');
|
||||
|
||||
// Start with Speed Golf (always flip)
|
||||
await bot.startGame({
|
||||
holes: 1,
|
||||
flipMode: 'always',
|
||||
});
|
||||
|
||||
// Play through
|
||||
const result = await bot.playRound(50);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
// No errors should occur
|
||||
expect(bot.getConsoleErrors()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Endgame mode (optional flip)', async ({ page }) => {
|
||||
test.setTimeout(90000); // 90 seconds
|
||||
|
||||
const bot = new GolfBot(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
await bot.addCPU('Sofia');
|
||||
|
||||
// Start with Endgame mode
|
||||
await bot.startGame({
|
||||
holes: 1,
|
||||
flipMode: 'endgame',
|
||||
});
|
||||
|
||||
// Play through
|
||||
const result = await bot.playRound(50);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
expect(bot.getConsoleErrors()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('Multiple decks with many players', async ({ page }) => {
|
||||
test.setTimeout(90000);
|
||||
|
||||
const bot = new GolfBot(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
|
||||
// Add 4 CPUs (5 total players)
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.addCPU('Marcus');
|
||||
await bot.addCPU('Maya');
|
||||
await bot.addCPU('Kenji');
|
||||
|
||||
// Start with 2 decks
|
||||
await bot.startGame({
|
||||
holes: 1,
|
||||
decks: 2,
|
||||
});
|
||||
|
||||
// Play through
|
||||
const result = await bot.playRound(100);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
expect(bot.getConsoleErrors()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
401
tests/e2e/specs/stress.spec.ts
Normal file
401
tests/e2e/specs/stress.spec.ts
Normal file
@@ -0,0 +1,401 @@
|
||||
/**
|
||||
* Stress tests
|
||||
* Tests for race conditions, memory leaks, and edge cases
|
||||
*/
|
||||
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { GolfBot } from '../bot/golf-bot';
|
||||
import { FreezeDetector } from '../health/freeze-detector';
|
||||
import { AnimationTracker } from '../health/animation-tracker';
|
||||
|
||||
test.describe('Stress Tests', () => {
|
||||
test('rapid action sequence (race condition detection)', async ({ page }) => {
|
||||
const bot = new GolfBot(page);
|
||||
const freezeDetector = new FreezeDetector(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('StressBot');
|
||||
await bot.addCPU('Maya'); // Aggressive, fast player
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
await bot.completeInitialFlips();
|
||||
|
||||
let actionCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
// Rapid turns with minimal delays
|
||||
while (await bot.getGamePhase() !== 'round_over' && actionCount < 100) {
|
||||
if (await bot.isMyTurn()) {
|
||||
// Reduce normal waits
|
||||
const state = await bot.getGameState();
|
||||
|
||||
if (!state.heldCard.visible) {
|
||||
// Quick draw
|
||||
const deck = page.locator('#deck');
|
||||
await deck.click({ timeout: 1000 }).catch(() => { errorCount++; });
|
||||
await page.waitForTimeout(100);
|
||||
} else {
|
||||
// Quick swap or discard
|
||||
const faceDown = state.myPlayer?.cards.find(c => !c.faceUp);
|
||||
if (faceDown) {
|
||||
const card = page.locator(`#player-cards .card:nth-child(${faceDown.position + 1})`);
|
||||
await card.click({ timeout: 1000 }).catch(() => { errorCount++; });
|
||||
} else {
|
||||
const discardBtn = page.locator('#discard-btn');
|
||||
await discardBtn.click({ timeout: 1000 }).catch(() => { errorCount++; });
|
||||
}
|
||||
await page.waitForTimeout(100);
|
||||
}
|
||||
|
||||
actionCount++;
|
||||
} else {
|
||||
await page.waitForTimeout(50);
|
||||
}
|
||||
|
||||
// Check for freezes
|
||||
if (await bot.isFrozen(2000)) {
|
||||
console.warn(`Freeze detected at action ${actionCount}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify no critical errors
|
||||
const health = await freezeDetector.runHealthCheck();
|
||||
expect(health.issues.filter(i => i.type === 'websocket_closed')).toHaveLength(0);
|
||||
|
||||
// Some click errors are acceptable (timing issues), but not too many
|
||||
expect(errorCount).toBeLessThan(10);
|
||||
|
||||
console.log(`Completed ${actionCount} rapid actions with ${errorCount} minor errors`);
|
||||
});
|
||||
|
||||
test('multiple games in succession (memory leak detection)', async ({ page }) => {
|
||||
test.setTimeout(300000); // 5 minutes
|
||||
|
||||
const bot = new GolfBot(page);
|
||||
const gamesCompleted: number[] = [];
|
||||
|
||||
// Get initial memory if available
|
||||
const getMemory = async () => {
|
||||
try {
|
||||
return await page.evaluate(() => {
|
||||
if ('memory' in performance) {
|
||||
return (performance as any).memory.usedJSHeapSize;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const initialMemory = await getMemory();
|
||||
console.log(`Initial memory: ${initialMemory ? Math.round(initialMemory / 1024 / 1024) + 'MB' : 'N/A'}`);
|
||||
|
||||
// Play 10 quick games
|
||||
for (let game = 0; game < 10; game++) {
|
||||
await bot.goto();
|
||||
await bot.createGame(`MemBot${game}`);
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
const result = await bot.playRound(50);
|
||||
|
||||
if (result.success) {
|
||||
gamesCompleted.push(game);
|
||||
}
|
||||
|
||||
// Check memory every few games
|
||||
if (game % 3 === 2) {
|
||||
const currentMemory = await getMemory();
|
||||
if (currentMemory) {
|
||||
console.log(`Game ${game + 1}: memory = ${Math.round(currentMemory / 1024 / 1024)}MB`);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear any accumulated errors
|
||||
bot.clearConsoleErrors();
|
||||
}
|
||||
|
||||
// Should complete most games
|
||||
expect(gamesCompleted.length).toBeGreaterThanOrEqual(8);
|
||||
|
||||
// Final memory check
|
||||
const finalMemory = await getMemory();
|
||||
if (initialMemory && finalMemory) {
|
||||
const memoryGrowth = finalMemory - initialMemory;
|
||||
console.log(`Memory growth: ${Math.round(memoryGrowth / 1024 / 1024)}MB`);
|
||||
|
||||
// Memory shouldn't grow excessively (allow 50MB growth for 10 games)
|
||||
expect(memoryGrowth).toBeLessThan(50 * 1024 * 1024);
|
||||
}
|
||||
});
|
||||
|
||||
test('6-player game with 5 CPUs (max players)', async ({ page }) => {
|
||||
test.setTimeout(180000); // 3 minutes
|
||||
|
||||
const bot = new GolfBot(page);
|
||||
const freezeDetector = new FreezeDetector(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
|
||||
// Add 5 CPU players (max typical setup)
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.addCPU('Maya');
|
||||
await bot.addCPU('Priya');
|
||||
await bot.addCPU('Marcus');
|
||||
await bot.addCPU('Kenji');
|
||||
|
||||
// Start with 2 decks (recommended for 6 players)
|
||||
await bot.startGame({
|
||||
holes: 3,
|
||||
decks: 2,
|
||||
});
|
||||
|
||||
// Play through all rounds
|
||||
const result = await bot.playGame(3);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.rounds).toBe(3);
|
||||
|
||||
// Check for issues
|
||||
const health = await freezeDetector.runHealthCheck();
|
||||
expect(health.healthy).toBe(true);
|
||||
|
||||
console.log(`6-player game completed in ${result.totalTurns} turns`);
|
||||
});
|
||||
|
||||
test('animation queue under load', async ({ page }) => {
|
||||
const bot = new GolfBot(page);
|
||||
const animTracker = new AnimationTracker(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('AnimBot');
|
||||
await bot.addCPU('Maya'); // Fast player
|
||||
await bot.addCPU('Sage'); // Sneaky finisher
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
await bot.completeInitialFlips();
|
||||
|
||||
let animationCount = 0;
|
||||
let stallCount = 0;
|
||||
|
||||
while (await bot.getGamePhase() !== 'round_over' && animationCount < 50) {
|
||||
if (await bot.isMyTurn()) {
|
||||
// Track animation timing
|
||||
animTracker.recordStart('turn');
|
||||
|
||||
await bot.playTurn();
|
||||
|
||||
const result = await animTracker.waitForAnimation('turn', 5000);
|
||||
if (!result.completed) {
|
||||
stallCount++;
|
||||
}
|
||||
|
||||
animationCount++;
|
||||
}
|
||||
|
||||
await page.waitForTimeout(100);
|
||||
}
|
||||
|
||||
// Check animation timing is reasonable
|
||||
const avgDuration = animTracker.getAverageDuration('turn');
|
||||
console.log(`Average turn animation: ${avgDuration?.toFixed(0) || 'N/A'}ms`);
|
||||
|
||||
// Stalls should be rare
|
||||
expect(stallCount).toBeLessThan(3);
|
||||
|
||||
// Check stall events
|
||||
const stalls = animTracker.getStalls();
|
||||
if (stalls.length > 0) {
|
||||
console.log(`Animation stalls:`, stalls);
|
||||
}
|
||||
});
|
||||
|
||||
test('websocket reconnection handling', async ({ page }) => {
|
||||
const bot = new GolfBot(page);
|
||||
const freezeDetector = new FreezeDetector(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('ReconnectBot');
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
await bot.completeInitialFlips();
|
||||
|
||||
// Play a few turns
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await bot.waitForMyTurn(10000);
|
||||
if (await bot.isMyTurn()) {
|
||||
await bot.playTurn();
|
||||
}
|
||||
}
|
||||
|
||||
// Check WebSocket is healthy
|
||||
const wsHealthy = await freezeDetector.checkWebSocket();
|
||||
expect(wsHealthy).toBe(true);
|
||||
|
||||
// Note: Actually closing/reopening websocket would require
|
||||
// server cooperation or network manipulation
|
||||
});
|
||||
|
||||
test('concurrent clicks during animation', async ({ page }) => {
|
||||
const bot = new GolfBot(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('ClickBot');
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
await bot.completeInitialFlips();
|
||||
await bot.waitForMyTurn(10000);
|
||||
|
||||
// Draw a card
|
||||
const deck = page.locator('#deck');
|
||||
await deck.click();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Try rapid clicks on multiple elements while animation might be running
|
||||
const clickPromises: Promise<void>[] = [];
|
||||
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const card = page.locator(`#player-cards .card:nth-child(${i + 1})`);
|
||||
clickPromises.push(
|
||||
card.click({ timeout: 500 }).catch(() => {})
|
||||
);
|
||||
}
|
||||
|
||||
// Wait for all clicks to complete or timeout
|
||||
await Promise.all(clickPromises);
|
||||
|
||||
// Wait for any animations
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Game should still be in a valid state
|
||||
const phase = await bot.getGamePhase();
|
||||
expect(['playing', 'waiting_for_flip', 'round_over']).toContain(phase);
|
||||
|
||||
// No console errors
|
||||
const errors = bot.getConsoleErrors();
|
||||
expect(errors.filter(e => e.includes('undefined') || e.includes('null'))).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Edge Cases', () => {
|
||||
test('all cards revealed simultaneously', async ({ page }) => {
|
||||
const bot = new GolfBot(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('EdgeBot');
|
||||
await bot.addCPU('Sofia');
|
||||
|
||||
// Start with Speed Golf (flip on discard) to reveal cards faster
|
||||
await bot.startGame({
|
||||
holes: 1,
|
||||
flipMode: 'always',
|
||||
initialFlips: 2,
|
||||
});
|
||||
|
||||
// Play until we trigger round end
|
||||
const result = await bot.playRound(100);
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
// Verify game handled the transition
|
||||
const phase = await bot.getGamePhase();
|
||||
expect(phase).toBe('round_over');
|
||||
});
|
||||
|
||||
test('deck reshuffle scenario', async ({ page }) => {
|
||||
test.setTimeout(180000); // 3 minutes for longer game
|
||||
|
||||
const bot = new GolfBot(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('ShuffleBot');
|
||||
|
||||
// Add many players to deplete deck faster
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.addCPU('Maya');
|
||||
await bot.addCPU('Marcus');
|
||||
await bot.addCPU('Kenji');
|
||||
|
||||
// Use only 1 deck to force reshuffle
|
||||
await bot.startGame({
|
||||
holes: 1,
|
||||
decks: 1,
|
||||
});
|
||||
|
||||
// Play through - deck should reshuffle during game
|
||||
const result = await bot.playRound(200);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
test('empty discard pile handling', async ({ page }) => {
|
||||
const bot = new GolfBot(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('EmptyBot');
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
// At game start, discard might be empty briefly
|
||||
const initialState = await bot.getGameState();
|
||||
|
||||
// Game should still function
|
||||
await bot.completeInitialFlips();
|
||||
await bot.waitForMyTurn(10000);
|
||||
|
||||
// Should be able to draw from deck even if discard is empty
|
||||
if (await bot.isMyTurn()) {
|
||||
const state = await bot.getGameState();
|
||||
if (!state.discard.hasCard) {
|
||||
// Draw from deck should work
|
||||
const deck = page.locator('#deck');
|
||||
await deck.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Should have a held card now
|
||||
const afterState = await bot.getGameState();
|
||||
expect(afterState.heldCard.visible).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('final turn badge timing', async ({ page }) => {
|
||||
const bot = new GolfBot(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('BadgeBot');
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
// Monitor for final turn badge
|
||||
let sawFinalTurnBadge = false;
|
||||
let turnsAfterBadge = 0;
|
||||
|
||||
while (await bot.getGamePhase() !== 'round_over') {
|
||||
const state = await bot.getGameState();
|
||||
|
||||
if (state.isFinalTurn) {
|
||||
sawFinalTurnBadge = true;
|
||||
}
|
||||
|
||||
if (sawFinalTurnBadge && await bot.isMyTurn()) {
|
||||
turnsAfterBadge++;
|
||||
}
|
||||
|
||||
if (await bot.isMyTurn()) {
|
||||
await bot.playTurn();
|
||||
}
|
||||
|
||||
await page.waitForTimeout(100);
|
||||
}
|
||||
|
||||
// If final turn happened, we should have had at most 1 turn after badge appeared
|
||||
// (this depends on whether we're the one who triggered final turn)
|
||||
if (sawFinalTurnBadge) {
|
||||
expect(turnsAfterBadge).toBeLessThanOrEqual(2);
|
||||
}
|
||||
});
|
||||
});
|
||||
348
tests/e2e/specs/visual.spec.ts
Normal file
348
tests/e2e/specs/visual.spec.ts
Normal file
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* Visual regression tests
|
||||
* Validates visual correctness at key game moments
|
||||
*/
|
||||
|
||||
import { test, expect, devices } from '@playwright/test';
|
||||
import { GolfBot } from '../bot/golf-bot';
|
||||
import { ScreenshotValidator } from '../visual/screenshot-validator';
|
||||
import {
|
||||
validateGameStart,
|
||||
validateAfterInitialFlip,
|
||||
validateDrawPhase,
|
||||
validateAfterDraw,
|
||||
validateRoundOver,
|
||||
validateFinalTurn,
|
||||
validateResponsiveLayout,
|
||||
} from '../visual/visual-rules';
|
||||
|
||||
test.describe('Visual Validation', () => {
|
||||
test('game start visual state', async ({ page }) => {
|
||||
const bot = new GolfBot(page);
|
||||
const validator = new ScreenshotValidator(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
// Wait for game to fully render
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Capture game start
|
||||
await validator.capture('game-start-visual');
|
||||
|
||||
// Validate visual state
|
||||
const result = await validateGameStart(validator);
|
||||
expect(result.passed).toBe(true);
|
||||
if (!result.passed) {
|
||||
console.log('Failures:', result.failures);
|
||||
}
|
||||
});
|
||||
|
||||
test('initial flip visual state', async ({ page }) => {
|
||||
const bot = new GolfBot(page);
|
||||
const validator = new ScreenshotValidator(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1, initialFlips: 2 });
|
||||
|
||||
// Complete initial flips
|
||||
await bot.completeInitialFlips();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Capture after flips
|
||||
await validator.capture('after-initial-flip-visual');
|
||||
|
||||
// Validate
|
||||
const result = await validateAfterInitialFlip(validator, 2);
|
||||
expect(result.passed).toBe(true);
|
||||
if (!result.passed) {
|
||||
console.log('Failures:', result.failures);
|
||||
}
|
||||
});
|
||||
|
||||
test('draw phase visual state', async ({ page }) => {
|
||||
const bot = new GolfBot(page);
|
||||
const validator = new ScreenshotValidator(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
// Complete initial phase and wait for our turn
|
||||
await bot.completeInitialFlips();
|
||||
|
||||
// Wait for our turn
|
||||
await bot.waitForMyTurn(10000);
|
||||
|
||||
// Capture draw phase
|
||||
await validator.capture('draw-phase-visual');
|
||||
|
||||
// Validate
|
||||
const result = await validateDrawPhase(validator);
|
||||
expect(result.passed).toBe(true);
|
||||
if (!result.passed) {
|
||||
console.log('Failures:', result.failures);
|
||||
}
|
||||
});
|
||||
|
||||
test('held card visual state', async ({ page }) => {
|
||||
const bot = new GolfBot(page);
|
||||
const validator = new ScreenshotValidator(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
await bot.completeInitialFlips();
|
||||
await bot.waitForMyTurn(10000);
|
||||
|
||||
// Draw a card
|
||||
const deck = page.locator('#deck');
|
||||
await deck.click();
|
||||
|
||||
// Wait for draw animation
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Capture held card state
|
||||
await validator.capture('held-card-visual');
|
||||
|
||||
// Validate held card is visible
|
||||
const heldResult = await validator.expectHeldCardVisible();
|
||||
expect(heldResult.passed).toBe(true);
|
||||
|
||||
// Validate cards are clickable
|
||||
const clickableResult = await validator.expectCount(
|
||||
'#player-cards .card.clickable',
|
||||
6
|
||||
);
|
||||
expect(clickableResult.passed).toBe(true);
|
||||
});
|
||||
|
||||
test('round over visual state', async ({ page }) => {
|
||||
const bot = new GolfBot(page);
|
||||
const validator = new ScreenshotValidator(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
// Play until round over
|
||||
await bot.playRound(50);
|
||||
|
||||
// Wait for animations
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Capture round over
|
||||
await validator.capture('round-over-visual');
|
||||
|
||||
// Validate
|
||||
const result = await validateRoundOver(validator);
|
||||
expect(result.passed).toBe(true);
|
||||
if (!result.passed) {
|
||||
console.log('Failures:', result.failures);
|
||||
}
|
||||
});
|
||||
|
||||
test('card flip animation renders correctly', async ({ page }) => {
|
||||
const bot = new GolfBot(page);
|
||||
const validator = new ScreenshotValidator(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1, initialFlips: 0 }); // No initial flips
|
||||
|
||||
// Wait for our turn
|
||||
await bot.waitForMyTurn(10000);
|
||||
|
||||
// Capture before flip
|
||||
await validator.capture('before-flip');
|
||||
|
||||
// Get a face-down card position
|
||||
const state = await bot.getGameState();
|
||||
const faceDownPos = state.myPlayer?.cards.find(c => !c.faceUp)?.position ?? 0;
|
||||
|
||||
// Draw and swap to trigger flip
|
||||
const deck = page.locator('#deck');
|
||||
await deck.click();
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
// Click the face-down card to swap
|
||||
const card = page.locator(`#player-cards .card:nth-child(${faceDownPos + 1})`);
|
||||
await card.click();
|
||||
|
||||
// Wait for animation to complete
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Capture after flip
|
||||
await validator.capture('after-flip');
|
||||
|
||||
// Verify card is now face-up
|
||||
const afterState = await bot.getGameState();
|
||||
const cardAfter = afterState.myPlayer?.cards.find(c => c.position === faceDownPos);
|
||||
expect(cardAfter?.faceUp).toBe(true);
|
||||
});
|
||||
|
||||
test('opponent highlighting on their turn', async ({ page }) => {
|
||||
const bot = new GolfBot(page);
|
||||
const validator = new ScreenshotValidator(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
await bot.completeInitialFlips();
|
||||
|
||||
// Play our turn and then wait for opponent's turn
|
||||
if (await bot.isMyTurn()) {
|
||||
await bot.playTurn();
|
||||
}
|
||||
|
||||
// Wait for opponent turn indicator
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Check if it's opponent's turn
|
||||
const state = await bot.getGameState();
|
||||
const opponentPlaying = state.opponents.some(o => o.isCurrentTurn);
|
||||
|
||||
if (opponentPlaying) {
|
||||
// Capture opponent turn
|
||||
await validator.capture('opponent-turn-visual');
|
||||
|
||||
// Find which opponent has current turn
|
||||
const currentOpponentIndex = state.opponents.findIndex(o => o.isCurrentTurn);
|
||||
if (currentOpponentIndex >= 0) {
|
||||
const result = await validator.expectOpponentCurrentTurn(currentOpponentIndex);
|
||||
expect(result.passed).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('discard pile updates correctly', async ({ page }) => {
|
||||
const bot = new GolfBot(page);
|
||||
const validator = new ScreenshotValidator(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
await bot.completeInitialFlips();
|
||||
await bot.waitForMyTurn(10000);
|
||||
|
||||
// Get initial discard state
|
||||
const beforeState = await bot.getGameState();
|
||||
const beforeDiscard = beforeState.discard.topCard;
|
||||
|
||||
// Draw from deck and discard
|
||||
const deck = page.locator('#deck');
|
||||
await deck.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Get the held card
|
||||
const heldCard = (await bot.getGameState()).heldCard.card;
|
||||
|
||||
// Discard the drawn card
|
||||
const discardBtn = page.locator('#discard-btn');
|
||||
await discardBtn.click();
|
||||
await page.waitForTimeout(800);
|
||||
|
||||
// Capture after discard
|
||||
await validator.capture('after-discard-visual');
|
||||
|
||||
// Verify discard pile has the card we discarded
|
||||
const afterState = await bot.getGameState();
|
||||
expect(afterState.discard.hasCard).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Responsive Layout', () => {
|
||||
test('mobile layout (375px)', async ({ browser }) => {
|
||||
const context = await browser.newContext({
|
||||
...devices['iPhone 13'],
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
const bot = new GolfBot(page);
|
||||
const validator = new ScreenshotValidator(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Capture mobile layout
|
||||
await validator.capture('mobile-375-layout');
|
||||
|
||||
// Validate responsive elements
|
||||
const result = await validateResponsiveLayout(validator, 375);
|
||||
expect(result.passed).toBe(true);
|
||||
if (!result.passed) {
|
||||
console.log('Mobile failures:', result.failures);
|
||||
}
|
||||
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('tablet layout (768px)', async ({ browser }) => {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 768, height: 1024 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
const bot = new GolfBot(page);
|
||||
const validator = new ScreenshotValidator(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Capture tablet layout
|
||||
await validator.capture('tablet-768-layout');
|
||||
|
||||
// Validate responsive elements
|
||||
const result = await validateResponsiveLayout(validator, 768);
|
||||
expect(result.passed).toBe(true);
|
||||
|
||||
await context.close();
|
||||
});
|
||||
|
||||
test('desktop layout (1920px)', async ({ browser }) => {
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
||||
const bot = new GolfBot(page);
|
||||
const validator = new ScreenshotValidator(page);
|
||||
|
||||
await bot.goto();
|
||||
await bot.createGame('TestBot');
|
||||
await bot.addCPU('Sofia');
|
||||
await bot.startGame({ holes: 1 });
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Capture desktop layout
|
||||
await validator.capture('desktop-1920-layout');
|
||||
|
||||
// Validate responsive elements
|
||||
const result = await validateResponsiveLayout(validator, 1920);
|
||||
expect(result.passed).toBe(true);
|
||||
|
||||
await context.close();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user