Files
chess-project/e2e/opening-training.spec.ts
Stefan 8ed402bbe0 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>
2025-12-08 15:05:05 +01:00

335 lines
12 KiB
TypeScript

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