diff --git a/app/Livewire/Tasks/Index.php b/app/Livewire/Tasks/Index.php index 9e7bee0..969f837 100644 --- a/app/Livewire/Tasks/Index.php +++ b/app/Livewire/Tasks/Index.php @@ -53,16 +53,10 @@ class Index extends Component $this->startedKeys[] = $key; } - // Start = open the Runner modal (actual quiz/exercise) — NOT the Detail - // modal which is reserved for the info-icon button. Use $this->js() to - // emit a browser-side Livewire dispatch — server-side $this->dispatch() - // with ->to(...) in Livewire 4 + wire-elements drops named payload args. - $payload = json_encode([ - 'component' => 'tasks.modals.runner', - 'arguments' => ['task' => $task], - ], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR); - - $this->js("Livewire.dispatch('openModal', {$payload})"); + // Start navigates to the dedicated Run page (subject-specific quiz) + // instead of opening a modal — same data but per-subject questions + // and a structured URL. + $this->redirectRoute('tasks.run', ['key' => $key], navigate: true); } public function repeatTask(string $key): void @@ -73,15 +67,7 @@ class Index extends Component #[Computed] public function tasks(): array { - return [ - ['key' => 't1', 'title' => 'Multiplikation bis 100', 'meta' => 'Mathematik · 12 Aufgaben · 15 Min.', 'icon' => '∑', 'color' => 'primary', 'points' => 120, 'status' => 'open', 'due' => 'Heute'], - ['key' => 't2', 'title' => 'Wortarten: Nomen & Verben', 'meta' => 'Deutsch · Lese-Aufgabe · 10 Min.', 'icon' => 'A', 'color' => 'rose', 'points' => 80, 'status' => 'open', 'due' => 'Heute'], - ['key' => 't3', 'title' => 'Vocab Review: Animals', 'meta' => 'Englisch · Karteikarten · 8 Min.', 'icon' => 'E', 'color' => 'violet', 'points' => 60, 'status' => 'open', 'due' => 'Heute'], - ['key' => 't4', 'title' => 'Kreislauf des Wassers', 'meta' => 'Sachkunde · Video + Quiz · 12 Min.', 'icon' => '🌱', 'color' => 'green', 'points' => 100, 'status' => 'open', 'due' => 'Heute'], - ['key' => 't5', 'title' => 'Geometrie: Flächen', 'meta' => 'Mathematik · Übung · 20 Min.', 'icon' => '∑', 'color' => 'primary', 'points' => 150, 'status' => 'open', 'due' => 'Morgen'], - ['key' => 't6', 'title' => 'Gedicht lernen', 'meta' => 'Deutsch · Lese-Aufgabe · 15 Min.', 'icon' => 'A', 'color' => 'rose', 'points' => 90, 'status' => 'done', 'due' => 'Gestern'], - ['key' => 't7', 'title' => 'Past Simple Übung', 'meta' => 'Englisch · Übung · 10 Min.', 'icon' => 'E', 'color' => 'violet', 'points' => 70, 'status' => 'done', 'due' => 'Gestern'], - ]; + return config('tasks', []); } #[Computed] diff --git a/app/Livewire/Tasks/Run.php b/app/Livewire/Tasks/Run.php new file mode 100644 index 0000000..1847cf4 --- /dev/null +++ b/app/Livewire/Tasks/Run.php @@ -0,0 +1,90 @@ + 'tasks'])] +#[Lazy] +class Run extends Component +{ + public string $key = ''; + public array $task = []; + public array $questions = []; + public int $currentStep = 1; + public int $totalSteps = 0; + public bool $finished = false; + public int $correctAnswers = 0; + public ?int $lastAnswerIndex = null; + public ?bool $lastAnswerCorrect = null; + + public function mount(string $key): void + { + $task = $this->findTask($key); + abort_if($task === null, 404, "Task '{$key}' not found."); + + $this->key = $key; + $this->task = $task; + $this->questions = config("task-questions.{$task['color']}", []); + $this->totalSteps = count($this->questions); + + abort_if($this->totalSteps === 0, 404, "No questions configured for color '{$task['color']}'."); + } + + public static function placeholder(array $params = []): View + { + return view('livewire.tasks.run-placeholder'); + } + + public function currentQuestion(): array + { + return $this->questions[$this->currentStep - 1] ?? $this->questions[0]; + } + + public function submitAnswer(int $optionIndex): void + { + $q = $this->currentQuestion(); + $isCorrect = $optionIndex === ($q['correct'] ?? -1); + + $this->lastAnswerIndex = $optionIndex; + $this->lastAnswerCorrect = $isCorrect; + + if ($isCorrect) { + $this->correctAnswers++; + } + } + + public function nextStep(): void + { + if ($this->currentStep >= $this->totalSteps) { + $this->finished = true; + return; + } + $this->currentStep++; + $this->lastAnswerIndex = null; + $this->lastAnswerCorrect = null; + } + + public function restart(): void + { + $this->currentStep = 1; + $this->finished = false; + $this->correctAnswers = 0; + $this->lastAnswerIndex = null; + $this->lastAnswerCorrect = null; + } + + /** Locate task by key — placeholder data (later DB-backed). */ + protected function findTask(string $key): ?array + { + return collect(config('tasks', []))->firstWhere('key', $key); + } + + public function render(): View + { + return view('livewire.tasks.run'); + } +} diff --git a/config/task-questions.php b/config/task-questions.php new file mode 100644 index 0000000..38bc770 --- /dev/null +++ b/config/task-questions.php @@ -0,0 +1,63 @@ + of 4 answer choices + * - correct: int index (0..3) of correct option + * + * Subjects: + * primary -> Mathematik + * rose -> Deutsch + * violet -> Englisch + * green -> Sachkunde + * amber -> Musik / Sport / Philosophie + * + * Future: replace with DB-backed Question + Answer models, scoped by tenant. + */ + +return [ + + 'primary' => [ + ['question' => 'Was ergibt 12 × 8?', 'options' => ['64', '88', '96', '102'], 'correct' => 2], + ['question' => 'Was ergibt 15 × 7?', 'options' => ['95', '105', '115', '125'], 'correct' => 1], + ['question' => 'Was ergibt 9 × 11?', 'options' => ['91', '99', '108', '111'], 'correct' => 1], + ['question' => 'Was ergibt 14 × 6?', 'options' => ['72', '78', '84', '88'], 'correct' => 2], + ['question' => 'Was ergibt 144 ÷ 12?', 'options' => ['10', '11', '12', '13'], 'correct' => 2], + ], + + 'rose' => [ + ['question' => 'Welches Wort ist ein Nomen?', 'options' => ['laufen', 'schnell', 'Hund', 'rot'], 'correct' => 2], + ['question' => 'Welches Wort ist ein Verb?', 'options' => ['Tisch', 'springen', 'grün', 'Lehrer'], 'correct' => 1], + ['question' => 'Welches Wort ist ein Adjektiv?', 'options' => ['Schule', 'lesen', 'glücklich', 'gehen'], 'correct' => 2], + ['question' => 'Wie wird "Buch" im Plural geschrieben?', 'options' => ['Buchs', 'Buchen', 'Bücher', 'Buches'], 'correct' => 2], + ['question' => 'Welcher Artikel passt zu "Sonne"?', 'options' => ['der', 'die', 'das', 'den'], 'correct' => 1], + ], + + 'violet' => [ + ['question' => 'What is the past tense of "go"?', 'options' => ['goed', 'goes', 'gone', 'went'], 'correct' => 3], + ['question' => 'Translate "Katze" to English:', 'options' => ['dog', 'cat', 'cow', 'mouse'], 'correct' => 1], + ['question' => 'Which is the correct plural of "child"?', 'options' => ['childs', 'childes', 'children', 'childer'], 'correct' => 2], + ['question' => 'Pick the correct: "She ___ a book."', 'options' => ['read', 'reads', 'reading', 'reader'], 'correct' => 1], + ['question' => 'Translate "Apfel" to English:', 'options' => ['apple', 'orange', 'banana', 'lemon'], 'correct' => 0], + ], + + 'green' => [ + ['question' => 'Was passiert beim Verdampfen von Wasser?', 'options' => ['Es wird flüssig', 'Es wird zu Dampf', 'Es gefriert', 'Es verschwindet'], 'correct' => 1], + ['question' => 'Wie viele Beine hat eine Spinne?', 'options' => ['6', '8', '10', '12'], 'correct' => 1], + ['question' => 'Welches Tier ist KEIN Säugetier?', 'options' => ['Hund', 'Pferd', 'Forelle', 'Katze'], 'correct' => 2], + ['question' => 'In welcher Jahreszeit fallen die Blätter?', 'options' => ['Frühling', 'Sommer', 'Herbst', 'Winter'], 'correct' => 2], + ['question' => 'Welcher Planet ist der Sonne am nächsten?', 'options' => ['Erde', 'Mars', 'Merkur', 'Venus'], 'correct' => 2], + ], + + 'amber' => [ + ['question' => 'Wie viele Linien hat ein Notensystem?', 'options' => ['3', '4', '5', '6'], 'correct' => 2], + ['question' => 'Welche Note ist die Längste?', 'options' => ['Achtel', 'Viertel', 'Halbe', 'Ganze'], 'correct' => 3], + ['question' => 'Was bedeutet "forte" in der Musik?', 'options' => ['leise', 'laut', 'schnell', 'langsam'], 'correct' => 1], + ['question' => 'Wie viele Saiten hat eine normale Gitarre?', 'options' => ['4', '5', '6', '7'], 'correct' => 2], + ['question' => 'Was ist ein Komponist?', 'options' => ['Lehrer', 'Musik-Erfinder', 'Sänger', 'Instrumentenbauer'], 'correct' => 1], + ], + +]; diff --git a/config/tasks.php b/config/tasks.php new file mode 100644 index 0000000..c8d1004 --- /dev/null +++ b/config/tasks.php @@ -0,0 +1,21 @@ + 't1', 'title' => 'Multiplikation bis 100', 'meta' => 'Mathematik · 12 Aufgaben · 15 Min.', 'icon' => '∑', 'color' => 'primary', 'points' => 120, 'status' => 'open', 'due' => 'Heute'], + ['key' => 't2', 'title' => 'Wortarten: Nomen & Verben', 'meta' => 'Deutsch · Lese-Aufgabe · 10 Min.', 'icon' => 'A', 'color' => 'rose', 'points' => 80, 'status' => 'open', 'due' => 'Heute'], + ['key' => 't3', 'title' => 'Vocab Review: Animals', 'meta' => 'Englisch · Karteikarten · 8 Min.', 'icon' => 'E', 'color' => 'violet', 'points' => 60, 'status' => 'open', 'due' => 'Heute'], + ['key' => 't4', 'title' => 'Kreislauf des Wassers', 'meta' => 'Sachkunde · Video + Quiz · 12 Min.', 'icon' => '🌱', 'color' => 'green', 'points' => 100, 'status' => 'open', 'due' => 'Heute'], + ['key' => 't5', 'title' => 'Geometrie: Flächen', 'meta' => 'Mathematik · Übung · 20 Min.', 'icon' => '∑', 'color' => 'primary', 'points' => 150, 'status' => 'open', 'due' => 'Morgen'], + ['key' => 't6', 'title' => 'Gedicht lernen', 'meta' => 'Deutsch · Lese-Aufgabe · 15 Min.', 'icon' => 'A', 'color' => 'rose', 'points' => 90, 'status' => 'done', 'due' => 'Gestern'], + ['key' => 't7', 'title' => 'Past Simple Übung', 'meta' => 'Englisch · Übung · 10 Min.', 'icon' => 'E', 'color' => 'violet', 'points' => 70, 'status' => 'done', 'due' => 'Gestern'], +]; diff --git a/resources/views/livewire/tasks/run-placeholder.blade.php b/resources/views/livewire/tasks/run-placeholder.blade.php new file mode 100644 index 0000000..17f87a9 --- /dev/null +++ b/resources/views/livewire/tasks/run-placeholder.blade.php @@ -0,0 +1,22 @@ +
+ {{-- Top nav skeleton --}} +
+
+
+
+ + {{-- Task header card --}} + + + {{-- Question card --}} +
+
+
+ +
+ @for($i = 0; $i < 4; $i++) +
+ @endfor +
+
+
diff --git a/resources/views/livewire/tasks/run.blade.php b/resources/views/livewire/tasks/run.blade.php new file mode 100644 index 0000000..953f5e0 --- /dev/null +++ b/resources/views/livewire/tasks/run.blade.php @@ -0,0 +1,154 @@ +@php + $colorMap = [ + 'primary' => ['bg' => 'bg-primary-soft', 'ink' => 'text-primary-ink', 'bar' => 'bg-primary-ink', 'ring' => 'focus-visible:ring-primary'], + 'rose' => ['bg' => 'bg-rose-soft', 'ink' => 'text-rose-ink', 'bar' => 'bg-rose-ink', 'ring' => 'focus-visible:ring-rose-ink'], + 'violet' => ['bg' => 'bg-violet-soft', 'ink' => 'text-violet-ink', 'bar' => 'bg-violet-ink', 'ring' => 'focus-visible:ring-violet-ink'], + 'green' => ['bg' => 'bg-green-soft', 'ink' => 'text-green-ink', 'bar' => 'bg-green-ink', 'ring' => 'focus-visible:ring-green-ink'], + 'amber' => ['bg' => 'bg-amber-soft', 'ink' => 'text-amber-ink', 'bar' => 'bg-amber-ink', 'ring' => 'focus-visible:ring-amber-ink'], + ]; + $c = $colorMap[$task['color']] ?? $colorMap['primary']; + $progressPercent = $totalSteps > 0 ? (int) round(($currentStep / $totalSteps) * 100) : 0; +@endphp + +
+ + {{-- Breadcrumb / back nav --}} +
+ + + Zurück zu Aufgaben + + {{ $task['meta'] ?? '' }} +
+ + {{-- Task header --}} +
+
+ {{ $task['icon'] }} +
+
+
{{ $task['meta'] }}
+

{{ $task['title'] }}

+
+
+
Belohnung
+
+{{ (int) $task['points'] }} Pkt.
+
+
+ + @if(! $finished) + {{-- Progress --}} +
+
+ + Frage {{ $currentStep }} / {{ $totalSteps }} + + + {{ $correctAnswers }} richtig + +
+
+
+
+
+ + {{-- Question --}} + @php $q = $this->currentQuestion(); @endphp +
+
Aufgabe
+

+ {{ $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) +
+ + @if($currentStep < $totalSteps) + Nächste Frage + + @else + Aufgabe abschließen + + @endif + +
+ @endif +
+ @else + @php + $passed = $correctAnswers >= (int) ceil($totalSteps / 2); + $percent = $totalSteps > 0 ? (int) round(($correctAnswers / $totalSteps) * 100) : 0; + $pointsEarned = $passed ? (int) $task['points'] : 0; + @endphp +
+
+ @if($passed) + + @else + + @endif +
+

+ @if($passed) Super gemacht! @else Fast geschafft @endif +

+

+ Du hast {{ $correctAnswers }} + von {{ $totalSteps }} Aufgaben richtig. +

+

{{ $percent }} % Erfolgsquote

+ + @if($pointsEarned > 0) +
+ + +{{ $pointsEarned }} Pkt. + verdient +
+ @endif + +
+ + + Nochmal + + + + Fertig — zurück + +
+
+ @endif + +
diff --git a/routes/web.php b/routes/web.php index 4f943f8..d6202a3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -13,6 +13,9 @@ Route::middleware(['auth', 'license'])->group(function () { Route::get('/dashboard', \App\Livewire\Dashboard\Index::class)->name('dashboard'); Route::get('/subjects', \App\Livewire\Subjects\Index::class)->name('subjects'); Route::get('/tasks', \App\Livewire\Tasks\Index::class)->name('tasks'); + Route::get('/tasks/{key}/run', \App\Livewire\Tasks\Run::class) + ->whereAlphaNumeric('key') + ->name('tasks.run'); Route::get('/photo', \App\Livewire\PhotoHelp\Index::class)->name('photo'); Route::get('/rewards', \App\Livewire\Rewards\Index::class)->name('rewards'); Route::get('/progress', \App\Livewire\Progress\Index::class)->name('progress'); diff --git a/tests/Feature/Frontend/TaskQuestionBankTest.php b/tests/Feature/Frontend/TaskQuestionBankTest.php new file mode 100644 index 0000000..1539822 --- /dev/null +++ b/tests/Feature/Frontend/TaskQuestionBankTest.php @@ -0,0 +1,31 @@ +toBeTrue(); +}); + +it('config has buckets for all 5 subject colors', function () { + $bank = config('task-questions'); + foreach (['primary', 'rose', 'violet', 'green', 'amber'] as $color) { + expect(array_key_exists($color, $bank))->toBeTrue("Question bank missing color '{$color}'"); + expect($bank[$color])->toBeArray()->not->toBeEmpty(); + } +}); + +it('each question has required keys (question, options, correct)', function () { + $bank = config('task-questions'); + foreach ($bank as $color => $questions) { + foreach ($questions as $i => $q) { + expect($q)->toHaveKeys(['question', 'options', 'correct']); + expect($q['options'])->toHaveCount(4); + expect($q['correct'])->toBeInt()->toBeGreaterThanOrEqual(0)->toBeLessThan(4); + } + } +}); + +it('Math (primary) questions differ from German (rose)', function () { + $bank = config('task-questions'); + $mathFirst = $bank['primary'][0]['question']; + $germanFirst = $bank['rose'][0]['question']; + expect($mathFirst)->not->toBe($germanFirst); +}); diff --git a/tests/Feature/Livewire/TasksRunPageTest.php b/tests/Feature/Livewire/TasksRunPageTest.php new file mode 100644 index 0000000..a949ade --- /dev/null +++ b/tests/Feature/Livewire/TasksRunPageTest.php @@ -0,0 +1,130 @@ +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 key param', function () { + expect(\Illuminate\Support\Facades\Route::has('tasks.run'))->toBeTrue(); +}); + +it('GET /tasks/t1/run returns 200 for authenticated user', function () { + $this->get(route('tasks.run', ['key' => 't1']))->assertOk(); +}); + +it('GET /tasks/{key}/run with unknown key returns 404', function () { + $this->get(route('tasks.run', ['key' => 'nonexistent']))->assertNotFound(); +}); + +it('Run mount: math task (t1, primary) loads MATH questions', function () { + $component = Livewire::test(Run::class, ['key' => 't1']); + $component->assertSet('totalSteps', 5); // primary bank has 5 questions + $component->assertSee('Multiplikation bis 100'); + $component->assertSee('Was ergibt 12 × 8?'); + $component->assertDontSee('Welches Wort ist ein Nomen?'); +}); + +it('Run mount: deutsch task (t2, rose) loads GERMAN questions', function () { + $component = Livewire::test(Run::class, ['key' => 't2']); + $component->assertSee('Welches Wort ist ein Nomen?'); + $component->assertDontSee('Was ergibt 12 × 8?'); +}); + +it('Run mount: english task (t3, violet) loads ENGLISH questions', function () { + $component = Livewire::test(Run::class, ['key' => 't3']); + $component->assertSee('past tense of "go"'); + $component->assertDontSee('Welches Wort ist ein Nomen?'); +}); + +it('Run mount: sachkunde task (t4, green) loads NATURE questions', function () { + $component = Livewire::test(Run::class, ['key' => 't4']); + $component->assertSee('Verdampfen von Wasser'); +}); + +it('submitAnswer marks correct/incorrect + correctAnswers counter', function () { + Livewire::test(Run::class, ['key' => 't1']) + ->call('submitAnswer', 2) // primary q1: correct = 2 (96) + ->assertSet('lastAnswerIndex', 2) + ->assertSet('lastAnswerCorrect', true) + ->assertSet('correctAnswers', 1); +}); + +it('submitAnswer with wrong index sets lastAnswerCorrect=false, no point', function () { + Livewire::test(Run::class, ['key' => 't1']) + ->call('submitAnswer', 0) + ->assertSet('lastAnswerCorrect', false) + ->assertSet('correctAnswers', 0); +}); + +it('nextStep advances and clears lastAnswer state', function () { + Livewire::test(Run::class, ['key' => 't1']) + ->call('submitAnswer', 2) + ->call('nextStep') + ->assertSet('currentStep', 2) + ->assertSet('lastAnswerIndex', null) + ->assertSet('lastAnswerCorrect', null); +}); + +it('finishing last step sets finished=true', function () { + $run = Livewire::test(Run::class, ['key' => 't1']); + // primary bank has 5 questions + foreach (range(1, 5) as $step) { + $run->call('submitAnswer', 0)->call('nextStep'); + } + $run->assertSet('finished', true); +}); + +it('restart resets all progress', function () { + Livewire::test(Run::class, ['key' => 't1']) + ->call('submitAnswer', 2) + ->call('nextStep') + ->call('restart') + ->assertSet('currentStep', 1) + ->assertSet('correctAnswers', 0) + ->assertSet('finished', false) + ->assertSet('lastAnswerIndex', null); +}); + +it('startTask in Index redirects to tasks.run route (not modal)', function () { + Livewire::test(\App\Livewire\Tasks\Index::class) + ->call('startTask', 't1') + ->assertRedirect(route('tasks.run', ['key' => 't1'])); +}); + +it('unauthenticated user redirected to login', function () { + auth()->logout(); + $this->get(route('tasks.run', ['key' => 't1']))->assertRedirect(route('login')); +}); + +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'); +}); diff --git a/tests/Feature/Livewire/TasksStartButtonTest.php b/tests/Feature/Livewire/TasksStartButtonTest.php index 1ed2362..7abc650 100644 --- a/tests/Feature/Livewire/TasksStartButtonTest.php +++ b/tests/Feature/Livewire/TasksStartButtonTest.php @@ -66,18 +66,10 @@ it('startTask action runs and adds key to startedKeys', function () { ->assertSet('startedKeys', ['t1']); }); -it('startTask emits browser-side Livewire.dispatch via $this->js() targeting RUNNER modal (not detail)', function () { - $tester = Livewire::test(TasksIndex::class)->call('startTask', 't1'); - - $effects = $tester->effects ?? []; - $scripts = $effects['xjs'] ?? $effects['js'] ?? []; - $allScripts = implode("\n", is_array($scripts) ? array_map(fn($s) => is_array($s) ? json_encode($s) : (string) $s, $scripts) : [(string) $scripts]); - - expect($allScripts)->toContain("Livewire.dispatch('openModal'"); - // Start button opens RUNNER, not detail - expect($allScripts)->toContain('tasks.modals.runner'); - expect($allScripts)->not->toContain('tasks.modals.detail'); - expect($allScripts)->toContain('t1'); +it('startTask redirects to tasks.run page (dedicated quiz, not modal)', function () { + Livewire::test(TasksIndex::class) + ->call('startTask', 't1') + ->assertRedirect(route('tasks.run', ['key' => 't1'])); }); it('Detail (info) button still opens tasks.modals.detail — separate from Start', function () {