8ed402bbe0
- 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>
335 lines
12 KiB
TypeScript
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();
|
|
});
|
|
});
|