feat: improve opening trainer flow and add deviation dialog
- Add tutor message guardrail to prevent rapid-fire messages - Track last message by move index - Wait for both player and opponent moves before speaking - Speak immediately when player deviates from theory - Add deviation dialog with three options: - Continue Playing (Start Game) - transitions to game mode - Undo & Return to Opening - returns to theory - Explore This Variation - continues off-book practice - Implement smooth game mode transition: - Pass opening context from trainer to game - Tutor welcomes player with context about their study - Computer makes first move if needed in starting position - Fix generate-test-fixtures script: - Add safety check to prevent overwriting real data - Restore full opening database (12,379 openings) - Rebuild move index (12,377 sequences) - Add clear warnings about test fixtures - Update ChessGame to accept openingContext prop - Update Tutor to display contextual greeting in game mode 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Game Playing Feature', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Set a mock API key in localStorage BEFORE any page loads
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem('gemini_api_key', 'mock-api-key-for-testing');
|
||||
});
|
||||
|
||||
// Mock the LLM API to avoid needing a real API key
|
||||
await page.route('**/api/v1/llm/chat', async (route) => {
|
||||
const request = route.request();
|
||||
const postData = request.postDataJSON();
|
||||
|
||||
// Generate contextual responses based on the message
|
||||
let reply = "Let's play chess! Make your move.";
|
||||
|
||||
if (postData.message?.includes('NEW GAME')) {
|
||||
reply = "Welcome! I'm ready to help you improve your chess. Let's begin!";
|
||||
} else if (postData.message?.includes('PLAYER MOVE')) {
|
||||
reply = "Interesting move! Let me think about my response.";
|
||||
} else if (postData.message?.includes('COMPUTER MOVE')) {
|
||||
reply = "I've made my move. What do you think about the position?";
|
||||
} else if (postData.message?.includes('GAME OVER')) {
|
||||
reply = "Good game! Let's review what we learned.";
|
||||
}
|
||||
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({ reply }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('should start a new game as white', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Should see the start screen heading
|
||||
await expect(page.getByRole('heading', { name: 'Chess Tutor AI' }).first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Click "Start New Game" button
|
||||
const startButton = page.locator('button:has-text("Start New Game")');
|
||||
await expect(startButton).toBeVisible();
|
||||
await startButton.click();
|
||||
|
||||
// Should see personality selection
|
||||
await expect(page.locator('button:has-text("Professional Coach")')).toBeVisible();
|
||||
|
||||
// White should be selected by default
|
||||
await expect(page.locator('button:has-text("Play as White")')).toHaveClass(/border-blue-600/);
|
||||
|
||||
// Click on a personality to start the game
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
|
||||
// Wait for game to load
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000); // Give Stockfish time to initialize
|
||||
|
||||
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
|
||||
// Look for the "Undo Last Move" button which appears when the game is loaded
|
||||
await expect(page.locator('button:has-text("Undo Last Move")')).toBeVisible({ timeout: 20000 });
|
||||
|
||||
// Should see evaluation bar (if present)
|
||||
const evaluationElement = page.locator('text=Evaluation').or(page.locator('[class*="evaluation"]'));
|
||||
if (await evaluationElement.count() > 0) {
|
||||
await expect(evaluationElement.first()).toBeVisible();
|
||||
}
|
||||
|
||||
// Should see chat interface
|
||||
await expect(page.locator('input[placeholder*="Ask"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should start a new game as black', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const startButton = page.locator('button:has-text("Start New Game")');
|
||||
await startButton.click();
|
||||
|
||||
// Select black
|
||||
await page.locator('button:has-text("Play as Black")').click();
|
||||
await expect(page.locator('button:has-text("Play as Black")')).toHaveClass(/border-blue-600/);
|
||||
|
||||
// Click on a personality to start the game
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
|
||||
await expect(page.locator('button:has-text("Undo Last Move")')).toBeVisible({ timeout: 20000 });
|
||||
|
||||
// Board should be flipped (black at bottom)
|
||||
// The computer should make the first move since we're playing as black
|
||||
await page.waitForTimeout(2000);
|
||||
});
|
||||
|
||||
test('should import a FEN position', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const startButton = page.locator('button:has-text("Start New Game")');
|
||||
await startButton.click();
|
||||
|
||||
// Expand advanced options
|
||||
const advancedButton = page.locator('button:has-text("Advanced")');
|
||||
await advancedButton.click();
|
||||
|
||||
// Enter a FEN position (Scholar's Mate position)
|
||||
const fenInput = page.locator('textarea[placeholder*="Paste FEN"]');
|
||||
await expect(fenInput).toBeVisible();
|
||||
await fenInput.fill('r1bqkb1r/pppp1Qpp/2n2n2/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 4');
|
||||
|
||||
// Should detect FEN format
|
||||
await expect(page.locator('text=FEN Position')).toBeVisible();
|
||||
|
||||
// Click on a personality to start the game
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Should load the position (react-chessboard uses SVG/images, not canvas)
|
||||
await expect(page.locator('button:has-text("Undo Last Move")')).toBeVisible({ timeout: 20000 });
|
||||
});
|
||||
|
||||
test('should show game controls', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Start a quick game
|
||||
const startButton = page.locator('button:has-text("Start New Game")');
|
||||
await startButton.click();
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(3000);
|
||||
|
||||
// Wait for game to load (react-chessboard uses SVG/images, not canvas)
|
||||
await expect(page.locator('button:has-text("Undo Last Move")')).toBeVisible({ timeout: 20000 });
|
||||
|
||||
// Should see game control buttons (using more flexible selectors)
|
||||
await expect(page.locator('button').filter({ hasText: /Resign|Give up/i }).first()).toBeVisible();
|
||||
await expect(page.locator('button').filter({ hasText: /Download|Save/i }).first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user