seed(\Database\Seeders\RolesSeeder::class); $tenant = Tenant::firstOrCreate(['name' => 'Test Schule'], ['type' => 'school', 'active' => true]); $this->user = User::withoutGlobalScopes()->create([ 'name' => 'Run Tester', 'email' => 'run@test.example', 'tenant_id' => $tenant->id, 'password' => bcrypt('password'), ]); $role = Role::where('name', 'child')->first(); DB::table('role_user')->insert(['user_id' => $this->user->id, 'role_id' => $role->id, 'created_at' => now(), 'updated_at' => now()]); $license = License::withoutGlobalScopes()->create([ 'tenant_id' => $tenant->id, 'type' => 'school_flat', 'expires_at' => now()->addYear(), 'active' => true, ]); DB::table('license_assignments')->insert(['license_id' => $license->id, 'user_id' => $this->user->id, 'created_at' => now(), 'updated_at' => now()]); $this->actingAs($this->user); }); it('route tasks.run registered with slug param', function () { expect(\Illuminate\Support\Facades\Route::has('tasks.run'))->toBeTrue(); }); it('GET /tasks/{slug}/run returns 200 for valid slug', function () { $this->get(route('tasks.run', ['slug' => 'math-multiplikation-bis-100']))->assertOk(); }); it('GET /tasks/{slug}/run with unknown slug returns 404', function () { $this->get(route('tasks.run', ['slug' => 'nonexistent-slug']))->assertNotFound(); }); it('GET /tasks/{slug}/run with bogus URL chars returns 404', function () { // Route regex [a-z0-9-]+ filters injection attempts $this->get('/tasks/UPPER_CASE/run')->assertNotFound(); }); it('Run mount: math slug loads MATH questions (not generic)', function () { $component = Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100']); $component->assertSet('totalSteps', 5); $component->assertSee('Multiplikation bis 100'); $component->assertSee('Was ergibt 12 × 8?'); $component->assertDontSee('Welches Wort ist ein Nomen?'); }); it('Run mount: deutsch slug loads GERMAN questions', function () { $component = Livewire::test(Run::class, ['slug' => 'deutsch-wortarten-nomen-verben']); $component->assertSee('Welches Wort ist ein Nomen?'); $component->assertDontSee('Was ergibt 12 × 8?'); }); it('Run mount: english vocab loads ENGLISH questions', function () { $component = Livewire::test(Run::class, ['slug' => 'englisch-vocab-animals']); $component->assertSee('Translate "Katze"'); }); it('Run mount: sachkunde wasserkreislauf loads NATURE questions', function () { $component = Livewire::test(Run::class, ['slug' => 'sachkunde-wasserkreislauf']); $component->assertSee('Verdampfen von Wasser'); }); it('math multiplikation task shows multiplication question', function () { Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100']) ->assertSee('Was ergibt 12'); }); it('math geometry task shows geometry question (not multiplikation)', function () { Livewire::withoutLazyLoading(); Livewire::test(Run::class, ['slug' => 'math-geometrie-flaechen']) ->assertSee('Rechtecks') ->assertDontSee('Was ergibt 12'); }); it('submitAnswer marks correct/incorrect + correctAnswers counter', function () { Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100']) ->call('submitAnswer', 2) // q1 correct = index 2 (96) ->assertSet('lastAnswerIndex', 2) ->assertSet('lastAnswerCorrect', true) ->assertSet('correctAnswers', 1); }); it('submitAnswer wrong index: no point', function () { Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100']) ->call('submitAnswer', 0) ->assertSet('lastAnswerCorrect', false) ->assertSet('correctAnswers', 0); }); it('nextStep advances + clears feedback + closes Lerni helper', function () { Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100']) ->call('toggleLerni') ->assertSet('showLerni', true) ->call('submitAnswer', 2) ->call('nextStep') ->assertSet('currentStep', 2) ->assertSet('lastAnswerIndex', null) ->assertSet('showLerni', false); }); it('finishing all 5 steps sets finished=true', function () { $run = Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100']); foreach (range(1, 5) as $step) { $run->call('submitAnswer', 0)->call('nextStep'); } $run->assertSet('finished', true); }); it('restart resets all progress including Lerni state', function () { Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100']) ->call('toggleLerni') ->call('submitAnswer', 2) ->call('nextStep') ->call('restart') ->assertSet('currentStep', 1) ->assertSet('correctAnswers', 0) ->assertSet('finished', false) ->assertSet('lastAnswerIndex', null) ->assertSet('showLerni', false); }); it('startTask in Index redirects to tasks.run with slug', function () { Livewire::test(\App\Livewire\Tasks\Index::class) ->call('startTask', 'math-multiplikation-bis-100') ->assertRedirect(route('tasks.run', ['slug' => 'math-multiplikation-bis-100'])); }); it('unauthenticated user redirected to login', function () { auth()->logout(); $this->get(route('tasks.run', ['slug' => 'math-multiplikation-bis-100']))->assertRedirect(route('login')); }); it('Lerni panel renders in HTML', function () { $html = Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100'])->html(); expect($html)->toContain('data-testid="lerni-panel"'); expect($html)->toContain('Lerni'); expect($html)->toContain('data-testid="lerni-open"'); }); it('toggleLerni reveals lerni-hint + lerni-steps', function () { $html = Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100']) ->call('toggleLerni') ->html(); expect($html)->toContain('data-testid="lerni-hint"'); expect($html)->toContain('data-testid="lerni-steps"'); expect($html)->toContain('Zerlege 12 in 10 + 2'); }); it('Lerni shortlink URL contains slug + ?lerni=1', function () { $html = Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100']) ->call('toggleLerni') ->html(); expect($html)->toContain('data-testid="lerni-shortlink"'); expect($html)->toContain('math-multiplikation-bis-100'); expect($html)->toContain('?lerni=1'); }); it('?lerni=1 query auto-opens helper on first paint', function () { Livewire::withQueryParams(['lerni' => 1]) ->test(Run::class, ['slug' => 'math-multiplikation-bis-100']) ->assertSet('showLerni', true); }); it('Run placeholder view exists with task-run-skeleton testid', function () { $view = Run::placeholder([]); expect($view->getName())->toBe('livewire.tasks.run-placeholder'); expect($view->render())->toContain('task-run-skeleton'); }); it('config/tasks.php uses semantic slugs (not t1/t2)', function () { $tasks = config('tasks'); foreach ($tasks as $t) { expect($t)->toHaveKey('slug'); expect($t['slug'])->toMatch('/^[a-z0-9-]+$/', "Slug '{$t['slug']}' must be lowercase + hyphens"); expect($t['slug'])->not->toMatch('/^t[0-9]+$/', "Slug '{$t['slug']}' is generic — use semantic name"); } }); it('every task slug has a matching question bank entry', function () { foreach (config('tasks', []) as $t) { $questions = config("task-questions.{$t['slug']}"); expect($questions)->toBeArray()->not->toBeEmpty("Slug '{$t['slug']}' missing questions"); } }); it('each question has lerni_hint + lerni_steps for AI helper', function () { foreach (config('task-questions', []) as $slug => $questions) { foreach ($questions as $i => $q) { expect($q)->toHaveKey('lerni_hint'); expect($q)->toHaveKey('lerni_steps'); expect($q['lerni_steps'])->toBeArray()->not->toBeEmpty(); } } });