207 lines
8.0 KiB
PHP
207 lines
8.0 KiB
PHP
<?php
|
||
|
||
use App\Livewire\Tasks\Run;
|
||
use App\Models\License;
|
||
use App\Models\Role;
|
||
use App\Models\Tenant;
|
||
use App\Models\User;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Livewire\Livewire;
|
||
|
||
uses(\Illuminate\Foundation\Testing\RefreshDatabase::class);
|
||
|
||
beforeEach(function () {
|
||
$this->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();
|
||
}
|
||
}
|
||
});
|