Implemented step '6.2 Frontend Testing Strategy'
parent
05d950218c
commit
bbc93bd46b
@ -0,0 +1,63 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test.describe('critical frontend flows', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/')
|
||||
await page.evaluate(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
})
|
||||
|
||||
test('player registration and demo login flow', async ({ page }) => {
|
||||
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()
|
||||
})
|
||||
|
||||
test('complete game session flow', async ({ page }) => {
|
||||
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()
|
||||
})
|
||||
|
||||
test('leaderboard viewing flow', async ({ page }) => {
|
||||
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('Alice')).toBeVisible()
|
||||
})
|
||||
|
||||
test('admin question management flow', async ({ page }) => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
@ -1 +1 @@
|
||||
{"version":"1.6.1","results":[[":src/services/validation.test.ts",{"duration":1,"failed":false}],[":src/routes/Results.test.tsx",{"duration":28,"failed":false}],[":src/routes/Leaderboard.test.tsx",{"duration":85,"failed":false}],[":src/routes/Game.test.tsx",{"duration":86,"failed":false}]]}
|
||||
{"version":"1.6.1","results":[[":src/hooks/useTimer.test.ts",{"duration":3,"failed":false}],[":src/services/session.test.ts",{"duration":3,"failed":false}],[":src/services/adminQuestions.test.ts",{"duration":2,"failed":false}],[":src/routes/Home.test.tsx",{"duration":97,"failed":false}],[":src/components/AppShell.test.tsx",{"duration":45,"failed":false}],[":src/routes/Profile.test.tsx",{"duration":118,"failed":false}],[":src/hooks/useAuth.test.ts",{"duration":2,"failed":false}],[":src/services/validation.test.ts",{"duration":2,"failed":false}],[":src/routes/Results.test.tsx",{"duration":31,"failed":false}],[":src/routes/Game.test.tsx",{"duration":92,"failed":false}],[":src/routes/AdminQuestions.test.tsx",{"duration":112,"failed":false}],[":src/routes/Leaderboard.test.tsx",{"duration":94,"failed":false}],[":src/services/api.test.ts",{"duration":3,"failed":false}]]}
|
||||
@ -0,0 +1,23 @@
|
||||
import path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { defineConfig } from '@playwright/test'
|
||||
|
||||
const configDir = path.dirname(fileURLToPath(import.meta.url))
|
||||
const frontendRoot = path.resolve(configDir, '../..')
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
timeout: 30_000,
|
||||
use: {
|
||||
baseURL: process.env.PW_BASE_URL ?? 'http://127.0.0.1:4173',
|
||||
headless: true,
|
||||
},
|
||||
webServer: {
|
||||
command: 'yarn workspace @knowfoolery/web dev --host 127.0.0.1 --port 4173',
|
||||
cwd: frontendRoot,
|
||||
port: 4173,
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 60_000,
|
||||
},
|
||||
})
|
||||
@ -0,0 +1,26 @@
|
||||
import { render, screen } from '@solidjs/testing-library'
|
||||
import type { JSX } from 'solid-js'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import AppShell from './AppShell'
|
||||
|
||||
vi.mock('@solidjs/router', () => ({
|
||||
A: (props: { children: unknown; href?: string; 'aria-label'?: string }): JSX.Element => (
|
||||
<a href={props.href} aria-label={props['aria-label']}>
|
||||
{props.children}
|
||||
</a>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('AppShell', () => {
|
||||
it('renders title and primary navigation actions', () => {
|
||||
render(() => <AppShell>{<div>content</div>}</AppShell>)
|
||||
|
||||
expect(screen.getByText('Know Foolery')).toBeTruthy()
|
||||
expect(screen.getByLabelText('Home')).toBeTruthy()
|
||||
expect(screen.getByLabelText('Game')).toBeTruthy()
|
||||
expect(screen.getByLabelText('Leaderboard')).toBeTruthy()
|
||||
expect(screen.getByLabelText('Profile')).toBeTruthy()
|
||||
expect(screen.getByLabelText('Admin Questions')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,27 @@
|
||||
import { createRoot } from 'solid-js'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { useAuth } from './useAuth'
|
||||
|
||||
describe('useAuth', () => {
|
||||
afterEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('starts unauthenticated and toggles auth state on sign in/out', () => {
|
||||
createRoot((dispose) => {
|
||||
const auth = useAuth()
|
||||
expect(auth.isAuthenticated()).toBe(false)
|
||||
|
||||
auth.signInDemo()
|
||||
expect(auth.isAuthenticated()).toBe(true)
|
||||
expect(localStorage.getItem('kf.auth.token')).toBe('demo-token')
|
||||
|
||||
auth.signOut()
|
||||
expect(auth.isAuthenticated()).toBe(false)
|
||||
expect(localStorage.getItem('kf.auth.token')).toBeNull()
|
||||
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,50 @@
|
||||
import { createRoot } from 'solid-js'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useTimer } from './useTimer'
|
||||
|
||||
describe('useTimer', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.setSystemTime(new Date('2026-02-13T10:00:00Z'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('counts down and expires after duration', () => {
|
||||
createRoot((dispose) => {
|
||||
const timer = useTimer(1_000)
|
||||
timer.start()
|
||||
|
||||
expect(timer.isExpired()).toBe(false)
|
||||
expect(timer.remainingMs()).toBe(1_000)
|
||||
|
||||
vi.advanceTimersByTime(1_250)
|
||||
|
||||
expect(timer.isExpired()).toBe(true)
|
||||
expect(timer.remainingMs()).toBe(0)
|
||||
|
||||
timer.stop()
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
|
||||
it('does not restart interval if start is called repeatedly', () => {
|
||||
createRoot((dispose) => {
|
||||
const timer = useTimer(5_000)
|
||||
timer.start()
|
||||
const firstRemaining = timer.remainingMs()
|
||||
|
||||
vi.advanceTimersByTime(500)
|
||||
timer.start()
|
||||
vi.advanceTimersByTime(500)
|
||||
|
||||
expect(timer.remainingMs()).toBeLessThan(firstRemaining)
|
||||
|
||||
timer.stop()
|
||||
dispose()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,18 @@
|
||||
import { fireEvent, render, screen } from '@solidjs/testing-library'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import AdminQuestionsRoute from './AdminQuestions'
|
||||
|
||||
describe('AdminQuestionsRoute', () => {
|
||||
afterEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('lists seeded questions and supports deletion', async () => {
|
||||
render(() => <AdminQuestionsRoute />)
|
||||
|
||||
expect(screen.getByText('Quel est le plus petit nombre premier ?')).toBeTruthy()
|
||||
await fireEvent.click(screen.getByRole('button', { name: 'Supprimer' }))
|
||||
expect(screen.queryByText('Quel est le plus petit nombre premier ?')).toBeNull()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,153 @@
|
||||
import { For, createSignal, type JSX } from 'solid-js'
|
||||
|
||||
import Box from '@suid/material/Box'
|
||||
import Button from '@suid/material/Button'
|
||||
import Card from '@suid/material/Card'
|
||||
import CardContent from '@suid/material/CardContent'
|
||||
import MenuItem from '@suid/material/MenuItem'
|
||||
import Stack from '@suid/material/Stack'
|
||||
import TextField from '@suid/material/TextField'
|
||||
import Typography from '@suid/material/Typography'
|
||||
|
||||
import {
|
||||
createAdminQuestion,
|
||||
deleteAdminQuestion,
|
||||
listAdminQuestions,
|
||||
type AdminQuestion,
|
||||
type CreateQuestionInput,
|
||||
} from '../services/adminQuestions'
|
||||
|
||||
type FormState = CreateQuestionInput
|
||||
|
||||
const defaultState: FormState = {
|
||||
theme: 'Général',
|
||||
text: '',
|
||||
answer: '',
|
||||
hint: '',
|
||||
difficulty: 'medium',
|
||||
}
|
||||
|
||||
function readInputValue(event: Event): string {
|
||||
return (event.target as HTMLInputElement).value
|
||||
}
|
||||
|
||||
export default function AdminQuestionsRoute(): JSX.Element {
|
||||
const [items, setItems] = createSignal<AdminQuestion[]>(listAdminQuestions())
|
||||
const [form, setForm] = createSignal<FormState>(defaultState)
|
||||
const [error, setError] = createSignal<string | null>(null)
|
||||
|
||||
const updateField = (key: keyof FormState, value: string): void => {
|
||||
setForm((prev) => ({ ...prev, [key]: value }) as FormState)
|
||||
}
|
||||
|
||||
const submit = (): void => {
|
||||
const current = form()
|
||||
if (!current.theme.trim() || !current.text.trim() || !current.answer.trim()) {
|
||||
setError('Theme, question et réponse sont requis.')
|
||||
return
|
||||
}
|
||||
|
||||
createAdminQuestion(current)
|
||||
setItems(listAdminQuestions())
|
||||
setForm(defaultState)
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const remove = (id: string): void => {
|
||||
deleteAdminQuestion(id)
|
||||
setItems(listAdminQuestions())
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Stack spacing={2}>
|
||||
<Typography variant="h4" sx={{ fontWeight: 800 }}>
|
||||
Admin - Questions
|
||||
</Typography>
|
||||
|
||||
<Card variant="outlined" sx={{ bgcolor: '#111827', borderColor: '#1f2937' }}>
|
||||
<CardContent>
|
||||
<Stack spacing={2}>
|
||||
<Typography variant="h6">Créer une question</Typography>
|
||||
{error() && <Typography color="error">{error()}</Typography>}
|
||||
|
||||
<TextField
|
||||
label="Theme"
|
||||
value={form().theme}
|
||||
onInput={(event) => updateField('theme', readInputValue(event))}
|
||||
inputProps={{ 'data-testid': 'admin-theme-input' }}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Question"
|
||||
value={form().text}
|
||||
onInput={(event) => updateField('text', readInputValue(event))}
|
||||
inputProps={{ 'data-testid': 'admin-question-input' }}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Réponse"
|
||||
value={form().answer}
|
||||
onInput={(event) => updateField('answer', readInputValue(event))}
|
||||
inputProps={{ 'data-testid': 'admin-answer-input' }}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Indice"
|
||||
value={form().hint}
|
||||
onInput={(event) => updateField('hint', readInputValue(event))}
|
||||
inputProps={{ 'data-testid': 'admin-hint-input' }}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<TextField
|
||||
select
|
||||
label="Difficulté"
|
||||
value={form().difficulty}
|
||||
onChange={(event) => updateField('difficulty', readInputValue(event))}
|
||||
inputProps={{ 'data-testid': 'admin-difficulty-select' }}
|
||||
fullWidth
|
||||
>
|
||||
<MenuItem value="easy">easy</MenuItem>
|
||||
<MenuItem value="medium">medium</MenuItem>
|
||||
<MenuItem value="hard">hard</MenuItem>
|
||||
</TextField>
|
||||
|
||||
<Button variant="contained" onClick={submit} data-testid="admin-create-question">
|
||||
Ajouter la question
|
||||
</Button>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card variant="outlined" sx={{ bgcolor: '#111827', borderColor: '#1f2937' }}>
|
||||
<CardContent>
|
||||
<Stack spacing={1}>
|
||||
<Typography variant="h6">Questions existantes</Typography>
|
||||
<For each={items()}>
|
||||
{(item) => (
|
||||
<Stack spacing={1} sx={{ border: '1px solid #1f2937', p: 1.5, borderRadius: 1 }}>
|
||||
<Typography sx={{ fontWeight: 700 }}>{item.text}</Typography>
|
||||
<Typography sx={{ opacity: 0.8 }}>Theme: {item.theme}</Typography>
|
||||
<Typography sx={{ opacity: 0.8 }}>Réponse: {item.answer}</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="error"
|
||||
onClick={() => remove(item.id)}
|
||||
data-testid={`admin-delete-${item.id}`}
|
||||
>
|
||||
Supprimer
|
||||
</Button>
|
||||
</Stack>
|
||||
)}
|
||||
</For>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import { fireEvent, render, screen } from '@solidjs/testing-library'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import HomeRoute from './Home'
|
||||
|
||||
const navigate = vi.fn()
|
||||
|
||||
vi.mock('@solidjs/router', () => ({
|
||||
useNavigate: (): ((path: string) => void) => navigate,
|
||||
}))
|
||||
|
||||
describe('HomeRoute', () => {
|
||||
beforeEach(() => {
|
||||
navigate.mockReset()
|
||||
})
|
||||
|
||||
it('navigates to leaderboard from secondary action', async () => {
|
||||
render(() => <HomeRoute />)
|
||||
await fireEvent.click(screen.getByRole('button', { name: 'Voir le leaderboard' }))
|
||||
expect(navigate).toHaveBeenCalledWith('/leaderboard')
|
||||
})
|
||||
|
||||
it('blocks game start and shows validation error when name is invalid', async () => {
|
||||
render(() => <HomeRoute />)
|
||||
|
||||
await fireEvent.click(screen.getByRole('button', { name: 'Démarrer la partie' }))
|
||||
|
||||
expect(navigate).not.toHaveBeenCalledWith('/game')
|
||||
expect(screen.getByText(/au moins 2 caractères|seulement lettres/)).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,22 @@
|
||||
import { fireEvent, render, screen } from '@solidjs/testing-library'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import ProfileRoute from './Profile'
|
||||
|
||||
describe('ProfileRoute', () => {
|
||||
afterEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('requires auth then allows demo login and profile actions', async () => {
|
||||
render(() => <ProfileRoute />)
|
||||
|
||||
expect(screen.getByText('Connexion requise pour accéder au profil joueur.')).toBeTruthy()
|
||||
|
||||
await fireEvent.click(screen.getByRole('button', { name: 'Se connecter (mode démo)' }))
|
||||
expect(screen.getByText('Statistiques joueur')).toBeTruthy()
|
||||
|
||||
await fireEvent.click(screen.getByRole('button', { name: 'Se déconnecter' }))
|
||||
expect(localStorage.getItem('kf.auth.token')).toBeNull()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,26 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { createAdminQuestion, deleteAdminQuestion, listAdminQuestions } from './adminQuestions'
|
||||
|
||||
describe('adminQuestions service', () => {
|
||||
afterEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('creates and deletes persisted questions', () => {
|
||||
const created = createAdminQuestion({
|
||||
theme: 'Science',
|
||||
text: 'Question test',
|
||||
answer: 'Réponse test',
|
||||
hint: 'Indice test',
|
||||
difficulty: 'easy',
|
||||
})
|
||||
|
||||
const withCreated = listAdminQuestions()
|
||||
expect(withCreated.some((item) => item.id === created.id)).toBe(true)
|
||||
|
||||
deleteAdminQuestion(created.id)
|
||||
const afterDelete = listAdminQuestions()
|
||||
expect(afterDelete.some((item) => item.id === created.id)).toBe(false)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { leaderboardClient } from './api'
|
||||
|
||||
describe('leaderboardClient', () => {
|
||||
it('returns deterministic mock top10 when API base URL is not configured', async () => {
|
||||
const rows = await leaderboardClient.top10()
|
||||
|
||||
expect(rows).toHaveLength(10)
|
||||
expect(rows[0]).toMatchObject({ player: 'Alice', score: 24 })
|
||||
expect(rows[9]).toMatchObject({ player: 'Jules' })
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,46 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
appendGameHistory,
|
||||
loadGameHistory,
|
||||
loadLastResult,
|
||||
saveLastResult,
|
||||
type GameResult,
|
||||
} from './session'
|
||||
|
||||
function fixture(score: number): GameResult {
|
||||
return {
|
||||
playerName: 'Alice',
|
||||
finalScore: score,
|
||||
answered: 5,
|
||||
correct: 4,
|
||||
successRate: 80,
|
||||
durationSec: 120,
|
||||
leaderboardPosition: 2,
|
||||
finishedAt: new Date('2026-02-13T10:00:00Z').toISOString(),
|
||||
}
|
||||
}
|
||||
|
||||
describe('session storage service', () => {
|
||||
afterEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('saves and loads last result', () => {
|
||||
const result = fixture(10)
|
||||
saveLastResult(result)
|
||||
|
||||
expect(loadLastResult()).toEqual(result)
|
||||
})
|
||||
|
||||
it('appends game history newest-first with max 20 entries', () => {
|
||||
for (let i = 0; i < 25; i += 1) {
|
||||
appendGameHistory(fixture(i))
|
||||
}
|
||||
|
||||
const history = loadGameHistory()
|
||||
expect(history).toHaveLength(20)
|
||||
expect(history[0]?.finalScore).toBe(24)
|
||||
expect(history[19]?.finalScore).toBe(5)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"failedTests": []
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
import { cleanup } from '@solidjs/testing-library'
|
||||
import { afterEach } from 'vitest'
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
localStorage.clear()
|
||||
})
|
||||
@ -1 +1 @@
|
||||
{"version":"1.6.1","results":[[":src/utils/timer.test.ts",{"duration":1,"failed":false}],[":src/components/AttemptIndicator.test.tsx",{"duration":18,"failed":false}],[":src/components/ResultsCard.test.tsx",{"duration":63,"failed":false}],[":src/components/LeaderboardTable.test.tsx",{"duration":27,"failed":false}],[":src/components/AnswerInput.test.tsx",{"duration":36,"failed":false}],[":src/components/HintButton.test.tsx",{"duration":64,"failed":false}]]}
|
||||
{"version":"1.6.1","results":[[":src/components/ResultsCard.test.tsx",{"duration":63,"failed":false}],[":src/components/LeaderboardTable.test.tsx",{"duration":28,"failed":false}],[":src/components/Timer.test.tsx",{"duration":25,"failed":false}],[":src/components/GameCard.test.tsx",{"duration":33,"failed":false}],[":src/components/HintButton.test.tsx",{"duration":83,"failed":false}],[":src/utils/timer.test.ts",{"duration":2,"failed":false}],[":src/components/AttemptIndicator.test.tsx",{"duration":21,"failed":false}],[":src/components/ThemeBadge.test.tsx",{"duration":18,"failed":false}],[":src/components/ScoreDisplay.test.tsx",{"duration":17,"failed":false}],[":src/components/AnswerInput.test.tsx",{"duration":34,"failed":false}]]}
|
||||
@ -0,0 +1,31 @@
|
||||
import { render, screen } from '@solidjs/testing-library'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import GameCard from './GameCard'
|
||||
|
||||
describe('GameCard', () => {
|
||||
it('renders question shell and injected slots', () => {
|
||||
render(() => (
|
||||
<GameCard
|
||||
title="Partie"
|
||||
theme="Science"
|
||||
questionText="Quelle est la formule de l'eau ?"
|
||||
timerSlot={<span>timer-slot</span>}
|
||||
scoreSlot={<span>score-slot</span>}
|
||||
answerSlot={<span>answer-slot</span>}
|
||||
attemptSlot={<span>attempt-slot</span>}
|
||||
actionsSlot={<span>actions-slot</span>}
|
||||
feedbackSlot={<span>feedback-slot</span>}
|
||||
/>
|
||||
))
|
||||
|
||||
expect(screen.getByText('Partie')).toBeTruthy()
|
||||
expect(screen.getByText("Quelle est la formule de l'eau ?")).toBeTruthy()
|
||||
expect(screen.getByText('timer-slot')).toBeTruthy()
|
||||
expect(screen.getByText('score-slot')).toBeTruthy()
|
||||
expect(screen.getByText('answer-slot')).toBeTruthy()
|
||||
expect(screen.getByText('attempt-slot')).toBeTruthy()
|
||||
expect(screen.getByText('actions-slot')).toBeTruthy()
|
||||
expect(screen.getByText('feedback-slot')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,11 @@
|
||||
import { render, screen } from '@solidjs/testing-library'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import ScoreDisplay from './ScoreDisplay'
|
||||
|
||||
describe('ScoreDisplay', () => {
|
||||
it('renders score with custom label prefix', () => {
|
||||
render(() => <ScoreDisplay score={9} labelPrefix="Points:" />)
|
||||
expect(screen.getByText('Points: 9')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,11 @@
|
||||
import { render, screen } from '@solidjs/testing-library'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import ThemeBadge from './ThemeBadge'
|
||||
|
||||
describe('ThemeBadge', () => {
|
||||
it('renders themed label with prefix', () => {
|
||||
render(() => <ThemeBadge theme="Histoire" labelPrefix="Thème : " />)
|
||||
expect(screen.getByText('Thème : Histoire')).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,20 @@
|
||||
import { render, screen } from '@solidjs/testing-library'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import Timer from './Timer'
|
||||
|
||||
describe('Timer', () => {
|
||||
it('renders formatted time and warning text when threshold is reached', () => {
|
||||
render(() => <Timer remainingMs={9_000} />)
|
||||
|
||||
expect(screen.getByText('0:09')).toBeTruthy()
|
||||
expect(screen.getByText('10 secondes restantes')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('suppresses warning text when showWarning is false', () => {
|
||||
render(() => <Timer remainingMs={9_000} showWarning={false} />)
|
||||
|
||||
expect(screen.getByText('0:09')).toBeTruthy()
|
||||
expect(screen.queryByText('10 secondes restantes')).toBeNull()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,6 @@
|
||||
import { cleanup } from '@solidjs/testing-library'
|
||||
import { afterEach } from 'vitest'
|
||||
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
})
|
||||
Loading…
Reference in New Issue