lernschiff/tests/Feature/Livewire/TasksRunPageTest.php

325 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?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('submitChoice marks correct/incorrect + correctAnswers counter', function () {
Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100'])
->call('submitChoice', 2) // q1 correct = index 2 (96)
->assertSet('lastAnswer', ['type' => 'choice', 'index' => 2])
->assertSet('lastAnswerCorrect', true)
->assertSet('correctAnswers', 1);
});
it('submitChoice wrong index: no point', function () {
Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100'])
->call('submitChoice', 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('submitChoice', 2)
->call('nextStep')
->assertSet('currentStep', 2)
->assertSet('lastAnswer', 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('submitChoice', 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('submitChoice', 2)
->call('nextStep')
->call('restart')
->assertSet('currentStep', 1)
->assertSet('correctAnswers', 0)
->assertSet('finished', false)
->assertSet('lastAnswer', null)
->assertSet('showLerni', false);
});
// ===== Cloze (Lückentext) tests =====
it('cloze: deutsch-lueckentexte loads with cloze type', function () {
Livewire::test(Run::class, ['slug' => 'deutsch-lueckentexte'])
->assertSee('Lückentext')
->assertSee('Der Hund');
});
it('cloze: submitCloze with correct text marks correct', function () {
Livewire::test(Run::class, ['slug' => 'deutsch-lueckentexte'])
->set('clozeInput', 'läuft')
->call('submitCloze')
->assertSet('lastAnswerCorrect', true)
->assertSet('correctAnswers', 1);
});
it('cloze: case-insensitive match', function () {
Livewire::test(Run::class, ['slug' => 'deutsch-lueckentexte'])
->set('clozeInput', 'LÄUFT')
->call('submitCloze')
->assertSet('lastAnswerCorrect', true);
});
it('cloze: accept[] alternative accepted', function () {
Livewire::test(Run::class, ['slug' => 'deutsch-lueckentexte'])
->set('clozeInput', 'rennt')
->call('submitCloze')
->assertSet('lastAnswerCorrect', true);
});
it('cloze: empty input is not correct', function () {
Livewire::test(Run::class, ['slug' => 'deutsch-lueckentexte'])
->set('clozeInput', '')
->call('submitCloze')
->assertSet('lastAnswerCorrect', false);
});
it('cloze: wrong answer recorded as incorrect', function () {
Livewire::test(Run::class, ['slug' => 'deutsch-lueckentexte'])
->set('clozeInput', 'springt')
->call('submitCloze')
->assertSet('lastAnswerCorrect', false)
->assertSet('correctAnswers', 0);
});
it('cloze: lastAnswer payload shape is cloze', function () {
Livewire::test(Run::class, ['slug' => 'deutsch-lueckentexte'])
->set('clozeInput', 'läuft')
->call('submitCloze')
->assertSet('lastAnswer', ['type' => 'cloze', 'text' => 'läuft']);
});
it('cloze: nextStep clears clozeInput', function () {
Livewire::test(Run::class, ['slug' => 'deutsch-lueckentexte'])
->set('clozeInput', 'läuft')
->call('submitCloze')
->call('nextStep')
->assertSet('clozeInput', '')
->assertSet('lastAnswer', null);
});
// ===== Geo-Map tests =====
it('geo-map: geografie-europa-laender loads with geo-map type', function () {
Livewire::test(Run::class, ['slug' => 'geografie-europa-laender'])
->assertSee('Klicke das Land')
->assertSee('Wo liegt Deutschland?');
});
it('geo-map: submitRegion correct key marks correct', function () {
Livewire::test(Run::class, ['slug' => 'geografie-europa-laender'])
->call('submitRegion', 'deutschland')
->assertSet('lastAnswerCorrect', true)
->assertSet('correctAnswers', 1);
});
it('geo-map: submitRegion wrong key marks incorrect', function () {
Livewire::test(Run::class, ['slug' => 'geografie-europa-laender'])
->call('submitRegion', 'spanien')
->assertSet('lastAnswerCorrect', false)
->assertSet('correctAnswers', 0);
});
it('geo-map: lastAnswer payload shape is geo-map', function () {
Livewire::test(Run::class, ['slug' => 'geografie-europa-laender'])
->call('submitRegion', 'deutschland')
->assertSet('lastAnswer', ['type' => 'geo-map', 'region' => 'deutschland']);
});
it('geo-map: SVG region paths render in HTML', function () {
$html = Livewire::test(Run::class, ['slug' => 'geografie-europa-laender'])->html();
expect($html)->toContain('data-testid="run-geomap"');
expect($html)->toContain('data-testid="run-geomap-region-deutschland"');
expect($html)->toContain('data-testid="run-geomap-region-frankreich"');
});
// ===== Cross-type guards =====
it('cross-type: submitChoice does nothing for cloze question', function () {
Livewire::test(Run::class, ['slug' => 'deutsch-lueckentexte'])
->call('submitChoice', 0)
->assertSet('lastAnswer', null)
->assertSet('correctAnswers', 0);
});
it('cross-type: submitCloze does nothing for choice question', function () {
Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100'])
->set('clozeInput', 'whatever')
->call('submitCloze')
->assertSet('lastAnswer', null);
});
it('cross-type: submitRegion does nothing for choice question', function () {
Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100'])
->call('submitRegion', 'deutschland')
->assertSet('lastAnswer', null);
});
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();
}
}
});