Improved E2E testing

master
oabrivard 1 month ago
parent 955d8dadfe
commit bf19f57961

@ -0,0 +1,175 @@
import { expect, test } from '@playwright/test'
test.describe('critical frontend flows - extended', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/')
await page.evaluate(() => {
localStorage.clear()
})
})
test('profile route requires auth, then allows access after demo login', async ({ page }) => {
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()
})
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 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()
})
test('hint usage reduces awarded score for a correct answer', async ({ page }) => {
await page.goto('/game')
await page.getByRole('button', { name: 'Indice (score réduit)' }).click()
await expect(page.getByText('Confirmer lindice')).toBeVisible()
await page.locator('button', { hasText: 'Oui, utiliser un indice' }).click()
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 page.getByRole('button', { name: 'Envoyer' }).click()
await expect(page).toHaveURL(/\/results$/)
await expect(page.getByText('Score final : 1')).toBeVisible()
})
test.fixme('after 3 wrong answers, game advances to next question', async ({ page }) => {
await page.goto('/game')
await page.getByLabel('Ta réponse').fill('wrong')
await page.getByRole('button', { name: 'Envoyer' }).click()
await page.getByLabel('Ta réponse').fill('wrong')
await page.getByRole('button', { name: 'Envoyer' }).click()
await page.getByLabel('Ta réponse').fill('wrong')
await page.getByRole('button', { name: 'Envoyer' }).click()
await expect(page.getByText('Question 2')).toBeVisible()
await expect(page.getByText("Plus d'essais.")).not.toBeVisible()
})
test.fixme('session timeout ends game and redirects to results', async ({ page }) => {
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()
})
test('results remain visible after page reload', async ({ page }) => {
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()
})
test.fixme('leaderboard shows empty and error states from API', async ({ page }) => {
await page.goto('/leaderboard')
await expect(page.getByText('Aucun score pour le moment.')).toBeVisible()
await page.getByRole('button', { name: 'Rafraîchir' }).click()
await expect(page.getByText(/Erreur|Impossible/i)).toBeVisible()
})
test.fixme('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 page.getByTestId('admin-create-question').click()
await expect(page.getByText('Quelle planète est rouge ?')).toBeVisible()
await page
.getByRole('button', { name: /Modifier/i })
.first()
.click()
await page
.getByTestId('admin-question-input')
.fill('Quelle planète est appelée planète rouge ?')
await page.getByRole('button', { name: /Enregistrer la modification/i }).click()
await expect(page.getByText('Quelle planète est appelée planète rouge ?')).toBeVisible()
await page.getByRole('button', { name: 'Supprimer' }).first().click()
await expect(page.getByText('Quelle planète est appelée planète rouge ?')).toHaveCount(0)
})
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('critical frontend flows - mobile viewport', () => {
test.use({ viewport: { width: 390, height: 844 } })
test('mobile player flow keeps core controls visible', async ({ page }) => {
await page.goto('/')
await page.evaluate(() => {
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 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()
})
})

@ -28,7 +28,8 @@ const defaultState: FormState = {
} }
function readInputValue(event: Event): string { function readInputValue(event: Event): string {
return (event.target as HTMLInputElement).value const input = event.currentTarget as HTMLInputElement | null
return input?.value ?? (event.target as HTMLInputElement).value
} }
export default function AdminQuestionsRoute(): JSX.Element { export default function AdminQuestionsRoute(): JSX.Element {
@ -75,6 +76,7 @@ export default function AdminQuestionsRoute(): JSX.Element {
label="Theme" label="Theme"
value={form().theme} value={form().theme}
onInput={(event) => updateField('theme', readInputValue(event))} onInput={(event) => updateField('theme', readInputValue(event))}
onChange={(event) => updateField('theme', readInputValue(event))}
inputProps={{ 'data-testid': 'admin-theme-input' }} inputProps={{ 'data-testid': 'admin-theme-input' }}
fullWidth fullWidth
/> />
@ -83,6 +85,7 @@ export default function AdminQuestionsRoute(): JSX.Element {
label="Question" label="Question"
value={form().text} value={form().text}
onInput={(event) => updateField('text', readInputValue(event))} onInput={(event) => updateField('text', readInputValue(event))}
onChange={(event) => updateField('text', readInputValue(event))}
inputProps={{ 'data-testid': 'admin-question-input' }} inputProps={{ 'data-testid': 'admin-question-input' }}
fullWidth fullWidth
/> />
@ -91,6 +94,7 @@ export default function AdminQuestionsRoute(): JSX.Element {
label="Réponse" label="Réponse"
value={form().answer} value={form().answer}
onInput={(event) => updateField('answer', readInputValue(event))} onInput={(event) => updateField('answer', readInputValue(event))}
onChange={(event) => updateField('answer', readInputValue(event))}
inputProps={{ 'data-testid': 'admin-answer-input' }} inputProps={{ 'data-testid': 'admin-answer-input' }}
fullWidth fullWidth
/> />
@ -99,6 +103,7 @@ export default function AdminQuestionsRoute(): JSX.Element {
label="Indice" label="Indice"
value={form().hint} value={form().hint}
onInput={(event) => updateField('hint', readInputValue(event))} onInput={(event) => updateField('hint', readInputValue(event))}
onChange={(event) => updateField('hint', readInputValue(event))}
inputProps={{ 'data-testid': 'admin-hint-input' }} inputProps={{ 'data-testid': 'admin-hint-input' }}
fullWidth fullWidth
/> />

@ -12,7 +12,8 @@ import Typography from '@suid/material/Typography'
import { validatePlayerName } from '../services/validation' import { validatePlayerName } from '../services/validation'
function readInputValue(event: Event): string { function readInputValue(event: Event): string {
return (event.target as HTMLInputElement).value const input = event.currentTarget as HTMLInputElement | null
return input?.value ?? (event.target as HTMLInputElement).value
} }
export default function HomeRoute(): JSX.Element { export default function HomeRoute(): JSX.Element {
@ -48,6 +49,7 @@ export default function HomeRoute(): JSX.Element {
label="Nom de joueur" label="Nom de joueur"
value={playerName()} value={playerName()}
onInput={(e) => setPlayerName(readInputValue(e))} onInput={(e) => setPlayerName(readInputValue(e))}
onChange={(e) => setPlayerName(readInputValue(e))}
error={!!error()} error={!!error()}
helperText={error() ?? '250 caractères'} helperText={error() ?? '250 caractères'}
fullWidth fullWidth

@ -14,7 +14,8 @@ import { useAuth } from '../hooks/useAuth'
import { loadGameHistory } from '../services/session' import { loadGameHistory } from '../services/session'
function readInputValue(event: Event): string { function readInputValue(event: Event): string {
return (event.target as HTMLInputElement).value const input = event.currentTarget as HTMLInputElement | null
return input?.value ?? (event.target as HTMLInputElement).value
} }
export default function ProfileRoute(): JSX.Element { export default function ProfileRoute(): JSX.Element {
@ -111,6 +112,7 @@ export default function ProfileRoute(): JSX.Element {
label="Nom de joueur" label="Nom de joueur"
value={name()} value={name()}
onInput={(e) => setName(readInputValue(e))} onInput={(e) => setName(readInputValue(e))}
onChange={(e) => setName(readInputValue(e))}
inputProps={{ 'data-testid': 'profile-player-name-input' }} inputProps={{ 'data-testid': 'profile-player-name-input' }}
fullWidth fullWidth
InputLabelProps={{ style: { color: '#cbd5e1' } }} InputLabelProps={{ style: { color: '#cbd5e1' } }}

@ -17,7 +17,14 @@ const AnswerInput: Component<AnswerInputProps> = (props) => {
label={props.label ?? 'Ta réponse'} label={props.label ?? 'Ta réponse'}
placeholder={props.placeholder} placeholder={props.placeholder}
value={props.value} value={props.value}
onInput={(event) => props.onInputValue((event.target as HTMLInputElement).value)} onInput={(event) => {
const input = event.currentTarget as HTMLInputElement | null
props.onInputValue(input?.value ?? (event.target as HTMLInputElement).value)
}}
onChange={(event) => {
const input = event.currentTarget as HTMLInputElement | null
props.onInputValue(input?.value ?? (event.target as HTMLInputElement).value)
}}
fullWidth fullWidth
disabled={props.disabled} disabled={props.disabled}
InputLabelProps={{ style: { color: '#cbd5e1' } }} InputLabelProps={{ style: { color: '#cbd5e1' } }}

Loading…
Cancel
Save