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:
Aaron D. Lee
2026-01-29 18:33:28 -05:00
parent 724bf87c43
commit 6950769bc3
29 changed files with 5153 additions and 348 deletions

View 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);
});
});

View 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);
}
});
});

View 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();
});
});