diff --git a/app/Livewire/Tasks/Run.php b/app/Livewire/Tasks/Run.php index 0f802c4..5321b45 100644 --- a/app/Livewire/Tasks/Run.php +++ b/app/Livewire/Tasks/Run.php @@ -19,9 +19,18 @@ class Run extends Component public int $totalSteps = 0; public bool $finished = false; public int $correctAnswers = 0; - public ?int $lastAnswerIndex = null; + + /** Last answer payload — varies by question type: + * - choice : ['type'=>'choice', 'index'=>int] + * - cloze : ['type'=>'cloze', 'text'=>string] + * - geo-map : ['type'=>'geo-map','region'=>string] + */ + public ?array $lastAnswer = null; public ?bool $lastAnswerCorrect = null; + /** Active typed input for cloze questions (wire:model). */ + public string $clozeInput = ''; + /** Lerni AI helper panel — toggled via showLerni; can be shared via URL (?lerni=1). */ #[Url(as: 'lerni', keep: false)] public bool $showLerni = false; @@ -46,17 +55,54 @@ class Run extends Component public function currentQuestion(): array { - return $this->questions[$this->currentStep - 1] ?? $this->questions[0]; + $q = $this->questions[$this->currentStep - 1] ?? $this->questions[0]; + // Default type for legacy entries + $q['type'] = $q['type'] ?? 'choice'; + return $q; } - public function submitAnswer(int $optionIndex): void + public function submitChoice(int $optionIndex): void { $q = $this->currentQuestion(); + if (($q['type'] ?? 'choice') !== 'choice') { + return; + } $isCorrect = $optionIndex === ($q['correct'] ?? -1); - - $this->lastAnswerIndex = $optionIndex; + $this->lastAnswer = ['type' => 'choice', 'index' => $optionIndex]; $this->lastAnswerCorrect = $isCorrect; + if ($isCorrect) { + $this->correctAnswers++; + } + } + public function submitCloze(): void + { + $q = $this->currentQuestion(); + if (($q['type'] ?? null) !== 'cloze') { + return; + } + $given = trim(mb_strtolower($this->clozeInput)); + $expected = trim(mb_strtolower((string) ($q['correct'] ?? ''))); + $accept = array_map(fn($a) => trim(mb_strtolower((string) $a)), $q['accept'] ?? []); + + $isCorrect = $given !== '' && ($given === $expected || in_array($given, $accept, true)); + + $this->lastAnswer = ['type' => 'cloze', 'text' => $this->clozeInput]; + $this->lastAnswerCorrect = $isCorrect; + if ($isCorrect) { + $this->correctAnswers++; + } + } + + public function submitRegion(string $region): void + { + $q = $this->currentQuestion(); + if (($q['type'] ?? null) !== 'geo-map') { + return; + } + $isCorrect = $region === ($q['correct'] ?? ''); + $this->lastAnswer = ['type' => 'geo-map', 'region' => $region]; + $this->lastAnswerCorrect = $isCorrect; if ($isCorrect) { $this->correctAnswers++; } @@ -69,9 +115,9 @@ class Run extends Component return; } $this->currentStep++; - $this->lastAnswerIndex = null; + $this->lastAnswer = null; $this->lastAnswerCorrect = null; - // Auto-close Lerni helper when moving to next question + $this->clozeInput = ''; $this->showLerni = false; } @@ -85,8 +131,9 @@ class Run extends Component $this->currentStep = 1; $this->finished = false; $this->correctAnswers = 0; - $this->lastAnswerIndex = null; + $this->lastAnswer = null; $this->lastAnswerCorrect = null; + $this->clozeInput = ''; $this->showLerni = false; } diff --git a/config/task-questions.php b/config/task-questions.php index 49e8a98..cbd9526 100644 --- a/config/task-questions.php +++ b/config/task-questions.php @@ -3,12 +3,18 @@ /** * Task Question Bank — keyed by task `slug`. * - * Each entry: array of question dicts: - * - question: prompt - * - options: [4 strings] - * - correct: int 0..3 - * - lerni_hint: short step-by-step Lerni explanation (KI-Helper) - * - lerni_steps: array of method-steps (KI-Helper "Lösungsweg") + * Each entry: array of question dicts. Shape varies by `type`: + * + * type=choice (default — multiple choice): + * - question, options[4], correct (int 0..3), lerni_hint, lerni_steps[] + * + * type=cloze (fill-in-the-blank): + * - question (use ___ as blank marker), correct (string), accept[] (alt. spellings), + * lerni_hint, lerni_steps[] + * + * type=geo-map (clickable map regions): + * - question, regions[] (each: key, label, d=SVG path), correct (region key), + * viewBox (optional), lerni_hint, lerni_steps[] * * Future: replace with DB-backed Question + Answer models. */ @@ -267,4 +273,122 @@ return [ ], ], + 'deutsch-lueckentexte' => [ + [ + 'type' => 'cloze', + 'question' => 'Der Hund ___ schnell durch den Park.', + 'correct' => 'läuft', + 'accept' => ['rennt', 'lauft'], + 'lerni_hint' => 'Verb für Fortbewegung im Präsens, 3. Person Singular. "läuft" passt zum Hund.', + 'lerni_steps' => ['Subjekt: Der Hund (3. Person Sg.)', 'Verb laufen → er/sie/es läuft', 'Ergebnis: läuft'], + ], + [ + 'type' => 'cloze', + 'question' => 'Lina ___ ein Buch über Tiere.', + 'correct' => 'liest', + 'accept' => ['liesst'], + 'lerni_hint' => 'Verb "lesen" in 3. Person Singular: liest.', + 'lerni_steps' => ['Grundform: lesen', '3. Person Sg.: liest', 'Lina liest ein Buch'], + ], + [ + 'type' => 'cloze', + 'question' => 'Wir ___ heute Pizza zum Mittagessen.', + 'correct' => 'essen', + 'accept' => ['esssen'], + 'lerni_hint' => 'Verb "essen" in 1. Person Plural: wir essen.', + 'lerni_steps' => ['Pronomen: wir', 'Grundform: essen', '1. Pers. Pl.: essen'], + ], + [ + 'type' => 'cloze', + 'question' => 'Die Sonne ___ am Morgen auf.', + 'correct' => 'geht', + 'accept' => ['steigt'], + 'lerni_hint' => 'Trennbares Verb "aufgehen". Stamm "geht" steht im Satzfeld, "auf" am Ende.', + 'lerni_steps' => ['Verb: aufgehen', '3. Person Sg. Präsens', 'Trennbar: geht … auf'], + ], + ], + + 'geografie-europa-laender' => [ + [ + 'type' => 'geo-map', + 'question' => 'Wo liegt Deutschland?', + 'correct' => 'deutschland', + 'viewBox' => '0 0 400 320', + 'regions' => [ + ['key' => 'frankreich', 'label' => 'Frankreich', 'd' => 'M60 150 L130 130 L160 200 L100 240 L40 220 Z'], + ['key' => 'deutschland', 'label' => 'Deutschland', 'd' => 'M170 90 L240 80 L260 160 L200 180 L150 150 Z'], + ['key' => 'italien', 'label' => 'Italien', 'd' => 'M180 200 L220 200 L240 290 L200 300 L170 240 Z'], + ['key' => 'spanien', 'label' => 'Spanien', 'd' => 'M20 220 L100 230 L110 290 L40 295 Z'], + ['key' => 'polen', 'label' => 'Polen', 'd' => 'M260 70 L340 70 L360 160 L290 170 L260 130 Z'], + ['key' => 'oesterreich', 'label' => 'Österreich', 'd' => 'M210 160 L290 165 L295 200 L220 200 Z'], + ], + 'lerni_hint' => 'Deutschland liegt in Mitteleuropa — nördlich von Österreich, westlich von Polen, östlich von Frankreich.', + 'lerni_steps' => ['Suche das mittlere Land', 'Nördlich der Alpen', 'Westlich von Polen', 'Mittelgroß, hellblau markiert'], + ], + [ + 'type' => 'geo-map', + 'question' => 'Wo liegt Frankreich?', + 'correct' => 'frankreich', + 'viewBox' => '0 0 400 320', + 'regions' => [ + ['key' => 'frankreich', 'label' => 'Frankreich', 'd' => 'M60 150 L130 130 L160 200 L100 240 L40 220 Z'], + ['key' => 'deutschland', 'label' => 'Deutschland', 'd' => 'M170 90 L240 80 L260 160 L200 180 L150 150 Z'], + ['key' => 'italien', 'label' => 'Italien', 'd' => 'M180 200 L220 200 L240 290 L200 300 L170 240 Z'], + ['key' => 'spanien', 'label' => 'Spanien', 'd' => 'M20 220 L100 230 L110 290 L40 295 Z'], + ['key' => 'polen', 'label' => 'Polen', 'd' => 'M260 70 L340 70 L360 160 L290 170 L260 130 Z'], + ['key' => 'oesterreich', 'label' => 'Österreich', 'd' => 'M210 160 L290 165 L295 200 L220 200 Z'], + ], + 'lerni_hint' => 'Frankreich liegt westlich von Deutschland, nördlich von Spanien.', + 'lerni_steps' => ['Westlich von Deutschland', 'Nördlich von Spanien', 'Großes Land an der Atlantik-Küste'], + ], + [ + 'type' => 'geo-map', + 'question' => 'Wo liegt Italien?', + 'correct' => 'italien', + 'viewBox' => '0 0 400 320', + 'regions' => [ + ['key' => 'frankreich', 'label' => 'Frankreich', 'd' => 'M60 150 L130 130 L160 200 L100 240 L40 220 Z'], + ['key' => 'deutschland', 'label' => 'Deutschland', 'd' => 'M170 90 L240 80 L260 160 L200 180 L150 150 Z'], + ['key' => 'italien', 'label' => 'Italien', 'd' => 'M180 200 L220 200 L240 290 L200 300 L170 240 Z'], + ['key' => 'spanien', 'label' => 'Spanien', 'd' => 'M20 220 L100 230 L110 290 L40 295 Z'], + ['key' => 'polen', 'label' => 'Polen', 'd' => 'M260 70 L340 70 L360 160 L290 170 L260 130 Z'], + ['key' => 'oesterreich', 'label' => 'Österreich', 'd' => 'M210 160 L290 165 L295 200 L220 200 Z'], + ], + 'lerni_hint' => 'Italien hat Stiefelform — südlich der Alpen, ragt ins Mittelmeer.', + 'lerni_steps' => ['Stiefelform suchen', 'Südlich von Österreich', 'Im Mittelmeer'], + ], + [ + 'type' => 'geo-map', + 'question' => 'Wo liegt Spanien?', + 'correct' => 'spanien', + 'viewBox' => '0 0 400 320', + 'regions' => [ + ['key' => 'frankreich', 'label' => 'Frankreich', 'd' => 'M60 150 L130 130 L160 200 L100 240 L40 220 Z'], + ['key' => 'deutschland', 'label' => 'Deutschland', 'd' => 'M170 90 L240 80 L260 160 L200 180 L150 150 Z'], + ['key' => 'italien', 'label' => 'Italien', 'd' => 'M180 200 L220 200 L240 290 L200 300 L170 240 Z'], + ['key' => 'spanien', 'label' => 'Spanien', 'd' => 'M20 220 L100 230 L110 290 L40 295 Z'], + ['key' => 'polen', 'label' => 'Polen', 'd' => 'M260 70 L340 70 L360 160 L290 170 L260 130 Z'], + ['key' => 'oesterreich', 'label' => 'Österreich', 'd' => 'M210 160 L290 165 L295 200 L220 200 Z'], + ], + 'lerni_hint' => 'Spanien liegt im Südwesten Europas, südlich von Frankreich.', + 'lerni_steps' => ['Iberische Halbinsel', 'Südlich von Frankreich', 'Im Südwesten'], + ], + [ + 'type' => 'geo-map', + 'question' => 'Wo liegt Polen?', + 'correct' => 'polen', + 'viewBox' => '0 0 400 320', + 'regions' => [ + ['key' => 'frankreich', 'label' => 'Frankreich', 'd' => 'M60 150 L130 130 L160 200 L100 240 L40 220 Z'], + ['key' => 'deutschland', 'label' => 'Deutschland', 'd' => 'M170 90 L240 80 L260 160 L200 180 L150 150 Z'], + ['key' => 'italien', 'label' => 'Italien', 'd' => 'M180 200 L220 200 L240 290 L200 300 L170 240 Z'], + ['key' => 'spanien', 'label' => 'Spanien', 'd' => 'M20 220 L100 230 L110 290 L40 295 Z'], + ['key' => 'polen', 'label' => 'Polen', 'd' => 'M260 70 L340 70 L360 160 L290 170 L260 130 Z'], + ['key' => 'oesterreich', 'label' => 'Österreich', 'd' => 'M210 160 L290 165 L295 200 L220 200 Z'], + ], + 'lerni_hint' => 'Polen liegt östlich von Deutschland in Mitteleuropa.', + 'lerni_steps' => ['Östlich von Deutschland', 'Nördlich von Tschechien', 'An der Ostsee'], + ], + ], + ]; diff --git a/config/tasks.php b/config/tasks.php index 83e9f2c..8b62fdd 100644 --- a/config/tasks.php +++ b/config/tasks.php @@ -83,4 +83,22 @@ return [ 'icon' => '🌱', 'color' => 'green', 'points' => 100, 'status' => 'open', 'due' => 'Heute', ], + [ + 'slug' => 'deutsch-lueckentexte', + 'title' => 'Lückentexte: Verben einsetzen', + 'topic' => 'Lückentext', + 'subject' => 'Deutsch', + 'meta' => 'Deutsch · 4 Aufgaben · 8 Min.', + 'icon' => 'A', 'color' => 'rose', + 'points' => 100, 'status' => 'open', 'due' => 'Heute', + ], + [ + 'slug' => 'geografie-europa-laender', + 'title' => 'Europa: Länder finden', + 'topic' => 'Weltkarte', + 'subject' => 'Geografie', + 'meta' => 'Geografie · 5 Aufgaben · 10 Min.', + 'icon' => '🌍', 'color' => 'amber', + 'points' => 130, 'status' => 'open', 'due' => 'Heute', + ], ]; diff --git a/resources/views/livewire/tasks/run.blade.php b/resources/views/livewire/tasks/run.blade.php index 1c931aa..d11e581 100644 --- a/resources/views/livewire/tasks/run.blade.php +++ b/resources/views/livewire/tasks/run.blade.php @@ -9,6 +9,8 @@ $c = $colorMap[$task['color']] ?? $colorMap['primary']; $progressPercent = $totalSteps > 0 ? (int) round(($currentStep / $totalSteps) * 100) : 0; $q = $finished ? null : $this->currentQuestion(); + $qType = $q['type'] ?? 'choice'; + $answered = $lastAnswer !== null; @endphp
@@ -61,45 +63,161 @@ {{-- Question card (col-span 2) --}}
-
Aufgabe
+
+ @switch($qType) + @case('cloze') Lückentext @break + @case('geo-map') Klicke das Land @break + @default Aufgabe + @endswitch +

{{ $q['question'] }}

-
- @foreach($q['options'] as $i => $opt) - @php - $isLast = $lastAnswerIndex === $i; - $cls = 'group px-5 py-5 rounded-lg border-2 text-ink-1 font-semibold text-base text-left transition-all cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 ' . $c['ring']; - $cls .= $lastAnswerIndex === null - ? ' bg-bg-soft border-line hover:border-primary hover:bg-primary-soft' - : ($isLast - ? ($lastAnswerCorrect ? ' bg-green-soft border-green-ink' : ' bg-rose-soft border-rose-ink') - : ' bg-bg-soft border-line opacity-60'); - @endphp - - @endforeach -
+ + @endforeach +
- @if($lastAnswerIndex !== null) + {{-- ========== CLOZE ========== --}} + @elseif($qType === 'cloze') +
+
Setze ein
+
+ + @if(! $answered) + + Prüfen + + + @endif +
+ @if($answered) +
+ @if($lastAnswerCorrect) + Richtig! + @else + + Leider falsch. Lösung: {{ $q['correct'] }} + @endif +
+ @endif +
+ + {{-- ========== GEO-MAP ========== --}} + @elseif($qType === 'geo-map') + @php + $viewBox = $q['viewBox'] ?? '0 0 400 320'; + $picked = ($lastAnswer['type'] ?? null) === 'geo-map' ? ($lastAnswer['region'] ?? null) : null; + $correctKey = $q['correct'] ?? null; + @endphp +
+ + @foreach($q['regions'] as $region) + @php + $rk = $region['key']; + $isPicked = $picked === $rk; + $isCorrect = $correctKey === $rk; + if (! $answered) { + $fill = '#E5E5DF'; $stroke = '#B4B4AC'; + } elseif ($isPicked && $lastAnswerCorrect) { + $fill = 'oklch(0.92 0.06 155)'; $stroke = 'oklch(0.55 0.16 155)'; + } elseif ($isPicked) { + $fill = 'oklch(0.92 0.06 25)'; $stroke = 'oklch(0.6 0.18 25)'; + } elseif ($isCorrect) { + $fill = 'oklch(0.92 0.06 155)'; $stroke = 'oklch(0.55 0.16 155)'; + } else { + $fill = '#E5E5DF'; $stroke = '#B4B4AC'; + } + @endphp + + {{ $region['label'] }} + + @endforeach + + + {{-- Legend / labels --}} +
+ @foreach($q['regions'] as $region) + + @endforeach +
+ @if($answered) +
+ @if($lastAnswerCorrect) + Richtig! + @else + Leider falsch. Markiertes Land ist die Lösung. + @endif +
+ @endif +
+ @endif + + @if($answered)
@if($currentStep < $totalSteps) diff --git a/tests/Feature/Frontend/TaskQuestionBankTest.php b/tests/Feature/Frontend/TaskQuestionBankTest.php index 503ad8a..21481fd 100644 --- a/tests/Feature/Frontend/TaskQuestionBankTest.php +++ b/tests/Feature/Frontend/TaskQuestionBankTest.php @@ -15,14 +15,35 @@ it('config is keyed by task slug (semantic, not by color)', function () { expect(array_key_exists('deutsch-wortarten-nomen-verben', $bank))->toBeTrue(); }); -it('each question has required keys (question, options, correct, lerni_hint, lerni_steps)', function () { +it('each question has required keys by type', function () { $bank = config('task-questions'); foreach ($bank as $slug => $questions) { foreach ($questions as $i => $q) { - expect($q)->toHaveKeys(['question', 'options', 'correct', 'lerni_hint', 'lerni_steps']); - expect($q['options'])->toHaveCount(4); - expect($q['correct'])->toBeInt()->toBeGreaterThanOrEqual(0)->toBeLessThan(4); + $type = $q['type'] ?? 'choice'; + expect($q)->toHaveKeys(['question', 'correct', 'lerni_hint', 'lerni_steps'], "Slug {$slug} q{$i} missing common keys"); expect($q['lerni_steps'])->toBeArray()->not->toBeEmpty(); + + match ($type) { + 'choice' => (function () use ($q, $slug, $i) { + expect($q)->toHaveKey('options'); + expect($q['options'])->toHaveCount(4); + expect($q['correct'])->toBeInt()->toBeGreaterThanOrEqual(0)->toBeLessThan(4); + })(), + 'cloze' => (function () use ($q, $slug, $i) { + expect($q['correct'])->toBeString()->not->toBe(''); + })(), + 'geo-map' => (function () use ($q, $slug, $i) { + expect($q)->toHaveKey('regions'); + expect($q['regions'])->toBeArray()->not->toBeEmpty(); + $keys = array_column($q['regions'], 'key'); + expect(in_array($q['correct'], $keys, true)) + ->toBeTrue("geo-map {$slug} q{$i}: correct '{$q['correct']}' not in regions[]"); + foreach ($q['regions'] as $r) { + expect($r)->toHaveKeys(['key', 'label', 'd']); + } + })(), + default => throw new \LogicException("Unknown type '{$type}' in {$slug} q{$i}"), + }; } } }); diff --git a/tests/Feature/Livewire/TasksRunPageTest.php b/tests/Feature/Livewire/TasksRunPageTest.php index 459c664..c383fed 100644 --- a/tests/Feature/Livewire/TasksRunPageTest.php +++ b/tests/Feature/Livewire/TasksRunPageTest.php @@ -84,17 +84,17 @@ it('math geometry task shows geometry question (not multiplikation)', function ( ->assertDontSee('Was ergibt 12'); }); -it('submitAnswer marks correct/incorrect + correctAnswers counter', function () { +it('submitChoice 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) + ->call('submitChoice', 2) // q1 correct = index 2 (96) + ->assertSet('lastAnswer', ['type' => 'choice', 'index' => 2]) ->assertSet('lastAnswerCorrect', true) ->assertSet('correctAnswers', 1); }); -it('submitAnswer wrong index: no point', function () { +it('submitChoice wrong index: no point', function () { Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100']) - ->call('submitAnswer', 0) + ->call('submitChoice', 0) ->assertSet('lastAnswerCorrect', false) ->assertSet('correctAnswers', 0); }); @@ -103,17 +103,17 @@ 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('submitChoice', 2) ->call('nextStep') ->assertSet('currentStep', 2) - ->assertSet('lastAnswerIndex', null) + ->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('submitAnswer', 0)->call('nextStep'); + $run->call('submitChoice', 0)->call('nextStep'); } $run->assertSet('finished', true); }); @@ -121,16 +121,134 @@ it('finishing all 5 steps sets finished=true', function () { it('restart resets all progress including Lerni state', function () { Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100']) ->call('toggleLerni') - ->call('submitAnswer', 2) + ->call('submitChoice', 2) ->call('nextStep') ->call('restart') ->assertSet('currentStep', 1) ->assertSet('correctAnswers', 0) ->assertSet('finished', false) - ->assertSet('lastAnswerIndex', null) + ->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')