seed(\Database\Seeders\RolesSeeder::class); $tenant = Tenant::firstOrCreate( ['name' => 'Test Schule'], ['type' => 'school', 'active' => true] ); $this->user = User::withoutGlobalScopes()->create([ 'name' => 'URL Tester', 'email' => 'url@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('Tasks: default filter is open, server-renders correct from first paint', function () { Livewire::test(TasksIndex::class) ->assertSet('filter', 'open') ->assertSee('Multiplikation bis 100') ->assertDontSee('Gedicht lernen'); }); it('Tasks: filter param from URL hydrates initial state', function () { Livewire::withQueryParams(['filter' => 'done']) ->test(TasksIndex::class) ->assertSet('filter', 'done') ->assertSee('Gedicht lernen') ->assertDontSee('Multiplikation bis 100'); }); it('Tasks: invalid filter param falls back to open', function () { Livewire::withQueryParams(['filter' => 'evil-injection']) ->test(TasksIndex::class) ->assertSet('filter', 'open'); }); it('Tasks: setFilter only accepts allowed values', function () { Livewire::test(TasksIndex::class) ->call('setFilter', 'done') ->assertSet('filter', 'done') ->call('setFilter', 'all') ->assertSet('filter', 'all') ->call('setFilter', 'malicious') ->assertSet('filter', 'all'); }); it('Tasks: filter pill has aria-current=page when active', function () { Livewire::test(TasksIndex::class) ->call('setFilter', 'done') ->assertSeeHtml('data-testid="filter-done"') ->assertSeeHtml('aria-current="page"'); }); it('Tasks: skeleton uses bg-bg-soft (same as table card), with bg-line bars', function () { $html = Livewire::test(TasksIndex::class)->html(); expect($html)->toContain('wire:loading.flex'); expect($html)->toContain('animate-pulse'); expect($html)->toContain('bg-line'); expect($html)->toContain('data-testid="tasks-skeleton"'); // Skeleton container must use same surface as table card (bg-bg-soft within same tag) expect(preg_match('/]+class="[^"]*\bbg-bg-soft\b[^"]*"[^>]+data-testid="tasks-skeleton"/', $html))->toBe(1); }); it('Tasks: task row is NOT clickable; modal trigger is button only', function () { $html = Livewire::test(TasksIndex::class)->html(); // Detail-button is the wire:click trigger expect($html)->toContain('data-testid="task-detail-btn"'); expect($html)->toContain("openModal"); // Row container does NOT have wire:click on itself expect(preg_match('/data-testid="task-row"[^>]*wire:click=/', $html))->toBe(0); }); it('Notifications: default filter is all', function () { Livewire::test(NotificationsIndex::class)->assertSet('filter', 'all'); }); it('Notifications: filter param "unread" hydrates from URL', function () { Livewire::withQueryParams(['filter' => 'unread']) ->test(NotificationsIndex::class) ->assertSet('filter', 'unread'); }); it('Notifications: invalid filter falls back to all', function () { Livewire::withQueryParams(['filter' => 'bogus']) ->test(NotificationsIndex::class) ->assertSet('filter', 'all'); }); it('Notifications: markRead adds key', function () { Livewire::test(NotificationsIndex::class) ->call('markRead', 'n1') ->assertSet('readKeys', ['n1']); }); it('Notifications: skeleton uses bg-bg-soft + bg-line bars', function () { $html = Livewire::test(NotificationsIndex::class)->html(); expect($html)->toContain('data-testid="notif-skeleton"'); expect($html)->toContain('bg-line'); expect(preg_match('/]+class="[^"]*\bbg-bg-soft\b[^"]*"[^>]+data-testid="notif-skeleton"/', $html))->toBe(1); });