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('#]*\bsrc=)[^>]*>(.*?)#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([]); });