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
+
- @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
+
+
+
+ {{-- Legend / labels --}}
+
+ @foreach($q['regions'] as $region)
+
+ {{ $region['label'] }}
+
+ @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')