import { expect, test, type Page } from '@playwright/test' import { countActiveQuestions, countLeaderboardEntriesByPlayer, questionIsActive, resetDatabases, seedQuestions, } from './helpers/stack' const apiBase = process.env.FULLSTACK_API_BASE_URL ?? 'http://127.0.0.1:18096' async function setInputValue(selector: string, value: string, page: Page): Promise { await page.locator(selector).evaluate((el, next) => { const input = el as HTMLInputElement input.value = next input.dispatchEvent(new Event('input', { bubbles: true })) input.dispatchEvent(new Event('change', { bubbles: true })) }, value) } async function assertBackendReachable( request: Parameters[0]['request'] ): Promise { const resp = await request.post(`${apiBase}/api/v1/questions/random`, { data: {} }) expect(resp.ok()).toBeTruthy() expect(countActiveQuestions()).toBeGreaterThan(0) } test.describe('full-stack parity with frontend e2e scenarios', () => { test.beforeEach(async ({ page }) => { resetDatabases() seedQuestions() await page.goto('/') await page.evaluate(() => { localStorage.clear() }) }) test('player registration and demo login flow', async ({ page, request }) => { await page.goto('/') await page.evaluate(() => { localStorage.setItem('kf.playerName', 'E2E Player') }) await page.goto('/game') await expect(page).toHaveURL(/\/game$/) await page.goto('/profile') await expect(page.getByText('Connexion requise pour accéder au profil joueur.')).toBeVisible() await page.getByRole('button', { name: 'Se connecter (mode démo)' }).click() await expect(page.getByText('Statistiques joueur')).toBeVisible() await assertBackendReachable(request) }) test('complete game session flow', async ({ page, request }) => { await page.goto('/') await page.evaluate(() => { localStorage.setItem( 'kf.lastResult', JSON.stringify({ playerName: 'Game Runner', finalScore: 2, answered: 1, correct: 1, successRate: 100, durationSec: 45, leaderboardPosition: 1, finishedAt: new Date().toISOString(), }) ) }) await page.goto('/results') await expect(page).toHaveURL(/\/results$/) await expect(page.getByText('Score final : 2')).toBeVisible() await assertBackendReachable(request) }) test('leaderboard viewing flow', async ({ page, request }) => { const playerID = `player-${Date.now()}` const playerName = `Parity${Date.now().toString().slice(-4)}` const resp = await request.post(`${apiBase}/api/v1/leaderboard/update`, { data: { session_id: `sess-${Date.now()}`, player_id: playerID, player_name: playerName, total_score: 11, questions_asked: 6, questions_correct: 5, hints_used: 1, duration_seconds: 123, completed_at: new Date().toISOString(), completion_type: 'completed', }, }) expect(resp.ok()).toBeTruthy() await page.goto('/leaderboard') await expect(page.getByRole('heading', { name: 'Leaderboard' })).toBeVisible() await expect(page.getByRole('table', { name: 'top-10-leaderboard' })).toBeVisible() await expect(page.getByText(playerName)).toBeVisible() expect(countLeaderboardEntriesByPlayer(playerID)).toBe(1) }) test('admin question management flow', async ({ page, request }) => { await page.goto('/admin/questions') await expect(page.getByText('Quel est le plus petit nombre premier ?')).toBeVisible() await page.getByRole('button', { name: 'Supprimer' }).first().click() await expect(page.getByText('Quel est le plus petit nombre premier ?')).toHaveCount(0) const deleteResp = await request.delete( `${apiBase}/api/v1/admin/questions/00000000-0000-0000-0000-000000000001` ) expect(deleteResp.status()).toBe(204) expect(questionIsActive('00000000-0000-0000-0000-000000000001')).toBeFalsy() }) test('profile route requires auth, then allows access after demo login', async ({ page, request, }) => { await page.goto('/profile') await expect(page.getByText('Connexion requise pour accéder au profil joueur.')).toBeVisible() await page.getByRole('button', { name: 'Se connecter (mode démo)' }).click() await expect(page.getByText('Statistiques joueur')).toBeVisible() await expect(page.getByText(/Parties jouées/i)).toBeVisible() await assertBackendReachable(request) }) test('start new game from home and render first playable question', async ({ page, request }) => { await page.goto('/') await setInputValue('[data-testid="home-player-name-input"]', 'E2E Player', page) await page.getByRole('button', { name: 'Démarrer la partie' }).click() await expect(page).toHaveURL(/\/game$/) await expect(page.getByRole('heading', { name: 'Partie' })).toBeVisible() await expect(page.getByText('Thème : Général')).toBeVisible() await expect(page.getByText('Quel est le plus petit nombre premier ?')).toBeVisible() await expect(page.getByLabel('Ta réponse')).toBeVisible() await expect(page.getByText('3 restant(s)')).toBeVisible() await assertBackendReachable(request) }) test('hint usage reduces awarded score for a correct answer', async ({ page, request }) => { await page.goto('/game') await page.getByRole('button', { name: 'Indice (score réduit)' }).click() await expect(page.getByText('Confirmer l’indice')).toBeVisible() await page.locator('button', { hasText: 'Oui, utiliser un indice' }).click() await expect(page.getByText(/Indice:/i)).toBeVisible() await setInputValue('[data-testid="game-answer-input"]', '2', page) await page.getByRole('button', { name: 'Envoyer' }).click() await expect(page.getByText('Score: 1')).toBeVisible() await setInputValue('[data-testid="game-answer-input"]', 'mars', page) await page.getByRole('button', { name: 'Envoyer' }).click() await expect(page).toHaveURL(/\/results$/) await expect(page.getByText('Score final : 3')).toBeVisible() await assertBackendReachable(request) }) test('after 3 wrong answers, game advances to next question', async ({ page, request }) => { await page.goto('/game') await setInputValue('[data-testid="game-answer-input"]', 'wrong', page) await page.getByRole('button', { name: 'Envoyer' }).click() await setInputValue('[data-testid="game-answer-input"]', 'wrong', page) await page.getByRole('button', { name: 'Envoyer' }).click() await setInputValue('[data-testid="game-answer-input"]', 'wrong', page) await page.getByRole('button', { name: 'Envoyer' }).click() await expect(page).toHaveURL(/\/game$/) await expect(page.getByText('Quelle planète est surnommée la planète rouge ?')).toBeVisible() await expect(page.getByText("Plus d'essais.")).not.toBeVisible() await assertBackendReachable(request) }) test('session timeout ends game and redirects to results', async ({ page, request }) => { await page.goto('/') await page.evaluate(() => { localStorage.setItem('kf.e2e.timerMs', '300') }) await page.goto('/game') await expect(page.getByRole('heading', { name: 'Partie' })).toBeVisible() await expect(page).toHaveURL(/\/results$/) await expect(page.getByText('Score final : 0')).toBeVisible() await assertBackendReachable(request) }) test('results remain visible after page reload', async ({ page, request }) => { await page.goto('/') await page.evaluate(() => { localStorage.setItem( 'kf.lastResult', JSON.stringify({ playerName: 'Reload Tester', finalScore: 7, answered: 4, correct: 3, successRate: 75, durationSec: 95, leaderboardPosition: 2, finishedAt: new Date().toISOString(), }) ) }) await page.goto('/results') await expect(page.getByText('Score final : 7')).toBeVisible() await page.reload() await expect(page.getByText('Score final : 7')).toBeVisible() await expect(page.getByText('Position leaderboard : #2')).toBeVisible() await assertBackendReachable(request) }) test('leaderboard shows empty and error states from API', async ({ page }) => { await page.goto('/') await page.evaluate(() => { localStorage.setItem('kf.leaderboard.mode', 'api') }) await page.goto('/leaderboard') await expect(page.getByText('Aucun score pour le moment.')).toBeVisible() await page.route('**/leaderboard/top10', async (route) => { await route.fulfill({ status: 500, contentType: 'application/json', body: JSON.stringify({ error: 'boom' }), }) }) await page.getByRole('button', { name: 'Rafraîchir' }).click() await expect( page.getByText('Impossible de charger le leaderboard. Veuillez réessayer.') ).toBeVisible() }) test('admin can create, edit, then delete a question', async ({ request }) => { const createResp = await request.post(`${apiBase}/api/v1/admin/questions`, { data: { theme: 'Science', text: 'Quelle planète est rouge ?', answer: 'Mars', hint: '4e planète', difficulty: 'medium', }, }) expect(createResp.status()).toBe(201) const created = await createResp.json() const id = created.data.id as string const updateResp = await request.put(`${apiBase}/api/v1/admin/questions/${id}`, { data: { theme: 'Science', text: 'Quelle planète est appelée planète rouge ?', answer: 'Mars', hint: '4e planète', difficulty: 'hard', is_active: true, }, }) expect(updateResp.ok()).toBeTruthy() const deleteResp = await request.delete(`${apiBase}/api/v1/admin/questions/${id}`) expect(deleteResp.status()).toBe(204) expect(questionIsActive(id)).toBeFalsy() }) test('admin form validation blocks invalid submission', async ({ page }) => { await page.goto('/admin/questions') await page.getByTestId('admin-theme-input').fill('') await page.getByTestId('admin-question-input').fill('') await page.getByTestId('admin-answer-input').fill('') await page.getByTestId('admin-create-question').click() await expect(page.getByText('Theme, question et réponse sont requis.')).toBeVisible() }) }) test.describe('full-stack parity mobile scenario', () => { test.use({ viewport: { width: 390, height: 844 } }) test.beforeEach(async ({ page }) => { resetDatabases() seedQuestions() await page.goto('/') await page.evaluate(() => { localStorage.clear() }) }) test('mobile player flow keeps core controls visible', async ({ page, request }) => { await page.goto('/') await setInputValue('[data-testid="home-player-name-input"]', 'Mobile Player', page) await page.getByRole('button', { name: 'Démarrer la partie' }).click() await expect(page).toHaveURL(/\/game$/) await expect(page.getByRole('button', { name: 'Envoyer' })).toBeVisible() await expect(page.getByRole('button', { name: 'Indice (score réduit)' })).toBeVisible() await expect(page.getByLabel('Ta réponse')).toBeVisible() await assertBackendReachable(request) }) })