diff --git a/frontend/apps/web/e2e/critical-flows-extended.spec.ts b/frontend/apps/web/e2e/critical-flows-extended.spec.ts index 94ebddc..55c3857 100644 --- a/frontend/apps/web/e2e/critical-flows-extended.spec.ts +++ b/frontend/apps/web/e2e/critical-flows-extended.spec.ts @@ -1,4 +1,13 @@ -import { expect, test } from '@playwright/test' +import { expect, test, type Page } from '@playwright/test' + +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) +} test.describe('critical frontend flows - extended', () => { test.beforeEach(async ({ page }) => { @@ -20,13 +29,7 @@ test.describe('critical frontend flows - extended', () => { test('start new game from home and render first playable question', async ({ page }) => { await page.goto('/') - const homeNameInput = page.getByTestId('home-player-name-input') - await homeNameInput.evaluate((el) => { - const input = el as HTMLInputElement - input.value = 'E2E Player' - input.dispatchEvent(new Event('input', { bubbles: true })) - input.dispatchEvent(new Event('change', { bubbles: true })) - }) + 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$/) @@ -46,38 +49,44 @@ test.describe('critical frontend flows - extended', () => { await expect(page.getByText(/Indice:/i)).toBeVisible() - const answerInput = page.getByLabel('Ta réponse') - await answerInput.evaluate((el) => { - const input = el as HTMLInputElement - input.value = '2' - input.dispatchEvent(new Event('input', { bubbles: true })) - input.dispatchEvent(new Event('change', { bubbles: true })) - }) + await setInputValue('[data-testid="game-answer-input"]', '2', page) + await page.getByRole('button', { name: 'Envoyer' }).click() + + await expect(page).toHaveURL(/\/game$/) + await expect(page.getByText('Score: 1')).toBeVisible() + await expect(page.getByText('Quelle planète est surnommée la planète rouge ?')).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 : 1')).toBeVisible() + await expect(page.getByText('Score final : 3')).toBeVisible() }) - test.fixme('after 3 wrong answers, game advances to next question', async ({ page }) => { + test('after 3 wrong answers, game advances to next question', async ({ page }) => { await page.goto('/game') - await page.getByLabel('Ta réponse').fill('wrong') + await setInputValue('[data-testid="game-answer-input"]', 'wrong', page) await page.getByRole('button', { name: 'Envoyer' }).click() - await page.getByLabel('Ta réponse').fill('wrong') + await setInputValue('[data-testid="game-answer-input"]', 'wrong', page) await page.getByRole('button', { name: 'Envoyer' }).click() - await page.getByLabel('Ta réponse').fill('wrong') + await setInputValue('[data-testid="game-answer-input"]', 'wrong', page) await page.getByRole('button', { name: 'Envoyer' }).click() - await expect(page.getByText('Question 2')).toBeVisible() + 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() }) - test.fixme('session timeout ends game and redirects to results', async ({ page }) => { + test('session timeout ends game and redirects to results', async ({ page }) => { + 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(/Temps écoulé/i)).toBeVisible() + await expect(page.getByText('Score final : 0')).toBeVisible() }) test('results remain visible after page reload', async ({ page }) => { @@ -105,21 +114,42 @@ test.describe('critical frontend flows - extended', () => { await expect(page.getByText('Position leaderboard : #2')).toBeVisible() }) - test.fixme('leaderboard shows empty and error states from API', async ({ page }) => { + 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.route('**/leaderboard/top10', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ items: [] }), + }) + }) await page.goto('/leaderboard') await expect(page.getByText('Aucun score pour le moment.')).toBeVisible() + await page.unroute('**/leaderboard/top10') + 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(/Erreur|Impossible/i)).toBeVisible() + await expect( + page.getByText('Impossible de charger le leaderboard. Veuillez réessayer.') + ).toBeVisible() }) - test.fixme('admin can create, edit, then delete a question', async ({ page }) => { + test('admin can create, edit, then delete a question', async ({ page }) => { await page.goto('/admin/questions') - await page.getByTestId('admin-theme-input').fill('Science') - await page.getByTestId('admin-question-input').fill('Quelle planète est rouge ?') - await page.getByTestId('admin-answer-input').fill('Mars') - await page.getByTestId('admin-hint-input').fill('4e planète') + await setInputValue('[data-testid="admin-theme-input"]', 'Science', page) + await setInputValue('[data-testid="admin-question-input"]', 'Quelle planète est rouge ?', page) + await setInputValue('[data-testid="admin-answer-input"]', 'Mars', page) + await setInputValue('[data-testid="admin-hint-input"]', '4e planète', page) await page.getByTestId('admin-create-question').click() await expect(page.getByText('Quelle planète est rouge ?')).toBeVisible() @@ -127,9 +157,11 @@ test.describe('critical frontend flows - extended', () => { .getByRole('button', { name: /Modifier/i }) .first() .click() - await page - .getByTestId('admin-question-input') - .fill('Quelle planète est appelée planète rouge ?') + await setInputValue( + '[data-testid="admin-question-input"]', + 'Quelle planète est appelée planète rouge ?', + page + ) await page.getByRole('button', { name: /Enregistrer la modification/i }).click() await expect(page.getByText('Quelle planète est appelée planète rouge ?')).toBeVisible() @@ -158,13 +190,7 @@ test.describe('critical frontend flows - mobile viewport', () => { localStorage.clear() }) - const mobileNameInput = page.getByTestId('home-player-name-input') - await mobileNameInput.evaluate((el) => { - const input = el as HTMLInputElement - input.value = 'Mobile Player' - input.dispatchEvent(new Event('input', { bubbles: true })) - input.dispatchEvent(new Event('change', { bubbles: true })) - }) + 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$/) diff --git a/frontend/apps/web/node_modules/.vite/vitest/results.json b/frontend/apps/web/node_modules/.vite/vitest/results.json index 65972cf..113a8cf 100644 --- a/frontend/apps/web/node_modules/.vite/vitest/results.json +++ b/frontend/apps/web/node_modules/.vite/vitest/results.json @@ -1 +1 @@ -{"version":"1.6.1","results":[[":src/hooks/useAuth.test.ts",{"duration":2,"failed":false}],[":src/services/session.test.ts",{"duration":4,"failed":false}],[":src/services/adminQuestions.test.ts",{"duration":1,"failed":false}],[":src/hooks/useTimer.test.ts",{"duration":3,"failed":false}],[":src/services/validation.test.ts",{"duration":2,"failed":false}],[":src/services/api.test.ts",{"duration":2,"failed":false}],[":src/routes/Home.test.tsx",{"duration":96,"failed":false}],[":src/routes/Profile.test.tsx",{"duration":111,"failed":false}],[":src/components/AppShell.test.tsx",{"duration":42,"failed":false}],[":src/routes/AdminQuestions.test.tsx",{"duration":113,"failed":false}],[":src/routes/Leaderboard.test.tsx",{"duration":89,"failed":false}],[":src/routes/Results.test.tsx",{"duration":31,"failed":false}],[":src/routes/Game.test.tsx",{"duration":98,"failed":false}]]} \ No newline at end of file +{"version":"1.6.1","results":[[":src/services/adminQuestions.test.ts",{"duration":2,"failed":false}],[":src/hooks/useTimer.test.ts",{"duration":3,"failed":false}],[":src/services/api.test.ts",{"duration":5,"failed":false}],[":src/services/session.test.ts",{"duration":3,"failed":false}],[":src/routes/Home.test.tsx",{"duration":104,"failed":false}],[":src/components/AppShell.test.tsx",{"duration":42,"failed":false}],[":src/hooks/useAuth.test.ts",{"duration":2,"failed":false}],[":src/routes/Results.test.tsx",{"duration":30,"failed":false}],[":src/routes/Leaderboard.test.tsx",{"duration":86,"failed":false}],[":src/routes/Game.test.tsx",{"duration":147,"failed":false}],[":src/routes/Profile.test.tsx",{"duration":115,"failed":false}],[":src/services/validation.test.ts",{"duration":2,"failed":false}],[":src/routes/AdminQuestions.test.tsx",{"duration":108,"failed":false}]]} \ No newline at end of file diff --git a/frontend/apps/web/src/routes/AdminQuestions.tsx b/frontend/apps/web/src/routes/AdminQuestions.tsx index c339fd5..7270e29 100644 --- a/frontend/apps/web/src/routes/AdminQuestions.tsx +++ b/frontend/apps/web/src/routes/AdminQuestions.tsx @@ -13,6 +13,7 @@ import { createAdminQuestion, deleteAdminQuestion, listAdminQuestions, + updateAdminQuestion, type AdminQuestion, type CreateQuestionInput, } from '../services/adminQuestions' @@ -36,6 +37,7 @@ export default function AdminQuestionsRoute(): JSX.Element { const [items, setItems] = createSignal(listAdminQuestions()) const [form, setForm] = createSignal(defaultState) const [error, setError] = createSignal(null) + const [editingId, setEditingId] = createSignal(null) const updateField = (key: keyof FormState, value: string): void => { setForm((prev) => ({ ...prev, [key]: value }) as FormState) @@ -48,15 +50,47 @@ export default function AdminQuestionsRoute(): JSX.Element { return } - createAdminQuestion(current) + if (editingId()) { + const updated = updateAdminQuestion(editingId() as string, current) + if (!updated) { + setError('Impossible de modifier la question.') + return + } + } else { + createAdminQuestion(current) + } setItems(listAdminQuestions()) setForm(defaultState) setError(null) + setEditingId(null) } const remove = (id: string): void => { deleteAdminQuestion(id) setItems(listAdminQuestions()) + if (editingId() === id) { + setEditingId(null) + setForm(defaultState) + setError(null) + } + } + + const edit = (item: AdminQuestion): void => { + setEditingId(item.id) + setForm({ + theme: item.theme, + text: item.text, + answer: item.answer, + hint: item.hint, + difficulty: item.difficulty, + }) + setError(null) + } + + const cancelEdit = (): void => { + setEditingId(null) + setForm(defaultState) + setError(null) } return ( @@ -70,6 +104,9 @@ export default function AdminQuestionsRoute(): JSX.Element { Créer une question + {editingId() && ( + Mode édition: {editingId()} + )} {error() && {error()}} hard - + + + {editingId() && ( + + )} + @@ -138,6 +182,13 @@ export default function AdminQuestionsRoute(): JSX.Element { {item.text} Theme: {item.theme} Réponse: {item.answer} +