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,220 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Tactical Practice 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');
|
||||
});
|
||||
});
|
||||
|
||||
test('should navigate to learning area from start screen', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wait for the Learning Area button to be visible
|
||||
const learningButton = page.locator('button:has-text("Learning Area")').last();
|
||||
await expect(learningButton).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Click on Learning Area button
|
||||
await learningButton.click();
|
||||
|
||||
// Should be on the learning page
|
||||
await expect(page).toHaveURL('/learning');
|
||||
|
||||
// Should see tactical pattern buttons
|
||||
await expect(page.locator('text=PIN')).toBeVisible();
|
||||
await expect(page.locator('text=FORK')).toBeVisible();
|
||||
await expect(page.locator('text=SKEWER')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should select a coach before starting practice', async ({ page }) => {
|
||||
await page.goto('/learning');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Should see personality selection
|
||||
await expect(page.locator('button:has-text("Professional Coach")').first()).toBeVisible();
|
||||
|
||||
// Select a coach
|
||||
const professionalCoach = page.locator('button:has-text("Professional Coach")').first();
|
||||
await professionalCoach.click();
|
||||
|
||||
// Coach should be selected (button should have blue border)
|
||||
await expect(professionalCoach).toHaveClass(/border-blue-500/);
|
||||
});
|
||||
|
||||
test('should start PIN tactical practice', async ({ page }) => {
|
||||
await page.goto('/learning');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Select a coach first
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
|
||||
// Click on PIN button
|
||||
await page.click('text=PIN');
|
||||
|
||||
// Should navigate to PIN practice page
|
||||
await expect(page).toHaveURL('/learning/tactics/pin');
|
||||
|
||||
// Wait for the page to load
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000); // Give time for puzzle to load
|
||||
|
||||
// Should see the chessboard (react-chessboard uses SVG/images, not canvas)
|
||||
// Look for statistics which are always visible
|
||||
await expect(page.getByText('Correct', { exact: true })).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// Should see statistics display
|
||||
await expect(page.getByText('Correct', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('Incorrect', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('Current Streak', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('Best Streak', { exact: true })).toBeVisible();
|
||||
|
||||
// Should see difficulty selector
|
||||
await expect(page.locator('button:has-text("Easy")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("Medium")')).toBeVisible();
|
||||
await expect(page.locator('button:has-text("Hard")')).toBeVisible();
|
||||
|
||||
// Should see puzzle rating
|
||||
await expect(page.locator('text=Puzzle Rating')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display skip button', async ({ page }) => {
|
||||
await page.goto('/learning');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
await page.click('text=PIN');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Should see Skip Puzzle button
|
||||
await expect(page.locator('button:has-text("Skip Puzzle")')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should switch difficulty levels', async ({ page }) => {
|
||||
await page.goto('/learning');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
await page.click('text=PIN');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Wait for initial puzzle to load
|
||||
await page.waitForSelector('text=Puzzle Rating', { timeout: 10000 });
|
||||
|
||||
// Close any modals or overlays that might be blocking
|
||||
const closeButtons = page.locator('button[aria-label*="Close"]').or(page.locator('button:has-text("×")'));
|
||||
if (await closeButtons.count() > 0) {
|
||||
await closeButtons.first().click({ force: true });
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Get initial difficulty button state (Easy is selected by default with green background)
|
||||
const easyButton = page.locator('button:has-text("Easy")');
|
||||
await expect(easyButton).toHaveClass(/bg-green-500/); // Easy is green when selected
|
||||
|
||||
// Click Medium difficulty - use force to bypass any overlays
|
||||
await page.locator('button:has-text("Medium")').click({ force: true });
|
||||
|
||||
// Wait for new puzzle to load
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Medium button should now be selected (yellow background)
|
||||
const mediumButton = page.locator('button:has-text("Medium")');
|
||||
await expect(mediumButton).toHaveClass(/bg-yellow-500/);
|
||||
});
|
||||
|
||||
test('should show coach chat interface', async ({ page }) => {
|
||||
await page.goto('/learning');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
await page.click('text=FORK');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000); // Give time for puzzle to load
|
||||
|
||||
// Wait for page to load (react-chessboard uses SVG/images, not canvas)
|
||||
await expect(page.getByText('Correct', { exact: true })).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// Close any modals or overlays that might be open
|
||||
const closeButtons = page.locator('button[aria-label*="Close"]').or(page.locator('button:has-text("×")'));
|
||||
if (await closeButtons.count() > 0) {
|
||||
await closeButtons.first().click({ force: true });
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Should see input field for asking questions
|
||||
await expect(page.locator('input[placeholder*="Ask"]')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Should see send button (it's an icon-only button with the Send icon)
|
||||
const sendButton = page.locator('button[type="submit"]').or(page.locator('form button').last());
|
||||
await expect(sendButton.first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should update statistics when skipping puzzle', async ({ page }) => {
|
||||
await page.goto('/learning');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
await page.click('text=PIN');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.waitForTimeout(2000); // Give time for puzzle to load
|
||||
|
||||
// Wait for puzzle to load (react-chessboard uses SVG/images, not canvas)
|
||||
await expect(page.getByText('Correct', { exact: true })).toBeVisible({ timeout: 15000 });
|
||||
|
||||
// Close any modals or overlays that might be blocking
|
||||
const closeButtons = page.locator('button[aria-label*="Close"]').or(page.locator('button:has-text("×")'));
|
||||
if (await closeButtons.count() > 0) {
|
||||
await closeButtons.first().click({ force: true });
|
||||
await page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
// Find the incorrect stat card - it's the second stat card
|
||||
const statCards = page.locator('.bg-white.dark\\:bg-gray-800.rounded-lg.p-4');
|
||||
const incorrectCard = statCards.nth(1);
|
||||
const incorrectValue = incorrectCard.locator('.text-2xl.font-bold');
|
||||
|
||||
// Initial stats should be 0
|
||||
await expect(incorrectValue).toHaveText('0');
|
||||
|
||||
// Click Skip Puzzle - use force if needed to bypass overlays
|
||||
await page.locator('button:has-text("Skip Puzzle")').click({ force: true });
|
||||
|
||||
// Wait for stats to update and new puzzle to load
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Incorrect count should increase
|
||||
await expect(incorrectValue).toHaveText('1');
|
||||
});
|
||||
|
||||
test('should navigate back to learning area', async ({ page }) => {
|
||||
await page.goto('/learning');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.locator('button:has-text("Professional Coach")').first().click();
|
||||
await page.click('text=PIN');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Click back button
|
||||
await page.click('text=Back to Learning');
|
||||
|
||||
// Should be back on learning page
|
||||
await expect(page).toHaveURL('/learning');
|
||||
});
|
||||
|
||||
test('should display all 8 tactical patterns', async ({ page }) => {
|
||||
await page.goto('/learning');
|
||||
|
||||
const patterns = [
|
||||
'PIN',
|
||||
'SKEWER',
|
||||
'FORK',
|
||||
'DISCOVERED CHECK',
|
||||
'DOUBLE ATTACK',
|
||||
'OVERLOADING',
|
||||
'BACK RANK WEAKNESS',
|
||||
'TRAPPED PIECE'
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
await expect(page.locator(`text=${pattern}`)).toBeVisible();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user