Files
chess-project/e2e/tactical-practice.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

221 lines
8.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
});
});