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>
233 lines
6.5 KiB
TypeScript
233 lines
6.5 KiB
TypeScript
/**
|
|
* Visual rules - expected visual states for different game phases
|
|
*/
|
|
|
|
import { ScreenshotValidator } from './screenshot-validator';
|
|
|
|
/**
|
|
* Expected visual states for game start
|
|
*/
|
|
export async function validateGameStart(
|
|
validator: ScreenshotValidator
|
|
): Promise<{ passed: boolean; failures: string[] }> {
|
|
const failures: string[] = [];
|
|
|
|
// All cards should be visible (face up or down)
|
|
for (let i = 0; i < 6; i++) {
|
|
const result = await validator.expectCount(
|
|
`#player-cards .card:nth-child(${i + 1})`,
|
|
1
|
|
);
|
|
if (!result.passed) {
|
|
failures.push(`Card ${i} not present`);
|
|
}
|
|
}
|
|
|
|
// Status message should indicate game phase
|
|
const statusResult = await validator.expectVisible('#status-message');
|
|
if (!statusResult.passed) {
|
|
failures.push('Status message not visible');
|
|
}
|
|
|
|
// Deck should be visible
|
|
const deckResult = await validator.expectVisible('#deck');
|
|
if (!deckResult.passed) {
|
|
failures.push('Deck not visible');
|
|
}
|
|
|
|
// Discard should be visible
|
|
const discardResult = await validator.expectVisible('#discard');
|
|
if (!discardResult.passed) {
|
|
failures.push('Discard not visible');
|
|
}
|
|
|
|
return { passed: failures.length === 0, failures };
|
|
}
|
|
|
|
/**
|
|
* Expected visual states after initial flip
|
|
*/
|
|
export async function validateAfterInitialFlip(
|
|
validator: ScreenshotValidator,
|
|
expectedFaceUp: number = 2
|
|
): Promise<{ passed: boolean; failures: string[] }> {
|
|
const failures: string[] = [];
|
|
|
|
// Count face-up cards
|
|
const faceUpResult = await validator.expectCount(
|
|
'#player-cards .card.card-front',
|
|
expectedFaceUp
|
|
);
|
|
if (!faceUpResult.passed) {
|
|
failures.push(`Expected ${expectedFaceUp} face-up cards, got ${faceUpResult.actual}`);
|
|
}
|
|
|
|
// Count face-down cards
|
|
const faceDownResult = await validator.expectCount(
|
|
'#player-cards .card.card-back',
|
|
6 - expectedFaceUp
|
|
);
|
|
if (!faceDownResult.passed) {
|
|
failures.push(`Expected ${6 - expectedFaceUp} face-down cards, got ${faceDownResult.actual}`);
|
|
}
|
|
|
|
return { passed: failures.length === 0, failures };
|
|
}
|
|
|
|
/**
|
|
* Expected visual states during player's turn (draw phase)
|
|
*/
|
|
export async function validateDrawPhase(
|
|
validator: ScreenshotValidator
|
|
): Promise<{ passed: boolean; failures: string[] }> {
|
|
const failures: string[] = [];
|
|
|
|
// Deck should be clickable
|
|
const deckResult = await validator.expectDeckClickable();
|
|
if (!deckResult.passed) {
|
|
failures.push('Deck should be clickable');
|
|
}
|
|
|
|
// Held card should NOT be visible yet
|
|
const heldResult = await validator.expectHeldCardHidden();
|
|
if (!heldResult.passed) {
|
|
failures.push('Held card should not be visible before draw');
|
|
}
|
|
|
|
// Discard button should be hidden
|
|
const discardBtnResult = await validator.expectNotVisible('#discard-btn');
|
|
if (!discardBtnResult.passed) {
|
|
failures.push('Discard button should be hidden before draw');
|
|
}
|
|
|
|
return { passed: failures.length === 0, failures };
|
|
}
|
|
|
|
/**
|
|
* Expected visual states after drawing a card
|
|
*/
|
|
export async function validateAfterDraw(
|
|
validator: ScreenshotValidator
|
|
): Promise<{ passed: boolean; failures: string[] }> {
|
|
const failures: string[] = [];
|
|
|
|
// Held card should be visible (floating)
|
|
const heldResult = await validator.expectHeldCardVisible();
|
|
if (!heldResult.passed) {
|
|
failures.push('Held card should be visible after draw');
|
|
}
|
|
|
|
// Player cards should be clickable
|
|
const clickableResult = await validator.expectCount(
|
|
'#player-cards .card.clickable',
|
|
6
|
|
);
|
|
if (!clickableResult.passed) {
|
|
failures.push('All player cards should be clickable');
|
|
}
|
|
|
|
return { passed: failures.length === 0, failures };
|
|
}
|
|
|
|
/**
|
|
* Expected visual states for round over
|
|
*/
|
|
export async function validateRoundOver(
|
|
validator: ScreenshotValidator
|
|
): Promise<{ passed: boolean; failures: string[] }> {
|
|
const failures: string[] = [];
|
|
|
|
// All player cards should be face-up
|
|
const faceUpResult = await validator.expectCount(
|
|
'#player-cards .card.card-front',
|
|
6
|
|
);
|
|
if (!faceUpResult.passed) {
|
|
failures.push('All cards should be face-up at round end');
|
|
}
|
|
|
|
// Next round button OR new game button should be visible
|
|
const nextRoundResult = await validator.expectVisible('#next-round-btn');
|
|
const newGameResult = await validator.expectVisible('#new-game-btn');
|
|
|
|
if (!nextRoundResult.passed && !newGameResult.passed) {
|
|
failures.push('Neither next round nor new game button visible');
|
|
}
|
|
|
|
// Game buttons container should be visible
|
|
const gameButtonsResult = await validator.expectVisible('#game-buttons');
|
|
if (!gameButtonsResult.passed) {
|
|
failures.push('Game buttons should be visible');
|
|
}
|
|
|
|
return { passed: failures.length === 0, failures };
|
|
}
|
|
|
|
/**
|
|
* Expected visual states for final turn
|
|
*/
|
|
export async function validateFinalTurn(
|
|
validator: ScreenshotValidator
|
|
): Promise<{ passed: boolean; failures: string[] }> {
|
|
const failures: string[] = [];
|
|
|
|
// Final turn badge should be visible
|
|
const badgeResult = await validator.expectFinalTurnBadge();
|
|
if (!badgeResult.passed) {
|
|
failures.push('Final turn badge should be visible');
|
|
}
|
|
|
|
return { passed: failures.length === 0, failures };
|
|
}
|
|
|
|
/**
|
|
* Expected visual states during opponent's turn
|
|
*/
|
|
export async function validateOpponentTurn(
|
|
validator: ScreenshotValidator,
|
|
opponentIndex: number
|
|
): Promise<{ passed: boolean; failures: string[] }> {
|
|
const failures: string[] = [];
|
|
|
|
// Opponent should have current-turn highlight
|
|
const turnResult = await validator.expectOpponentCurrentTurn(opponentIndex);
|
|
if (!turnResult.passed) {
|
|
failures.push(`Opponent ${opponentIndex} should have current-turn class`);
|
|
}
|
|
|
|
// Deck should NOT be clickable (not our turn)
|
|
const deckResult = await validator.expectNoClass('#deck', 'clickable');
|
|
if (!deckResult.passed) {
|
|
failures.push('Deck should not be clickable during opponent turn');
|
|
}
|
|
|
|
return { passed: failures.length === 0, failures };
|
|
}
|
|
|
|
/**
|
|
* Validate responsive layout at specific width
|
|
*/
|
|
export async function validateResponsiveLayout(
|
|
validator: ScreenshotValidator,
|
|
width: number
|
|
): Promise<{ passed: boolean; failures: string[] }> {
|
|
const failures: string[] = [];
|
|
|
|
// Core elements should still be visible
|
|
const elements = [
|
|
'#deck',
|
|
'#discard',
|
|
'#player-cards',
|
|
'#status-message',
|
|
];
|
|
|
|
for (const selector of elements) {
|
|
const result = await validator.expectVisible(selector);
|
|
if (!result.passed) {
|
|
failures.push(`${selector} not visible at ${width}px width`);
|
|
}
|
|
}
|
|
|
|
return { passed: failures.length === 0, failures };
|
|
}
|