You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

306 lines
11 KiB
TypeScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<void> {
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<typeof test>[0]['request']
): Promise<void> {
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 lindice')).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)
})
})