diff --git a/docs/journal.md b/docs/journal.md index a51a497..56f7bcc 100644 --- a/docs/journal.md +++ b/docs/journal.md @@ -935,3 +935,21 @@ La bonne nouvelle de cette fonction, c'est qu'elle n'a presque rien coûté en c **Suite** Chantier « vrai cockpit », étape 3 : Devoirs maison. + +## 2026-10-13 — Chantier « vrai cockpit », étape 3 : Devoirs maison + +Deux décisions d'Adam avant de coder : rendu en texte libre (pas de dépôt de fichier, qui demanderait le stockage S3 prévu au bloc B, pas commencé) ; notation sur 20 **et** suivi « rendu / pas rendu » ensemble. + +**Fait** + +- `Devoir` (titre, consigne, échéance) par classe ; `RenduDevoir` (texte, note sur 20 facultative, horodatage), un seul par élève et par devoir — un nouvel envoi remplace le précédent, pas d'historique. Rendre n'est plus possible après l'échéance. +- Prof : `/mes-classes/{classe}/devoirs` (créer, voir qui a rendu et qui non, lire chaque réponse, noter). +- Élève : une vraie section sur `/eleve` (devoirs des 7 derniers jours, à rendre ou déjà rendus), et une page par devoir pour lire la consigne, rendre ou modifier son rendu, voir sa note une fois donnée. +- Menu : le lien démo « Devoirs maison » du mégamenu pointe vers `/mes-classes` pour un prof connecté — même correctif que pour le tableau de bord, le quiz et la banque. +- 7 nouveaux tests Pest (82 au total) : parcours complet, modification du rendu avant l'échéance (un seul en base), refus après l'échéance, étanchéité entre classes (élève et prof), purge RGPD qui emporte devoirs et rendus. + +**Vérifié sur le serveur réel** (scénario jetable, supprimé ensuite) : un prof a créé un devoir avec une échéance à 48 h ; l'élève l'a vu apparaître sur son tableau de bord, a rendu une réponse ; le prof a vu le rendu immédiatement, l'a noté 16/20 ; l'élève a vu sa note. Aucune unité en échec après coup. + +**Suite** + +Avec Quiz flash, la banque de quiz et les devoirs maison, trois des quatre fonctions fictives du tableau de bord démo sont maintenant réelles. Reste : Observations (puis Surveillance d'examen, la plus sensible — suivi de mineurs à cadrer avec Adam avant de coder). diff --git a/portail/app/Console/Commands/PurgerClassesArchivees.php b/portail/app/Console/Commands/PurgerClassesArchivees.php index b003478..780c6f5 100644 --- a/portail/app/Console/Commands/PurgerClassesArchivees.php +++ b/portail/app/Console/Commands/PurgerClassesArchivees.php @@ -10,7 +10,7 @@ use Illuminate\Support\Facades\Log; /** * RGPD (CLAUDE.md, étape 14) : supprime les classes archivées depuis * 90 jours, avec en cascade leurs élèves, séances, sessions de lab, machines, - * corrections et quiz (chantier « vrai cockpit », étape 2). Le compte du + * corrections, quiz et devoirs maison (chantier « vrai cockpit »). Le compte du * prof est conservé. Le journal ne garde que des identifiants et des * nombres, jamais de nom d'élève. * diff --git a/portail/app/Http/Controllers/DevoirController.php b/portail/app/Http/Controllers/DevoirController.php new file mode 100644 index 0000000..9c99125 --- /dev/null +++ b/portail/app/Http/Controllers/DevoirController.php @@ -0,0 +1,85 @@ +verifierProprietaire($classe); + + return view('devoirs.index', ['classe' => $classe, 'devoirs' => $classe->devoirs()->withCount('rendus')->get()]); + } + + public function store(Request $request, Classe $classe): RedirectResponse + { + $this->verifierProprietaire($classe); + $donnees = $request->validate([ + 'titre' => ['required', 'string', 'max:150'], + 'consigne' => ['required', 'string', 'max:5000'], + 'echeance' => ['required', 'date', 'after:now'], + ]); + + $devoir = $classe->devoirs()->create($donnees); + + return redirect()->route('mes-classes.devoirs.show', [$classe, $devoir]); + } + + public function show(Classe $classe, Devoir $devoir): View + { + $this->verifierProprietaire($classe); + $this->verifierAppartenance($classe, $devoir); + + $rendus = $devoir->rendus()->with('eleve')->get()->keyBy('eleve_id'); + + return view('devoirs.show', [ + 'classe' => $classe, + 'devoir' => $devoir, + 'eleves' => $classe->eleves, + 'rendus' => $rendus, + ]); + } + + public function noter(Request $request, Classe $classe, Devoir $devoir, RenduDevoir $rendu): RedirectResponse + { + $this->verifierProprietaire($classe); + $this->verifierAppartenance($classe, $devoir); + abort_if($rendu->devoir_id !== $devoir->id, 404); + + $donnees = $request->validate(['note' => ['required', 'integer', 'min:0', 'max:20']]); + $rendu->update($donnees); + + return redirect()->route('mes-classes.devoirs.show', [$classe, $devoir]); + } + + public function destroy(Classe $classe, Devoir $devoir): RedirectResponse + { + $this->verifierProprietaire($classe); + $this->verifierAppartenance($classe, $devoir); + $devoir->delete(); + + return redirect()->route('mes-classes.devoirs.index', $classe); + } + + private function verifierProprietaire(Classe $classe): void + { + abort_if($classe->user_id !== Auth::guard('web')->id(), 403); + } + + private function verifierAppartenance(Classe $classe, Devoir $devoir): void + { + abort_if($devoir->classe_id !== $classe->id, 404); + } +} diff --git a/portail/app/Http/Controllers/DevoirEleveController.php b/portail/app/Http/Controllers/DevoirEleveController.php new file mode 100644 index 0000000..0e8e8f3 --- /dev/null +++ b/portail/app/Http/Controllers/DevoirEleveController.php @@ -0,0 +1,51 @@ +verifierAppartenance($devoir); + + return view('devoirs.eleve', [ + 'devoir' => $devoir, + 'rendu' => RenduDevoir::where('devoir_id', $devoir->id)->where('eleve_id', $eleve->id)->first(), + ]); + } + + public function rendre(Request $request, Devoir $devoir): RedirectResponse + { + $eleve = $this->verifierAppartenance($devoir); + abort_if($devoir->echeanceDepassee(), 409, 'L’échéance est dépassée.'); + + $donnees = $request->validate(['reponse' => ['required', 'string', 'max:10000']]); + + RenduDevoir::updateOrCreate( + ['devoir_id' => $devoir->id, 'eleve_id' => $eleve->id], + ['reponse' => $donnees['reponse'], 'rendu_a' => now()] + ); + + return redirect()->route('devoirs.eleve.show', $devoir)->with('rendu_envoye', true); + } + + private function verifierAppartenance(Devoir $devoir): Eleve + { + $eleve = Auth::guard('eleve')->user(); + abort_if($devoir->classe_id !== $eleve->classe_id, 403); + + return $eleve; + } +} diff --git a/portail/app/Models/Classe.php b/portail/app/Models/Classe.php index 3c6ac7f..f367d11 100644 --- a/portail/app/Models/Classe.php +++ b/portail/app/Models/Classe.php @@ -73,6 +73,12 @@ class Classe extends Model return $this->hasMany(Quiz::class)->latest(); } + /** @return HasMany */ + public function devoirs(): HasMany + { + return $this->hasMany(Devoir::class)->latest('echeance'); + } + public function estArchivee(): bool { return $this->archivee_a !== null; diff --git a/portail/app/Models/Devoir.php b/portail/app/Models/Devoir.php new file mode 100644 index 0000000..c5ef834 --- /dev/null +++ b/portail/app/Models/Devoir.php @@ -0,0 +1,37 @@ + 'datetime', + ]; + + /** @return BelongsTo */ + public function classe(): BelongsTo + { + return $this->belongsTo(Classe::class); + } + + /** @return HasMany */ + public function rendus(): HasMany + { + return $this->hasMany(RenduDevoir::class); + } + + public function echeanceDepassee(): bool + { + return $this->echeance->isPast(); + } +} diff --git a/portail/app/Models/RenduDevoir.php b/portail/app/Models/RenduDevoir.php new file mode 100644 index 0000000..1e4aa97 --- /dev/null +++ b/portail/app/Models/RenduDevoir.php @@ -0,0 +1,30 @@ + 'datetime', + 'note' => 'integer', + ]; + + /** @return BelongsTo */ + public function devoir(): BelongsTo + { + return $this->belongsTo(Devoir::class); + } + + /** @return BelongsTo */ + public function eleve(): BelongsTo + { + return $this->belongsTo(Eleve::class); + } +} diff --git a/portail/database/migrations/2026_10_13_120000_create_devoirs_table.php b/portail/database/migrations/2026_10_13_120000_create_devoirs_table.php new file mode 100644 index 0000000..5da4a68 --- /dev/null +++ b/portail/database/migrations/2026_10_13_120000_create_devoirs_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('classe_id')->constrained()->cascadeOnDelete(); + $table->string('titre'); + $table->text('consigne'); + $table->timestamp('echeance'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('devoirs'); + } +}; diff --git a/portail/database/migrations/2026_10_13_120001_create_rendus_devoirs_table.php b/portail/database/migrations/2026_10_13_120001_create_rendus_devoirs_table.php new file mode 100644 index 0000000..38a4241 --- /dev/null +++ b/portail/database/migrations/2026_10_13_120001_create_rendus_devoirs_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('devoir_id')->constrained()->cascadeOnDelete(); + $table->foreignId('eleve_id')->constrained()->cascadeOnDelete(); + $table->text('reponse'); + $table->unsignedTinyInteger('note')->nullable(); + $table->timestamp('rendu_a'); + $table->timestamps(); + + $table->unique(['devoir_id', 'eleve_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('rendus_devoirs'); + } +}; diff --git a/portail/resources/views/devoirs/eleve.blade.php b/portail/resources/views/devoirs/eleve.blade.php new file mode 100644 index 0000000..256372d --- /dev/null +++ b/portail/resources/views/devoirs/eleve.blade.php @@ -0,0 +1,41 @@ +@extends('layouts.vitrine') + +@section('titre', $devoir->titre) + +@section('contenu') +
+

Mon espace

+

{{ $devoir->titre }}

+

{{ $devoir->echeanceDepassee() ? 'Échéance dépassée le' : 'À rendre avant le' }} {{ $devoir->echeance->timezone(config('app.fuseau_utilisateurs'))->translatedFormat('d/m/Y H:i') }}

+

{{ $devoir->consigne }}

+ + @if (session('rendu_envoye')) +

Rendu envoyé.

+ @endif + + @if ($rendu && $rendu->note !== null) +

Note : {{ $rendu->note }}/20

+ @endif + + @if ($devoir->echeanceDepassee() && ! $rendu) +

Échéance dépassée : tu n'as pas rendu ce devoir.

+ @else +
+

{{ $rendu ? 'Modifier mon rendu' : 'Rendre ce devoir' }}

+ @if ($rendu) +

Rendu le {{ $rendu->rendu_a->timezone(config('app.fuseau_utilisateurs'))->translatedFormat('d/m/Y H:i') }}. Un nouvel envoi remplace celui-ci{{ $devoir->echeanceDepassee() ? '' : ', jusqu\'à l\'échéance' }}.

+ @endif + @error('reponse')

{{ $message }}

@enderror + @if (! $devoir->echeanceDepassee()) +
+ @csrf + + +
+ @elseif ($rendu) +

{{ $rendu->reponse }}

+ @endif +
+ @endif +
+@endsection diff --git a/portail/resources/views/devoirs/index.blade.php b/portail/resources/views/devoirs/index.blade.php new file mode 100644 index 0000000..94943c0 --- /dev/null +++ b/portail/resources/views/devoirs/index.blade.php @@ -0,0 +1,46 @@ +@extends('layouts.vitrine') + +@section('titre', 'Devoirs · '.$classe->nom) + +@section('contenu') +
+

{{ $classe->nom }}

+

Devoirs maison

+

Les élèves rendent un texte libre (pas de fichier pour l'instant) avant l'échéance. Passée l'échéance, le rendu n'est plus possible.

+ +
+

Nouveau devoir

+ @error('titre')

{{ $message }}

@enderror + @error('consigne')

{{ $message }}

@enderror + @error('echeance')

L'échéance doit être dans le futur.

@enderror +
+ @csrf + + + + +
+
+ +
+

Devoirs ({{ $devoirs->count() }})

+ @if ($devoirs->isEmpty()) +

Aucun devoir pour l'instant.

+ @else + + @endif +
+
+@endsection diff --git a/portail/resources/views/devoirs/show.blade.php b/portail/resources/views/devoirs/show.blade.php new file mode 100644 index 0000000..9d852f8 --- /dev/null +++ b/portail/resources/views/devoirs/show.blade.php @@ -0,0 +1,52 @@ +@extends('layouts.vitrine') + +@section('titre', $devoir->titre) + +@section('contenu') +
+

Devoirs · {{ $classe->nom }}

+
+
+

{{ $devoir->titre }}

+

{{ $devoir->echeanceDepassee() ? 'Échéance dépassée le' : 'À rendre avant le' }} {{ $devoir->echeance->timezone(config('app.fuseau_utilisateurs'))->translatedFormat('d/m/Y H:i') }}

+
+
+ @csrf + +
+
+

{{ $devoir->consigne }}

+ +
+

Rendus ({{ $rendus->count() }}/{{ $eleves->count() }})

+
    + @foreach ($eleves as $eleve) + @php($rendu = $rendus->get($eleve->id)) +
  • +
    + {{ $eleve->nomAffiche() }} + @if ($rendu) + Rendu le {{ $rendu->rendu_a->timezone(config('app.fuseau_utilisateurs'))->translatedFormat('d/m/Y H:i') }} + @else + Pas encore rendu + @endif +
    + @if ($rendu) +

    {{ $rendu->reponse }}

    +
    + @csrf + + +
    + @endif +
  • + @endforeach +
+
+
+ +@endsection diff --git a/portail/resources/views/eleve.blade.php b/portail/resources/views/eleve.blade.php index 2860943..5477ba4 100644 --- a/portail/resources/views/eleve.blade.php +++ b/portail/resources/views/eleve.blade.php @@ -43,6 +43,29 @@ @endif + {{-- Chantier « vrai cockpit », étape 3 : Devoirs maison, réels. --}} + @php($devoirsClasse = $eleve->classe->devoirs()->with(['rendus' => fn ($q) => $q->where('eleve_id', $eleve->id)])->where('echeance', '>=', now()->subDays(7))->get()) + @if ($devoirsClasse->isNotEmpty()) +
+

Devoirs maison

+ +
+ @endif

À faire

diff --git a/portail/resources/views/layouts/vitrine.blade.php b/portail/resources/views/layouts/vitrine.blade.php index 5a26d94..c74c87b 100644 --- a/portail/resources/views/layouts/vitrine.blade.php +++ b/portail/resources/views/layouts/vitrine.blade.php @@ -25,7 +25,7 @@ Syllabus
diff --git a/portail/resources/views/mes-classes/show.blade.php b/portail/resources/views/mes-classes/show.blade.php index a68b2c1..fe0642f 100644 --- a/portail/resources/views/mes-classes/show.blade.php +++ b/portail/resources/views/mes-classes/show.blade.php @@ -31,6 +31,7 @@
  • Élèves
  • {{ $classe->estArchivee() ? 'Classe archivée' : 'Séance' }}
  • Quiz flash
  • +
  • Devoirs maison
  • @if ($seance)
    diff --git a/portail/routes/web.php b/portail/routes/web.php index 698cf98..0827ecf 100644 --- a/portail/routes/web.php +++ b/portail/routes/web.php @@ -5,6 +5,8 @@ use App\Http\Controllers\BanqueQuizController; use App\Http\Controllers\ClasseController; use App\Http\Controllers\ConsoleController; use App\Http\Controllers\CorrectionController; +use App\Http\Controllers\DevoirController; +use App\Http\Controllers\DevoirEleveController; use App\Http\Controllers\EleveTableauDeBordController; use App\Http\Controllers\InscriptionController; use App\Http\Controllers\QuizController; @@ -106,6 +108,10 @@ Route::middleware(['auth:eleve', 'session.unique:eleve'])->group(function () { Route::post('/sessions-quiz/{session}/repondre', [QuizEleveController::class, 'repondre']) ->middleware('throttle:30,1') ->name('sessions-quiz.repondre'); + + // Chantier « vrai cockpit », étape 3 : Devoirs maison. + Route::get('/devoirs/{devoir}', [DevoirEleveController::class, 'show'])->name('devoirs.eleve.show'); + Route::post('/devoirs/{devoir}/rendre', [DevoirEleveController::class, 'rendre'])->name('devoirs.eleve.rendre'); }); // --- Espace prof (protégé, session unique) --- @@ -149,4 +155,11 @@ Route::middleware(['auth:web', 'session.unique:web'])->group(function () { Route::get('/banque-quiz', [BanqueQuizController::class, 'index'])->name('banque-quiz.index'); Route::post('/mes-classes/{classe}/quiz/{quiz}/partager', [BanqueQuizController::class, 'partager'])->name('mes-classes.quiz.partager'); Route::post('/banque-quiz/{quiz}/dupliquer', [BanqueQuizController::class, 'dupliquer'])->name('banque-quiz.dupliquer'); + + // Chantier « vrai cockpit », étape 3 : Devoirs maison. + Route::get('/mes-classes/{classe}/devoirs', [DevoirController::class, 'index'])->name('mes-classes.devoirs.index'); + Route::post('/mes-classes/{classe}/devoirs', [DevoirController::class, 'store'])->name('mes-classes.devoirs.store'); + Route::get('/mes-classes/{classe}/devoirs/{devoir}', [DevoirController::class, 'show'])->name('mes-classes.devoirs.show'); + Route::post('/mes-classes/{classe}/devoirs/{devoir}/supprimer', [DevoirController::class, 'destroy'])->name('mes-classes.devoirs.destroy'); + Route::post('/mes-classes/{classe}/devoirs/{devoir}/rendus/{rendu}/noter', [DevoirController::class, 'noter'])->name('mes-classes.devoirs.noter'); }); diff --git a/portail/tests/Feature/DevoirTest.php b/portail/tests/Feature/DevoirTest.php new file mode 100644 index 0000000..4a8796d --- /dev/null +++ b/portail/tests/Feature/DevoirTest.php @@ -0,0 +1,115 @@ +create(); + $eleve = Eleve::factory()->create(['classe_id' => $classe->id]); + + commeProf($this, $classe->prof)->post(route('mes-classes.devoirs.store', $classe), [ + 'titre' => 'Plan d’adressage', + 'consigne' => 'Découpe 192.168.1.0/24 en 4 sous-réseaux.', + 'echeance' => now()->addDays(3)->format('Y-m-d\TH:i'), + ])->assertRedirect(); + + $devoir = Devoir::where('titre', 'Plan d’adressage')->first(); + expect($devoir)->not->toBeNull()->and($devoir->classe_id)->toBe($classe->id); + + eleve($this, $eleve)->get(route('devoirs.eleve.show', $devoir))->assertOk()->assertSee('Découpe 192.168.1.0/24'); + + eleve($this, $eleve)->post(route('devoirs.eleve.rendre', $devoir), ['reponse' => 'Voici mon découpage : /26 x4.']) + ->assertRedirect(); + + $rendu = RenduDevoir::where('devoir_id', $devoir->id)->where('eleve_id', $eleve->id)->first(); + expect($rendu)->not->toBeNull()->and($rendu->reponse)->toBe('Voici mon découpage : /26 x4.')->and($rendu->note)->toBeNull(); + + commeProf($this, $classe->prof)->get(route('mes-classes.devoirs.show', [$classe, $devoir])) + ->assertSee($eleve->nomAffiche()) + ->assertSee('Voici mon découpage'); + + commeProf($this, $classe->prof)->post(route('mes-classes.devoirs.noter', [$classe, $devoir, $rendu]), ['note' => 15]) + ->assertRedirect(); + expect($rendu->refresh()->note)->toBe(15); +}); + +test('un élève peut modifier son rendu avant l’échéance, un seul rendu par devoir en base', function () { + $classe = Classe::factory()->create(); + $eleve = Eleve::factory()->create(['classe_id' => $classe->id]); + $devoir = Devoir::create(['classe_id' => $classe->id, 'titre' => 'Test', 'consigne' => 'Test', 'echeance' => now()->addDay()]); + + eleve($this, $eleve)->post(route('devoirs.eleve.rendre', $devoir), ['reponse' => 'Premier essai'])->assertRedirect(); + eleve($this, $eleve)->post(route('devoirs.eleve.rendre', $devoir), ['reponse' => 'Version corrigée'])->assertRedirect(); + + expect(RenduDevoir::where('devoir_id', $devoir->id)->count())->toBe(1); + expect(RenduDevoir::first()->reponse)->toBe('Version corrigée'); +}); + +test('un élève ne peut plus rendre après l’échéance', function () { + $classe = Classe::factory()->create(); + $eleve = Eleve::factory()->create(['classe_id' => $classe->id]); + $devoir = Devoir::create(['classe_id' => $classe->id, 'titre' => 'Test', 'consigne' => 'Test', 'echeance' => now()->subHour()]); + + eleve($this, $eleve)->post(route('devoirs.eleve.rendre', $devoir), ['reponse' => 'Trop tard'])->assertStatus(409); + expect(RenduDevoir::count())->toBe(0); +}); + +test('un élève d’une autre classe ne peut pas voir ni rendre ce devoir', function () { + $classe = Classe::factory()->create(); + $devoir = Devoir::create(['classe_id' => $classe->id, 'titre' => 'Test', 'consigne' => 'Test', 'echeance' => now()->addDay()]); + $autreEleve = Eleve::factory()->create(); + + eleve($this, $autreEleve)->get(route('devoirs.eleve.show', $devoir))->assertForbidden(); + eleve($this, $autreEleve)->post(route('devoirs.eleve.rendre', $devoir), ['reponse' => 'Intrus'])->assertForbidden(); +}); + +test('un prof d’une autre classe ne peut pas voir ni noter les rendus', function () { + $classe = Classe::factory()->create(); + $eleve = Eleve::factory()->create(['classe_id' => $classe->id]); + $devoir = Devoir::create(['classe_id' => $classe->id, 'titre' => 'Test', 'consigne' => 'Test', 'echeance' => now()->addDay()]); + $rendu = RenduDevoir::create(['devoir_id' => $devoir->id, 'eleve_id' => $eleve->id, 'reponse' => 'x', 'rendu_a' => now()]); + $autreClasse = Classe::factory()->create(); + + commeProf($this, $autreClasse->prof)->get(route('mes-classes.devoirs.show', [$classe, $devoir]))->assertForbidden(); + commeProf($this, $autreClasse->prof)->post(route('mes-classes.devoirs.noter', [$classe, $devoir, $rendu]), ['note' => 20])->assertForbidden(); +}); + +test('archiver puis purger une classe supprime aussi ses devoirs et rendus', function () { + $classe = Classe::factory()->create(); + $eleve = Eleve::factory()->create(['classe_id' => $classe->id]); + $devoir = Devoir::create(['classe_id' => $classe->id, 'titre' => 'Test', 'consigne' => 'Test', 'echeance' => now()->addDay()]); + RenduDevoir::create(['devoir_id' => $devoir->id, 'eleve_id' => $eleve->id, 'reponse' => 'x', 'rendu_a' => now()]); + + $classe->forceFill(['archivee_a' => now()->subDays(91)])->save(); + $this->artisan('classes:purger-archivees'); + + expect(Devoir::count())->toBe(0)->and(RenduDevoir::count())->toBe(0); +}); + +test('les scripts en ligne de la page de notation d’un devoir sont du JavaScript valide', function () { + if (Process::run('node --version')->failed()) { + $this->markTestSkipped('Node absent'); + } + + $classe = Classe::factory()->create(); + $devoir = Devoir::create(['classe_id' => $classe->id, 'titre' => 'Test', 'consigne' => 'Test', 'echeance' => now()->addDay()]); + + $html = commeProf($this, $classe->prof)->get(route('mes-classes.devoirs.show', [$classe, $devoir]))->getContent(); + preg_match_all('#]*\bsrc=)[^>]*>(.*?)#s', $html, $scripts); + + $erreurs = []; + foreach ($scripts[1] as $i => $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[] = "script {$i} : ".strtok($resultat->errorOutput(), "\n"); + } + } + + expect($erreurs)->toBe([]); +});