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,334 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* E2E Tests for Opening Training Feature
|
||||
*
|
||||
* These tests verify the complete training flow from opening selection
|
||||
* to receiving move feedback with engine evaluation and LLM explanations.
|
||||
*/
|
||||
|
||||
test.describe('Opening Training', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
// Navigate to opening selection page
|
||||
await page.goto('/learning/openings');
|
||||
await page.waitForLoadState('networkidle');
|
||||
});
|
||||
|
||||
test('should display opening selection page with family groups', async ({ page }) => {
|
||||
// Check page title
|
||||
await expect(page.getByText('Opening Training')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Check that opening family cards are present (look for the family buttons instead of category headers)
|
||||
const familyCards = page.locator('button[aria-label*="opening family"]');
|
||||
await expect(familyCards.first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Should have multiple families
|
||||
const count = await familyCards.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should display opening families by category', async ({ page }) => {
|
||||
// Check that family cards are displayed (wait for them to load)
|
||||
const familyCards = page.locator('button[aria-label*="opening family"]');
|
||||
|
||||
// Wait for at least one family card to be visible
|
||||
await expect(familyCards.first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Give a moment for other cards to render
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Should have at least one family displayed (could be more depending on data)
|
||||
const count = await familyCards.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('should select an opening family', async ({ page }) => {
|
||||
// Look for a family card (e.g., one with "Variations:" text)
|
||||
const familyCard = page.locator('button[aria-label*="opening family"]').first();
|
||||
await expect(familyCard).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Get the family name before clicking
|
||||
const familyName = await familyCard.locator('h3').textContent();
|
||||
|
||||
// Click on the family
|
||||
await familyCard.click();
|
||||
|
||||
// Should see the family name in the selector or openings list
|
||||
await page.waitForTimeout(1000);
|
||||
});
|
||||
|
||||
test('should navigate directly to an opening', async ({ page }) => {
|
||||
// Test direct navigation first
|
||||
await page.goto('/learning/openings/A01');
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
console.log('Current URL:', page.url());
|
||||
|
||||
// Check if we're on the right page
|
||||
const pageContent = await page.textContent('body');
|
||||
console.log('Page contains Opening Training:', pageContent?.includes('Opening Training'));
|
||||
console.log('Page contains Nimzo-Larsen:', pageContent?.includes('Nimzo-Larsen'));
|
||||
|
||||
// Verify chess board or loading state
|
||||
const hasBoard = await page.locator('[aria-label*="Chess board"]').count();
|
||||
const hasLoading = await page.locator('text="Initializing training session"').count();
|
||||
const hasNoSession = await page.locator('text="No active session"').count();
|
||||
|
||||
console.log('Has chessboard:', hasBoard);
|
||||
console.log('Has loading:', hasLoading);
|
||||
console.log('Has no session:', hasNoSession);
|
||||
});
|
||||
|
||||
test('should start a training session for an opening', async ({ page }) => {
|
||||
// Enable console logging
|
||||
const consoleLogs: Array<{ type: string; text: string }> = [];
|
||||
page.on('console', msg => {
|
||||
consoleLogs.push({ type: msg.type(), text: msg.text() });
|
||||
console.log(`[Browser ${msg.type()}]:`, msg.text());
|
||||
});
|
||||
|
||||
// Track page errors
|
||||
const pageErrors: string[] = [];
|
||||
page.on('pageerror', error => {
|
||||
pageErrors.push(error.message);
|
||||
console.error('Page Error:', error.message);
|
||||
});
|
||||
|
||||
// First, select a family to show opening cards
|
||||
const familyCard = page.locator('button[aria-label*="opening family"]').first();
|
||||
await expect(familyCard).toBeVisible({ timeout: 10000 });
|
||||
await familyCard.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Now get opening cards using data-testid
|
||||
const openingCards = page.locator('[data-testid^="opening-card-"]');
|
||||
const count = await openingCards.count();
|
||||
console.log('Found opening cards:', count);
|
||||
|
||||
// Get first opening card
|
||||
const firstOpening = openingCards.first();
|
||||
const eco = await firstOpening.getAttribute('data-opening-eco');
|
||||
const openingText = await firstOpening.textContent();
|
||||
console.log('Clicking on opening:', eco, openingText?.substring(0, 30));
|
||||
|
||||
const currentUrl = page.url();
|
||||
console.log('Current URL before click:', currentUrl);
|
||||
|
||||
await firstOpening.click();
|
||||
|
||||
// Wait a bit for navigation to start
|
||||
await page.waitForTimeout(500);
|
||||
console.log('URL after click:', page.url());
|
||||
|
||||
// Wait for training page to load
|
||||
try {
|
||||
await page.waitForURL(/\/learning\/openings\/[A-E]\d{2}/, { timeout: 10000 });
|
||||
console.log('Successfully navigated to:', page.url());
|
||||
} catch (e) {
|
||||
console.error('Failed to navigate. Current URL:', page.url());
|
||||
console.error('Console logs:', consoleLogs.slice(-10)); // Last 10 logs
|
||||
console.error('Page errors:', pageErrors);
|
||||
|
||||
// Take screenshot for debugging
|
||||
await page.screenshot({ path: 'e2e/screenshots/navigation-failed.png', fullPage: true });
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Wait for the page to settle
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Log any errors that occurred
|
||||
if (pageErrors.length > 0) {
|
||||
console.error('Page errors found:', pageErrors);
|
||||
await page.screenshot({ path: 'e2e/screenshots/page-errors.png', fullPage: true });
|
||||
}
|
||||
|
||||
// Verify chess board is visible
|
||||
await expect(page.locator('[aria-label*="Chess board"]')).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Verify move history section
|
||||
await expect(page.getByText('Move History')).toBeVisible();
|
||||
|
||||
// Verify Wikipedia summary loads (check for any of the possible states)
|
||||
const wikiSection = page.locator('text=/Loading opening background|No background information|Wikipedia/i');
|
||||
await expect(wikiSection.first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Ensure no critical errors occurred
|
||||
expect(pageErrors.length).toBe(0);
|
||||
});
|
||||
|
||||
test('should show session recovery dialog for existing session', async ({
|
||||
page,
|
||||
}) => {
|
||||
// First, select a family to show opening cards
|
||||
const familyCard = page.locator('button[aria-label*="opening family"]').first();
|
||||
await expect(familyCard).toBeVisible({ timeout: 10000 });
|
||||
await familyCard.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Start a session
|
||||
const firstOpening = page.locator('[data-testid^="opening-card-"]').first();
|
||||
await firstOpening.click();
|
||||
|
||||
// Wait for session to initialize
|
||||
await page.waitForURL(/\/learning\/openings\/[A-E]\d{2}/);
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Navigate away and back
|
||||
await page.goBack();
|
||||
|
||||
// Select family again
|
||||
const familyCardAgain = page.locator('button[aria-label*="opening family"]').first();
|
||||
await familyCardAgain.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const firstOpeningAgain = page.locator('[data-testid^="opening-card-"]').first();
|
||||
await firstOpeningAgain.click();
|
||||
|
||||
// Expect recovery dialog if session has moves
|
||||
// (This would need actual moves to be made in a more complete test)
|
||||
await page.waitForURL(/\/learning\/openings\/[A-E]\d{2}/);
|
||||
});
|
||||
|
||||
test.skip('should make a move and receive feedback', async ({ page }) => {
|
||||
// NOTE: This test is skipped because it requires:
|
||||
// 1. Stockfish engine to be loaded
|
||||
// 2. Move interaction with the chess board
|
||||
// 3. LLM API to be available
|
||||
//
|
||||
// To implement:
|
||||
// 1. Start a training session
|
||||
// 2. Make a move on the board (drag and drop)
|
||||
// 3. Wait for engine evaluation
|
||||
// 4. Verify move feedback is displayed
|
||||
// 5. Verify categorization (in-theory/playable/weak)
|
||||
// 6. Verify LLM explanation appears
|
||||
});
|
||||
|
||||
test.skip('should navigate through move history', async ({ page }) => {
|
||||
// NOTE: This test is skipped - requires moves to be made first
|
||||
//
|
||||
// To implement:
|
||||
// 1. Make several moves
|
||||
// 2. Click "Back" button
|
||||
// 3. Verify board position changes
|
||||
// 4. Verify feedback updates
|
||||
// 5. Click "Forward" button
|
||||
// 6. Verify board returns to later position
|
||||
});
|
||||
|
||||
test.skip('should detect deviation from theory', async ({ page }) => {
|
||||
// NOTE: This test is skipped - requires specific opening and off-book move
|
||||
//
|
||||
// To implement:
|
||||
// 1. Start a known opening
|
||||
// 2. Make moves following theory
|
||||
// 3. Make an off-book move
|
||||
// 4. Verify "Off-book since move X" indicator appears
|
||||
// 5. Verify feedback categorizes move as playable or weak (not in-theory)
|
||||
});
|
||||
|
||||
test.skip('should cache LLM explanations on navigation', async ({ page }) => {
|
||||
// NOTE: This test is skipped - requires checking network requests
|
||||
//
|
||||
// To implement:
|
||||
// 1. Make a move and wait for LLM explanation
|
||||
// 2. Navigate to different move
|
||||
// 3. Navigate back to original move
|
||||
// 4. Verify no new LLM API call was made (check network tab)
|
||||
// 5. Verify explanation is displayed immediately
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Performance Tests
|
||||
*/
|
||||
test.describe('Opening Training Performance', () => {
|
||||
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.skip('should initialize session within 3 seconds', async ({ page }) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
await page.goto('/learning/openings');
|
||||
const firstOpening = page.locator('[data-testid^="opening-card-"]').first();
|
||||
await firstOpening.click();
|
||||
|
||||
await page.waitForURL(/\/learning\/openings\/[A-E]\d{2}/);
|
||||
await page.waitForSelector('[aria-label*="Chess board"]');
|
||||
|
||||
const loadTime = Date.now() - startTime;
|
||||
expect(loadTime).toBeLessThan(3000);
|
||||
});
|
||||
|
||||
test.skip('should evaluate position within 2 seconds', async ({ page }) => {
|
||||
// NOTE: Requires making a move and measuring eval time
|
||||
// Target: Engine evaluation completes in <2s for 95% of moves
|
||||
});
|
||||
|
||||
test.skip('should provide complete feedback within 5 seconds', async ({
|
||||
page,
|
||||
}) => {
|
||||
// NOTE: Requires making a move and measuring total feedback time
|
||||
// Target: Engine eval + LLM explanation in <5s for 90% of moves
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Accessibility Tests
|
||||
*/
|
||||
test.describe('Opening Training Accessibility', () => {
|
||||
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 have proper ARIA labels on key elements', async ({ page }) => {
|
||||
await page.goto('/learning/openings');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Select a family first
|
||||
const familyCard = page.locator('button[aria-label*="opening family"]').first();
|
||||
await expect(familyCard).toBeVisible({ timeout: 10000 });
|
||||
await familyCard.click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Click on first opening
|
||||
const firstOpening = page.locator('[data-testid^="opening-card-"]').first();
|
||||
await firstOpening.click();
|
||||
|
||||
await page.waitForURL(/\/learning\/openings\/[A-E]\d{2}/);
|
||||
|
||||
// Verify ARIA labels
|
||||
await expect(page.locator('[aria-label*="Chess board"]')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('[aria-label="Move history and navigation"]')
|
||||
).toBeVisible();
|
||||
await expect(page.locator('[aria-label="Go to previous move"]')).toBeVisible();
|
||||
await expect(page.locator('[aria-label="Go to next move"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('should be keyboard navigable', async ({ page }) => {
|
||||
await page.goto('/learning/openings');
|
||||
|
||||
// Tab through elements
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Tab');
|
||||
|
||||
// Verify focus is visible
|
||||
const focused = await page.evaluate(() => document.activeElement?.tagName);
|
||||
expect(focused).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user