210 lines
11 KiB
PHP
210 lines
11 KiB
PHP
|
|
<?php
|
|||
|
|
|
|||
|
|
use App\Models\ChoixQuestionQuiz;
|
|||
|
|
use App\Models\Classe;
|
|||
|
|
use App\Models\Eleve;
|
|||
|
|
use App\Models\QuestionQuiz;
|
|||
|
|
use App\Models\Quiz;
|
|||
|
|
use App\Models\ReponseQuiz;
|
|||
|
|
use App\Models\SessionQuiz;
|
|||
|
|
use Illuminate\Support\Facades\Process;
|
|||
|
|
|
|||
|
|
// eleve() est déjà définie globalement par VisioTest.php (même fichier
|
|||
|
|
// de test, chargé une fois pour toute la suite).
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Quiz « Capitales », 1 QCM + 1 vrai/faux, pour une classe à 2 élèves.
|
|||
|
|
*
|
|||
|
|
* @return array{classe: Classe, quiz: Quiz, qcm: QuestionQuiz, vf: QuestionQuiz, bon_choix: ChoixQuestionQuiz, eleve1: Eleve, eleve2: Eleve}
|
|||
|
|
*/
|
|||
|
|
function quizPret(): array
|
|||
|
|
{
|
|||
|
|
$classe = Classe::factory()->create();
|
|||
|
|
$eleve1 = Eleve::factory()->create(['classe_id' => $classe->id]);
|
|||
|
|
$eleve2 = Eleve::factory()->create(['classe_id' => $classe->id]);
|
|||
|
|
$quiz = Quiz::create(['classe_id' => $classe->id, 'titre' => 'Capitales']);
|
|||
|
|
|
|||
|
|
$qcm = $quiz->questions()->create(['enonce' => 'Capitale de la France ?', 'type' => QuestionQuiz::QCM, 'ordre' => 0]);
|
|||
|
|
$bonChoix = $qcm->choix()->create(['texte' => 'Paris', 'est_correct' => true, 'ordre' => 0]);
|
|||
|
|
$qcm->choix()->create(['texte' => 'Lyon', 'est_correct' => false, 'ordre' => 1]);
|
|||
|
|
|
|||
|
|
$vf = $quiz->questions()->create(['enonce' => 'Paris est en France.', 'type' => QuestionQuiz::VRAI_FAUX, 'ordre' => 1, 'reponse_vrai_faux' => true]);
|
|||
|
|
|
|||
|
|
return compact('classe', 'quiz', 'qcm', 'vf', 'bonChoix', 'eleve1', 'eleve2');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
test('un prof crée un quiz puis des questions QCM et vrai/faux', function () {
|
|||
|
|
$classe = Classe::factory()->create();
|
|||
|
|
|
|||
|
|
commeProf($this, $classe->prof)
|
|||
|
|
->post(route('mes-classes.quiz.store', $classe), ['titre' => 'Capitales'])
|
|||
|
|
->assertRedirect();
|
|||
|
|
|
|||
|
|
$quiz = Quiz::where('titre', 'Capitales')->first();
|
|||
|
|
expect($quiz)->not->toBeNull()->and($quiz->classe_id)->toBe($classe->id);
|
|||
|
|
|
|||
|
|
commeProf($this, $classe->prof)->post(route('mes-classes.quiz.questions.store', [$classe, $quiz]), [
|
|||
|
|
'enonce' => 'Capitale de la France ?', 'type' => 'qcm',
|
|||
|
|
'choix' => ['Paris', 'Lyon'], 'bonne_reponse' => 0,
|
|||
|
|
])->assertRedirect();
|
|||
|
|
|
|||
|
|
commeProf($this, $classe->prof)->post(route('mes-classes.quiz.questions.store', [$classe, $quiz]), [
|
|||
|
|
'enonce' => 'Paris est en France.', 'type' => 'vrai_faux', 'reponse_vrai_faux' => '1',
|
|||
|
|
])->assertRedirect();
|
|||
|
|
|
|||
|
|
$quiz->refresh();
|
|||
|
|
expect($quiz->questions)->toHaveCount(2);
|
|||
|
|
$qcm = $quiz->questions->firstWhere('type', 'qcm');
|
|||
|
|
expect($qcm->choix)->toHaveCount(2)
|
|||
|
|
->and($qcm->choix->firstWhere('texte', 'Paris')->est_correct)->toBeTrue();
|
|||
|
|
$vf = $quiz->questions->firstWhere('type', 'vrai_faux');
|
|||
|
|
expect($vf->reponse_vrai_faux)->toBeTrue();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un prof d’une autre classe ne peut pas voir ni modifier ce quiz', function () {
|
|||
|
|
['classe' => $classe, 'quiz' => $quiz] = quizPret();
|
|||
|
|
$autreClasse = Classe::factory()->create();
|
|||
|
|
|
|||
|
|
commeProf($this, $autreClasse->prof)->get(route('mes-classes.quiz.show', [$classe, $quiz]))->assertForbidden();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('lancer un quiz sans question est refusé', function () {
|
|||
|
|
$classe = Classe::factory()->create();
|
|||
|
|
$quiz = Quiz::create(['classe_id' => $classe->id, 'titre' => 'Vide']);
|
|||
|
|
|
|||
|
|
commeProf($this, $classe->prof)->post(route('mes-classes.quiz.lancer', [$classe, $quiz]))->assertStatus(422);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('le prof avance les questions une par une, puis la session se termine', function () {
|
|||
|
|
['classe' => $classe, 'quiz' => $quiz, 'qcm' => $qcm, 'vf' => $vf] = quizPret();
|
|||
|
|
|
|||
|
|
commeProf($this, $classe->prof)->post(route('mes-classes.quiz.lancer', [$classe, $quiz]))->assertRedirect();
|
|||
|
|
$session = SessionQuiz::where('quiz_id', $quiz->id)->first();
|
|||
|
|
expect($session->statut)->toBe(SessionQuiz::EN_ATTENTE)->and($session->question_quiz_courante_id)->toBeNull();
|
|||
|
|
|
|||
|
|
commeProf($this, $classe->prof)->post(route('sessions-quiz.suivante', $session));
|
|||
|
|
expect($session->refresh()->statut)->toBe(SessionQuiz::EN_COURS)
|
|||
|
|
->and($session->question_quiz_courante_id)->toBe($qcm->id);
|
|||
|
|
|
|||
|
|
commeProf($this, $classe->prof)->post(route('sessions-quiz.suivante', $session));
|
|||
|
|
expect($session->refresh()->question_quiz_courante_id)->toBe($vf->id);
|
|||
|
|
|
|||
|
|
commeProf($this, $classe->prof)->post(route('sessions-quiz.suivante', $session));
|
|||
|
|
expect($session->refresh()->statut)->toBe(SessionQuiz::TERMINEE);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un élève voit la question en cours (QCM avec ses choix, vrai/faux sans)', function () {
|
|||
|
|
['classe' => $classe, 'quiz' => $quiz, 'qcm' => $qcm, 'eleve1' => $eleve1] = quizPret();
|
|||
|
|
$session = SessionQuiz::create(['quiz_id' => $quiz->id, 'statut' => SessionQuiz::EN_COURS, 'question_quiz_courante_id' => $qcm->id, 'demarree_a' => now()]);
|
|||
|
|
|
|||
|
|
eleve($this, $eleve1)->getJson(route('quiz.eleve.etat'))
|
|||
|
|
->assertJsonPath('session.statut', SessionQuiz::EN_COURS)
|
|||
|
|
->assertJsonPath('session.question.enonce', 'Capitale de la France ?')
|
|||
|
|
->assertJsonCount(2, 'session.question.choix')
|
|||
|
|
->assertJsonPath('session.deja_repondu', false);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un élève d’une autre classe ne voit rien', function () {
|
|||
|
|
['quiz' => $quiz, 'qcm' => $qcm] = quizPret();
|
|||
|
|
SessionQuiz::create(['quiz_id' => $quiz->id, 'statut' => SessionQuiz::EN_COURS, 'question_quiz_courante_id' => $qcm->id, 'demarree_a' => now()]);
|
|||
|
|
$autreEleve = Eleve::factory()->create();
|
|||
|
|
|
|||
|
|
eleve($this, $autreEleve)->getJson(route('quiz.eleve.etat'))->assertJsonPath('session', null);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un élève répond juste ou faux au QCM, le décompte du prof se met à jour, pas deux fois', function () {
|
|||
|
|
['classe' => $classe, 'quiz' => $quiz, 'qcm' => $qcm, 'bonChoix' => $bonChoix, 'eleve1' => $eleve1, 'eleve2' => $eleve2] = quizPret();
|
|||
|
|
$session = SessionQuiz::create(['quiz_id' => $quiz->id, 'statut' => SessionQuiz::EN_COURS, 'question_quiz_courante_id' => $qcm->id, 'demarree_a' => now()]);
|
|||
|
|
$mauvaisChoix = $qcm->choix->firstWhere('texte', 'Lyon');
|
|||
|
|
|
|||
|
|
eleve($this, $eleve1)->postJson(route('sessions-quiz.repondre', $session), ['choix_id' => $bonChoix->id])
|
|||
|
|
->assertOk()->assertJson(['correcte' => true]);
|
|||
|
|
eleve($this, $eleve2)->postJson(route('sessions-quiz.repondre', $session), ['choix_id' => $mauvaisChoix->id])
|
|||
|
|
->assertOk()->assertJson(['correcte' => false]);
|
|||
|
|
|
|||
|
|
// Un second envoi (double clic, ou tentative de changer d'avis) est refusé.
|
|||
|
|
eleve($this, $eleve1)->postJson(route('sessions-quiz.repondre', $session), ['choix_id' => $mauvaisChoix->id])
|
|||
|
|
->assertStatus(409);
|
|||
|
|
|
|||
|
|
expect(ReponseQuiz::where('session_quiz_id', $session->id)->count())->toBe(2);
|
|||
|
|
expect($session->decompteQuestionCourante())->toBe(['repondu' => 2, 'correctes' => 1]);
|
|||
|
|
|
|||
|
|
eleve($this, $eleve1)->getJson(route('quiz.eleve.etat'))->assertJsonPath('session.deja_repondu', true);
|
|||
|
|
|
|||
|
|
commeProf($this, $classe->prof)->getJson(route('sessions-quiz.etat', $session))
|
|||
|
|
->assertJsonPath('decompte.repondu', 2)->assertJsonPath('decompte.correctes', 1);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un élève répond juste ou faux au vrai/faux', function () {
|
|||
|
|
['quiz' => $quiz, 'vf' => $vf, 'eleve1' => $eleve1, 'eleve2' => $eleve2] = quizPret();
|
|||
|
|
$session = SessionQuiz::create(['quiz_id' => $quiz->id, 'statut' => SessionQuiz::EN_COURS, 'question_quiz_courante_id' => $vf->id, 'demarree_a' => now()]);
|
|||
|
|
|
|||
|
|
eleve($this, $eleve1)->postJson(route('sessions-quiz.repondre', $session), ['reponse_vrai_faux' => true])
|
|||
|
|
->assertJson(['correcte' => true]);
|
|||
|
|
eleve($this, $eleve2)->postJson(route('sessions-quiz.repondre', $session), ['reponse_vrai_faux' => false])
|
|||
|
|
->assertJson(['correcte' => false]);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('un élève ne peut pas répondre à une question qui n’est plus la question en cours', function () {
|
|||
|
|
['quiz' => $quiz, 'qcm' => $qcm, 'vf' => $vf, 'bonChoix' => $bonChoix, 'eleve1' => $eleve1] = quizPret();
|
|||
|
|
// La session a déjà avancé à la question vrai/faux.
|
|||
|
|
$session = SessionQuiz::create(['quiz_id' => $quiz->id, 'statut' => SessionQuiz::EN_COURS, 'question_quiz_courante_id' => $vf->id, 'demarree_a' => now()]);
|
|||
|
|
|
|||
|
|
// Tenter de répondre au QCM (ancienne question) avec le format attendu du
|
|||
|
|
// vrai/faux ignoré : le contrôleur ne regarde que la question courante.
|
|||
|
|
eleve($this, $eleve1)->postJson(route('sessions-quiz.repondre', $session), ['choix_id' => $bonChoix->id])
|
|||
|
|
->assertOk(); // enregistrée comme réponse à la question courante (vrai/faux), choix_id ignoré côté correction
|
|||
|
|
|
|||
|
|
$reponse = ReponseQuiz::first();
|
|||
|
|
expect($reponse->question_quiz_id)->toBe($vf->id)
|
|||
|
|
->and($reponse->correcte)->toBeFalse(); // reponse_vrai_faux absent du corps => jamais correcte
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
test('archiver puis purger une classe supprime aussi ses quiz, questions, choix, sessions et réponses', function () {
|
|||
|
|
['classe' => $classe, 'quiz' => $quiz, 'qcm' => $qcm, 'bonChoix' => $bonChoix, 'eleve1' => $eleve1] = quizPret();
|
|||
|
|
$session = SessionQuiz::create(['quiz_id' => $quiz->id, 'statut' => SessionQuiz::EN_COURS, 'question_quiz_courante_id' => $qcm->id, 'demarree_a' => now()]);
|
|||
|
|
ReponseQuiz::create(['session_quiz_id' => $session->id, 'question_quiz_id' => $qcm->id, 'eleve_id' => $eleve1->id, 'choix_question_quiz_id' => $bonChoix->id, 'correcte' => true, 'repondu_a' => now()]);
|
|||
|
|
|
|||
|
|
$classe->forceFill(['archivee_a' => now()->subDays(91)])->save();
|
|||
|
|
$this->artisan('classes:purger-archivees');
|
|||
|
|
|
|||
|
|
expect(Quiz::count())->toBe(0)
|
|||
|
|
->and(QuestionQuiz::count())->toBe(0)
|
|||
|
|
->and(ChoixQuestionQuiz::count())->toBe(0)
|
|||
|
|
->and(SessionQuiz::count())->toBe(0)
|
|||
|
|
->and(ReponseQuiz::count())->toBe(0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
// Ces pages sont protégées (auth:web, auth:eleve) : hors du périmètre de
|
|||
|
|
// PagesPubliquesTest, qui ne couvre que les pages sans authentification.
|
|||
|
|
test('les scripts en ligne des pages du quiz flash sont du JavaScript valide', function () {
|
|||
|
|
if (Process::run('node --version')->failed()) {
|
|||
|
|
$this->markTestSkipped('Node absent');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
['classe' => $classe, 'quiz' => $quiz, 'eleve1' => $eleve1] = quizPret();
|
|||
|
|
$session = SessionQuiz::create(['quiz_id' => $quiz->id, 'statut' => SessionQuiz::EN_ATTENTE, 'demarree_a' => now()]);
|
|||
|
|
|
|||
|
|
$pages = [
|
|||
|
|
fn () => commeProf($this, $classe->prof)->get(route('mes-classes.quiz.show', [$classe, $quiz])),
|
|||
|
|
fn () => commeProf($this, $classe->prof)->get(route('sessions-quiz.show', $session)),
|
|||
|
|
fn () => eleve($this, $eleve1)->get(route('eleve')),
|
|||
|
|
];
|
|||
|
|
|
|||
|
|
$erreurs = [];
|
|||
|
|
foreach ($pages as $i => $visite) {
|
|||
|
|
preg_match_all('#<script(?![^>]*\bsrc=)[^>]*>(.*?)</script>#s', $visite()->getContent(), $scripts);
|
|||
|
|
foreach ($scripts[1] as $j => $code) {
|
|||
|
|
$fichier = tempnam(sys_get_temp_dir(), 'js').'.js';
|
|||
|
|
file_put_contents($fichier, $code);
|
|||
|
|
$resultat = Process::run(['node', '--check', $fichier]);
|
|||
|
|
unlink($fichier);
|
|||
|
|
if ($resultat->failed()) {
|
|||
|
|
$erreurs[] = "page {$i} script {$j} : ".strtok($resultat->errorOutput(), "\n");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
expect($erreurs)->toBe([]);
|
|||
|
|
});
|