/** * Playwright E2E tests for mini-games scoring flows * Run: npx playwright test tests/e2e/mini-games-playwright.spec.js */ const { test, expect } = require('@playwright/test'); test.describe('Mini-Games E2E - Playwright', () => { test.beforeEach(async ({ page }) => { // Navigate to the mini-games list page (uni-app H5 hash router) // Requires `npm run dev:h5` running in cfc-frontend await page.goto('/#/pages/games/list'); // Wait for page to load await page.waitForLoadState('networkidle'); }); test('1a2b - should display game and start correctly', async ({ page }) => { // Navigate to 1a2b game await page.click('text=猜数字'); // Wait for game page to load await page.waitForSelector('.game-container'); // Verify game title await expect(page.locator('.game-title')).toContainText('猜数字'); // Verify initial state await expect(page.locator('.instructions')).toBeVisible(); }); test('1a2b - should calculate and display score on win', async ({ page }) => { await page.goto('/#/pages/games/1a2b'); // Mock the API response await page.route('**/api/mini-game/complete', async route => { await route.fulfill({ status: 200, body: JSON.stringify({ code: 200, data: { pointsEarned: 10, newBalance: 110 } }) }); }); // Start game with input await page.fill('input[placeholder="输入4位不重复数字"]', '1234'); await page.click('button:has-text("开始游戏")'); // If first guess is correct (lucky), verify result modal // Note: In real scenario, we'd need to mock the answer generation const resultModal = page.locator('.result-modal'); if (await resultModal.isVisible()) { await expect(resultModal.locator('.result-score')).toContainText('积分'); } }); test('1a2b - should handle missing childId gracefully', async ({ page }) => { await page.goto('/#/pages/games/1a2b'); // Clear any stored user data await page.evaluate(() => { uni.removeStorageSync('userId'); uni.removeStorageSync('token'); }); // Start game await page.fill('input[placeholder="输入4位不重复数字"]', '1234'); await page.click('button:has-text("开始游戏")'); // Game should still work, but score submission should be skipped // No error should be thrown await page.waitForSelector('.game-container', { state: 'visible' }); }); test('Sudoku - should display difficulty selection', async ({ page }) => { await page.goto('/#/pages/games/sudoku'); // Verify difficulty buttons await expect(page.locator('.level-btn:has-text("简单")')).toBeVisible(); await expect(page.locator('.level-btn:has-text("中等")')).toBeVisible(); await expect(page.locator('.level-btn:has-text("困难")')).toBeVisible(); }); test('Sudoku - should start game and display board', async ({ page }) => { await page.goto('/#/pages/games/sudoku'); // Select difficulty await page.click('.level-btn:has-text("简单")'); // Start game await page.click('button:has-text("开始游戏")'); // Verify board is displayed await expect(page.locator('.board')).toBeVisible(); // Verify number pad await expect(page.locator('.number-pad')).toBeVisible(); }); test('Sudoku - should calculate score based on difficulty and time', async ({ page }) => { await page.goto('/#/pages/games/sudoku'); // Mock API await page.route('**/api/mini-game/complete', async route => { await route.fulfill({ status: 200, body: JSON.stringify({ code: 200, data: { pointsEarned: 12, newBalance: 112 } }) }); }); // Select hard difficulty await page.click('.level-btn:has-text("困难")'); await page.click('button:has-text("开始游戏")'); // Simulate completion (in real test, would need to solve the puzzle) // This is a placeholder for actual game completion logic const board = page.locator('.board'); await expect(board).toBeVisible(); }); test('API - should call completeMiniGame with correct params', async ({ page }) => { await page.goto('/#/pages/games/schulte'); // Intercept API call let apiCallParams = null; await page.route('**/api/mini-game/complete', async (route, request) => { apiCallParams = request.postDataJSON(); await route.fulfill({ status: 200, body: JSON.stringify({ code: 200, data: { pointsEarned: 8, newBalance: 108 } }) }); }); // Start and complete Schulte game await page.click('button:has-text("开始游戏")'); // Click cells in order (1-25) // Note: This would need actual cell clicking logic // For now, we'll just verify the API would be called with correct params // If game completes, verify API call if (apiCallParams) { expect(apiCallParams.childId).toBeDefined(); expect(apiCallParams.gameCode).toBe('schulte'); expect(apiCallParams.completionTime).toBeGreaterThanOrEqual(0); expect(apiCallParams.score).toBeGreaterThanOrEqual(60); } }); test('UI - should display result modal with score', async ({ page }) => { await page.goto('/#/pages/games/schulte'); // Mock a completed game state await page.evaluate(() => { // This would set the game state to show results // In a real test, we'd complete the actual game window.testGameComplete = true; }); // If result modal is shown, verify it displays score const resultModal = page.locator('.result-modal'); if (await resultModal.isVisible({ timeout: 5000 }).catch(() => false)) { await expect(resultModal.locator('.result-title')).toBeVisible(); await expect(resultModal.locator('.result-score')).toContainText('积分'); await expect(resultModal.locator('button:has-text("再来")')).toBeVisible(); } }); test('Games List - should navigate to all games', async ({ page }) => { await page.goto('/#/pages/games/list'); // Verify all game cards are visible await expect(page.locator('text=舒尔特方格')).toBeVisible(); await expect(page.locator('text=猜数字')).toBeVisible(); await expect(page.locator('text=数独')).toBeVisible(); // Click and verify navigation await page.click('text=舒尔特方格'); await expect(page).toHaveURL(/.*schulte/); await page.goBack(); await page.click('text=数独'); await expect(page).toHaveURL(/.*sudoku/); }); }); // Integration test for score calculation logic test.describe('Score Calculation Logic', () => { test('1a2b score calculation', () => { const calculateScore = (guessesUsed) => Math.max(100 - guessesUsed * 8, 60); // Test various scenarios expect(calculateScore(1)).toBe(92); expect(calculateScore(2)).toBe(84); expect(calculateScore(5)).toBe(60); // Minimum expect(calculateScore(10)).toBe(60); }); test('Sudoku score calculation', () => { const calculateSudokuScore = (difficulty, time) => { const baseScore = { easy: 60, medium: 75, hard: 90 }; const base = baseScore[difficulty] || 60; const timeBonus = Math.max(40 - Math.floor(time / 10), 0); return base + timeBonus; }; // Test various scenarios expect(calculateSudokuScore('easy', 30)).toBe(97); expect(calculateSudokuScore('medium', 60)).toBe(109); expect(calculateSudokuScore('hard', 120)).toBe(118); expect(calculateSudokuScore('easy', 500)).toBe(60); // No time bonus }); test('Schulte score calculation', () => { const calculateSchulteScore = (time) => Math.max(100 - Math.floor(time * 2), 60); expect(calculateSchulteScore(10)).toBe(80); expect(calculateSchulteScore(20)).toBe(60); // Minimum expect(calculateSchulteScore(30)).toBe(60); }); });