feat(tasks): multi-type questions (cloze + geo-map) + Run.php type dispatch
Tasks Run page now switches on question $type and renders one of three layouts:
choice (existing multiple-choice), cloze (Lückentext text input), geo-map
(clickable SVG region picker). Adds Deutsch-Lückentexte + Geografie-Europa
question banks with the required type-specific keys.
- Run.php: replaces submitAnswer/lastAnswerIndex with submitChoice + submitCloze
+ submitRegion. lastAnswer is now an array { type, ...payload }. Cross-type
calls are no-ops so a stray dispatch can't pollute state.
- run.blade.php: branches on $q['type']; cloze has wire:model.live debounce
+ Enter-to-submit; geo-map has SVG + accessible label-button grid; feedback
pill below each layout.
- config/tasks.php: adds deutsch-lueckentexte + geografie-europa-laender.
- config/task-questions.php: type=cloze entries with correct+accept[] for
case-insensitive alt-spelling match; type=geo-map entries with regions[]
and SVG paths.
- Tests: renames submitAnswer→submitChoice across run page suite; adds full
coverage for cloze (correct/wrong/empty/case/accept[]/payload shape/clear
on next), geo-map (correct/wrong/payload/SVG render), and cross-type
guards. TaskQuestionBank test is now type-aware (choice needs options[4],
cloze needs correct string, geo-map needs regions[]+matching correct key).
Test: 204/204 Pest green. Smoke: 10/10 task routes 200, 4/4 modal components
mount cleanly, no errors written to laravel.log during the smoke pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
main
parent
d3f976b18e
commit
88945b47f6
|
|
@ -19,9 +19,18 @@ class Run extends Component
|
||||||
public int $totalSteps = 0;
|
public int $totalSteps = 0;
|
||||||
public bool $finished = false;
|
public bool $finished = false;
|
||||||
public int $correctAnswers = 0;
|
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;
|
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). */
|
/** Lerni AI helper panel — toggled via showLerni; can be shared via URL (?lerni=1). */
|
||||||
#[Url(as: 'lerni', keep: false)]
|
#[Url(as: 'lerni', keep: false)]
|
||||||
public bool $showLerni = false;
|
public bool $showLerni = false;
|
||||||
|
|
@ -46,17 +55,54 @@ class Run extends Component
|
||||||
|
|
||||||
public function currentQuestion(): array
|
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();
|
$q = $this->currentQuestion();
|
||||||
|
if (($q['type'] ?? 'choice') !== 'choice') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
$isCorrect = $optionIndex === ($q['correct'] ?? -1);
|
$isCorrect = $optionIndex === ($q['correct'] ?? -1);
|
||||||
|
$this->lastAnswer = ['type' => 'choice', 'index' => $optionIndex];
|
||||||
$this->lastAnswerIndex = $optionIndex;
|
|
||||||
$this->lastAnswerCorrect = $isCorrect;
|
$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) {
|
if ($isCorrect) {
|
||||||
$this->correctAnswers++;
|
$this->correctAnswers++;
|
||||||
}
|
}
|
||||||
|
|
@ -69,9 +115,9 @@ class Run extends Component
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$this->currentStep++;
|
$this->currentStep++;
|
||||||
$this->lastAnswerIndex = null;
|
$this->lastAnswer = null;
|
||||||
$this->lastAnswerCorrect = null;
|
$this->lastAnswerCorrect = null;
|
||||||
// Auto-close Lerni helper when moving to next question
|
$this->clozeInput = '';
|
||||||
$this->showLerni = false;
|
$this->showLerni = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,8 +131,9 @@ class Run extends Component
|
||||||
$this->currentStep = 1;
|
$this->currentStep = 1;
|
||||||
$this->finished = false;
|
$this->finished = false;
|
||||||
$this->correctAnswers = 0;
|
$this->correctAnswers = 0;
|
||||||
$this->lastAnswerIndex = null;
|
$this->lastAnswer = null;
|
||||||
$this->lastAnswerCorrect = null;
|
$this->lastAnswerCorrect = null;
|
||||||
|
$this->clozeInput = '';
|
||||||
$this->showLerni = false;
|
$this->showLerni = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,18 @@
|
||||||
/**
|
/**
|
||||||
* Task Question Bank — keyed by task `slug`.
|
* Task Question Bank — keyed by task `slug`.
|
||||||
*
|
*
|
||||||
* Each entry: array of question dicts:
|
* Each entry: array of question dicts. Shape varies by `type`:
|
||||||
* - question: prompt
|
*
|
||||||
* - options: [4 strings]
|
* type=choice (default — multiple choice):
|
||||||
* - correct: int 0..3
|
* - question, options[4], correct (int 0..3), lerni_hint, lerni_steps[]
|
||||||
* - lerni_hint: short step-by-step Lerni explanation (KI-Helper)
|
*
|
||||||
* - lerni_steps: array of method-steps (KI-Helper "Lösungsweg")
|
* 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.
|
* 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'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -83,4 +83,22 @@ return [
|
||||||
'icon' => '🌱', 'color' => 'green',
|
'icon' => '🌱', 'color' => 'green',
|
||||||
'points' => 100, 'status' => 'open', 'due' => 'Heute',
|
'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',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@
|
||||||
$c = $colorMap[$task['color']] ?? $colorMap['primary'];
|
$c = $colorMap[$task['color']] ?? $colorMap['primary'];
|
||||||
$progressPercent = $totalSteps > 0 ? (int) round(($currentStep / $totalSteps) * 100) : 0;
|
$progressPercent = $totalSteps > 0 ? (int) round(($currentStep / $totalSteps) * 100) : 0;
|
||||||
$q = $finished ? null : $this->currentQuestion();
|
$q = $finished ? null : $this->currentQuestion();
|
||||||
|
$qType = $q['type'] ?? 'choice';
|
||||||
|
$answered = $lastAnswer !== null;
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="flex flex-col gap-6 max-w-6xl mx-auto w-full" data-testid="task-run-page">
|
<div class="flex flex-col gap-6 max-w-6xl mx-auto w-full" data-testid="task-run-page">
|
||||||
|
|
@ -61,45 +63,161 @@
|
||||||
|
|
||||||
{{-- Question card (col-span 2) --}}
|
{{-- Question card (col-span 2) --}}
|
||||||
<div class="col-span-2 bg-bg-soft border border-line rounded-xl p-8 shadow-sm" wire:key="step-{{ $currentStep }}">
|
<div class="col-span-2 bg-bg-soft border border-line rounded-xl p-8 shadow-sm" wire:key="step-{{ $currentStep }}">
|
||||||
<div class="text-[11px] uppercase tracking-[0.12em] text-ink-3 font-semibold mb-3">Aufgabe</div>
|
<div class="text-[11px] uppercase tracking-[0.12em] text-ink-3 font-semibold mb-3" data-testid="run-q-type-{{ $qType }}">
|
||||||
|
@switch($qType)
|
||||||
|
@case('cloze') Lückentext @break
|
||||||
|
@case('geo-map') Klicke das Land @break
|
||||||
|
@default Aufgabe
|
||||||
|
@endswitch
|
||||||
|
</div>
|
||||||
<h2 class="text-2xl font-bold tracking-tight text-ink-1 mb-8 leading-snug" data-testid="run-question">
|
<h2 class="text-2xl font-bold tracking-tight text-ink-1 mb-8 leading-snug" data-testid="run-question">
|
||||||
{{ $q['question'] }}
|
{{ $q['question'] }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="grid grid-cols-2 gap-3">
|
{{-- ========== CHOICE ========== --}}
|
||||||
@foreach($q['options'] as $i => $opt)
|
@if($qType === 'choice')
|
||||||
@php
|
<div class="grid grid-cols-2 gap-3">
|
||||||
$isLast = $lastAnswerIndex === $i;
|
@foreach($q['options'] as $i => $opt)
|
||||||
$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'];
|
@php
|
||||||
$cls .= $lastAnswerIndex === null
|
$isLast = $answered && ($lastAnswer['type'] ?? null) === 'choice' && ($lastAnswer['index'] ?? null) === $i;
|
||||||
? ' bg-bg-soft border-line hover:border-primary hover:bg-primary-soft'
|
$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'];
|
||||||
: ($isLast
|
$cls .= ! $answered
|
||||||
? ($lastAnswerCorrect ? ' bg-green-soft border-green-ink' : ' bg-rose-soft border-rose-ink')
|
? ' bg-bg-soft border-line hover:border-primary hover:bg-primary-soft'
|
||||||
: ' bg-bg-soft border-line opacity-60');
|
: ($isLast
|
||||||
@endphp
|
? ($lastAnswerCorrect ? ' bg-green-soft border-green-ink' : ' bg-rose-soft border-rose-ink')
|
||||||
<button
|
: ' bg-bg-soft border-line opacity-60');
|
||||||
type="button"
|
@endphp
|
||||||
@if($lastAnswerIndex !== null) disabled @else wire:click="submitAnswer({{ $i }})" @endif
|
<button
|
||||||
class="{{ $cls }}"
|
type="button"
|
||||||
data-testid="run-option-{{ $i }}"
|
@if($answered) disabled @else wire:click="submitChoice({{ $i }})" @endif
|
||||||
>
|
class="{{ $cls }}"
|
||||||
<span class="flex items-center gap-3">
|
data-testid="run-option-{{ $i }}"
|
||||||
<span class="w-7 h-7 rounded-full grid place-items-center text-xs font-bold font-mono tabular-nums shrink-0
|
>
|
||||||
{{ $isLast ? ($lastAnswerCorrect ? 'bg-green-ink text-white' : 'bg-rose-ink text-white') : 'bg-bg-tint text-ink-2' }}">
|
<span class="flex items-center gap-3">
|
||||||
{{ chr(65 + $i) }}
|
<span class="w-7 h-7 rounded-full grid place-items-center text-xs font-bold font-mono tabular-nums shrink-0
|
||||||
|
{{ $isLast ? ($lastAnswerCorrect ? 'bg-green-ink text-white' : 'bg-rose-ink text-white') : 'bg-bg-tint text-ink-2' }}">
|
||||||
|
{{ chr(65 + $i) }}
|
||||||
|
</span>
|
||||||
|
<span>{{ $opt }}</span>
|
||||||
|
@if($isLast && $lastAnswerCorrect)
|
||||||
|
<x-heroicon-s-check-circle class="w-5 h-5 text-green-ink ml-auto shrink-0" />
|
||||||
|
@elseif($isLast)
|
||||||
|
<x-heroicon-s-x-circle class="w-5 h-5 text-rose-ink ml-auto shrink-0" />
|
||||||
|
@endif
|
||||||
</span>
|
</span>
|
||||||
<span>{{ $opt }}</span>
|
</button>
|
||||||
@if($isLast && $lastAnswerCorrect)
|
@endforeach
|
||||||
<x-heroicon-s-check-circle class="w-5 h-5 text-green-ink ml-auto shrink-0" />
|
</div>
|
||||||
@elseif($isLast)
|
|
||||||
<x-heroicon-s-x-circle class="w-5 h-5 text-rose-ink ml-auto shrink-0" />
|
|
||||||
@endif
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
@endforeach
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if($lastAnswerIndex !== null)
|
{{-- ========== CLOZE ========== --}}
|
||||||
|
@elseif($qType === 'cloze')
|
||||||
|
<div class="bg-bg-tint rounded-lg p-5 mb-4 border border-line">
|
||||||
|
<div class="text-[10px] uppercase tracking-wider text-ink-3 font-semibold mb-2">Setze ein</div>
|
||||||
|
<div class="flex items-center gap-3 flex-wrap">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
wire:model.live.debounce.150ms="clozeInput"
|
||||||
|
@if(! $answered) wire:keydown.enter="submitCloze" @endif
|
||||||
|
@if($answered) disabled @endif
|
||||||
|
autocomplete="off"
|
||||||
|
autocapitalize="none"
|
||||||
|
spellcheck="false"
|
||||||
|
class="flex-1 min-w-[180px] px-4 py-3 rounded-lg border-2 font-mono text-lg bg-bg-soft text-ink-1 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 {{ $c['ring'] }}
|
||||||
|
{{ $answered ? ($lastAnswerCorrect ? 'border-green-ink bg-green-soft' : 'border-rose-ink bg-rose-soft') : 'border-line focus:border-primary' }}"
|
||||||
|
placeholder="Antwort eingeben…"
|
||||||
|
data-testid="run-cloze-input"
|
||||||
|
/>
|
||||||
|
@if(! $answered)
|
||||||
|
<x-btn variant="primary" wire:click="submitCloze" data-testid="run-cloze-submit">
|
||||||
|
Prüfen
|
||||||
|
<x-heroicon-o-check class="w-4 h-4" />
|
||||||
|
</x-btn>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@if($answered)
|
||||||
|
<div class="mt-3 text-sm font-semibold {{ $lastAnswerCorrect ? 'text-green-ink' : 'text-rose-ink' }}" data-testid="run-cloze-feedback">
|
||||||
|
@if($lastAnswerCorrect)
|
||||||
|
<x-heroicon-s-check-circle class="w-4 h-4 inline" /> Richtig!
|
||||||
|
@else
|
||||||
|
<x-heroicon-s-x-circle class="w-4 h-4 inline" />
|
||||||
|
Leider falsch. Lösung: <span class="font-mono">{{ $q['correct'] }}</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- ========== 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
|
||||||
|
<div class="bg-primary-soft/30 rounded-lg p-4 border border-line">
|
||||||
|
<svg viewBox="{{ $viewBox }}" class="w-full h-auto max-h-[420px]" role="img" aria-label="Europa-Karte" data-testid="run-geomap">
|
||||||
|
@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
|
||||||
|
<path
|
||||||
|
d="{{ $region['d'] }}"
|
||||||
|
fill="{{ $fill }}"
|
||||||
|
stroke="{{ $stroke }}"
|
||||||
|
stroke-width="2"
|
||||||
|
@if(! $answered)
|
||||||
|
wire:click="submitRegion('{{ $rk }}')"
|
||||||
|
class="cursor-pointer transition-all hover:opacity-80"
|
||||||
|
@endif
|
||||||
|
data-testid="run-geomap-region-{{ $rk }}"
|
||||||
|
>
|
||||||
|
<title>{{ $region['label'] }}</title>
|
||||||
|
</path>
|
||||||
|
@endforeach
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{{-- Legend / labels --}}
|
||||||
|
<div class="mt-4 grid grid-cols-3 gap-2 text-xs">
|
||||||
|
@foreach($q['regions'] as $region)
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@if($answered) disabled @else wire:click="submitRegion('{{ $region['key'] }}')" @endif
|
||||||
|
class="px-3 py-2 rounded-md border font-semibold text-ink-2 transition-colors text-left
|
||||||
|
@if($answered && $region['key'] === ($picked ?: '') && $lastAnswerCorrect) bg-green-soft border-green-ink text-green-ink
|
||||||
|
@elseif($answered && $region['key'] === ($picked ?: '')) bg-rose-soft border-rose-ink text-rose-ink
|
||||||
|
@elseif($answered && $region['key'] === $correctKey) bg-green-soft border-green-ink text-green-ink
|
||||||
|
@else bg-bg-soft border-line hover:border-primary @endif"
|
||||||
|
data-testid="run-geomap-btn-{{ $region['key'] }}"
|
||||||
|
>
|
||||||
|
{{ $region['label'] }}
|
||||||
|
</button>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@if($answered)
|
||||||
|
<div class="mt-4 text-sm font-semibold {{ $lastAnswerCorrect ? 'text-green-ink' : 'text-rose-ink' }}" data-testid="run-geomap-feedback">
|
||||||
|
@if($lastAnswerCorrect)
|
||||||
|
<x-heroicon-s-check-circle class="w-4 h-4 inline" /> Richtig!
|
||||||
|
@else
|
||||||
|
<x-heroicon-s-x-circle class="w-4 h-4 inline" /> Leider falsch. Markiertes Land ist die Lösung.
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($answered)
|
||||||
<div class="mt-6 flex justify-end">
|
<div class="mt-6 flex justify-end">
|
||||||
<x-btn variant="primary" wire:click="nextStep" data-testid="run-next">
|
<x-btn variant="primary" wire:click="nextStep" data-testid="run-next">
|
||||||
@if($currentStep < $totalSteps)
|
@if($currentStep < $totalSteps)
|
||||||
|
|
|
||||||
|
|
@ -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();
|
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');
|
$bank = config('task-questions');
|
||||||
foreach ($bank as $slug => $questions) {
|
foreach ($bank as $slug => $questions) {
|
||||||
foreach ($questions as $i => $q) {
|
foreach ($questions as $i => $q) {
|
||||||
expect($q)->toHaveKeys(['question', 'options', 'correct', 'lerni_hint', 'lerni_steps']);
|
$type = $q['type'] ?? 'choice';
|
||||||
expect($q['options'])->toHaveCount(4);
|
expect($q)->toHaveKeys(['question', 'correct', 'lerni_hint', 'lerni_steps'], "Slug {$slug} q{$i} missing common keys");
|
||||||
expect($q['correct'])->toBeInt()->toBeGreaterThanOrEqual(0)->toBeLessThan(4);
|
|
||||||
expect($q['lerni_steps'])->toBeArray()->not->toBeEmpty();
|
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}"),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -84,17 +84,17 @@ it('math geometry task shows geometry question (not multiplikation)', function (
|
||||||
->assertDontSee('Was ergibt 12');
|
->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'])
|
Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100'])
|
||||||
->call('submitAnswer', 2) // q1 correct = index 2 (96)
|
->call('submitChoice', 2) // q1 correct = index 2 (96)
|
||||||
->assertSet('lastAnswerIndex', 2)
|
->assertSet('lastAnswer', ['type' => 'choice', 'index' => 2])
|
||||||
->assertSet('lastAnswerCorrect', true)
|
->assertSet('lastAnswerCorrect', true)
|
||||||
->assertSet('correctAnswers', 1);
|
->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'])
|
Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100'])
|
||||||
->call('submitAnswer', 0)
|
->call('submitChoice', 0)
|
||||||
->assertSet('lastAnswerCorrect', false)
|
->assertSet('lastAnswerCorrect', false)
|
||||||
->assertSet('correctAnswers', 0);
|
->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'])
|
Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100'])
|
||||||
->call('toggleLerni')
|
->call('toggleLerni')
|
||||||
->assertSet('showLerni', true)
|
->assertSet('showLerni', true)
|
||||||
->call('submitAnswer', 2)
|
->call('submitChoice', 2)
|
||||||
->call('nextStep')
|
->call('nextStep')
|
||||||
->assertSet('currentStep', 2)
|
->assertSet('currentStep', 2)
|
||||||
->assertSet('lastAnswerIndex', null)
|
->assertSet('lastAnswer', null)
|
||||||
->assertSet('showLerni', false);
|
->assertSet('showLerni', false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('finishing all 5 steps sets finished=true', function () {
|
it('finishing all 5 steps sets finished=true', function () {
|
||||||
$run = Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100']);
|
$run = Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100']);
|
||||||
foreach (range(1, 5) as $step) {
|
foreach (range(1, 5) as $step) {
|
||||||
$run->call('submitAnswer', 0)->call('nextStep');
|
$run->call('submitChoice', 0)->call('nextStep');
|
||||||
}
|
}
|
||||||
$run->assertSet('finished', true);
|
$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 () {
|
it('restart resets all progress including Lerni state', function () {
|
||||||
Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100'])
|
Livewire::test(Run::class, ['slug' => 'math-multiplikation-bis-100'])
|
||||||
->call('toggleLerni')
|
->call('toggleLerni')
|
||||||
->call('submitAnswer', 2)
|
->call('submitChoice', 2)
|
||||||
->call('nextStep')
|
->call('nextStep')
|
||||||
->call('restart')
|
->call('restart')
|
||||||
->assertSet('currentStep', 1)
|
->assertSet('currentStep', 1)
|
||||||
->assertSet('correctAnswers', 0)
|
->assertSet('correctAnswers', 0)
|
||||||
->assertSet('finished', false)
|
->assertSet('finished', false)
|
||||||
->assertSet('lastAnswerIndex', null)
|
->assertSet('lastAnswer', null)
|
||||||
->assertSet('showLerni', false);
|
->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 () {
|
it('startTask in Index redirects to tasks.run with slug', function () {
|
||||||
Livewire::test(\App\Livewire\Tasks\Index::class)
|
Livewire::test(\App\Livewire\Tasks\Index::class)
|
||||||
->call('startTask', 'math-multiplikation-bis-100')
|
->call('startTask', 'math-multiplikation-bis-100')
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue