admin = User::factory()->admin()->create(); }); // =========================================== // Pending Bookings Widget Tests // =========================================== test('pending bookings widget displays pending count', function () { Consultation::factory()->count(3)->pending()->create(); $this->actingAs($this->admin); Volt::test('admin.widgets.pending-bookings') ->assertSee('3') ->assertSee(__('widgets.pending_bookings')); }); test('pending bookings widget shows empty state when none pending', function () { $this->actingAs($this->admin); Volt::test('admin.widgets.pending-bookings') ->assertSee(__('widgets.no_pending_bookings')); }); test('pending bookings widget shows up to 5 bookings', function () { $clients = User::factory()->count(7)->individual()->create(); foreach ($clients as $client) { Consultation::factory()->pending()->create([ 'user_id' => $client->id, ]); } $this->actingAs($this->admin); $component = Volt::test('admin.widgets.pending-bookings'); $pendingBookings = $component->viewData('pendingBookings'); expect($pendingBookings)->toHaveCount(5); expect($component->viewData('pendingCount'))->toBe(7); }); test('pending bookings widget shows view all link when more than 5', function () { Consultation::factory()->count(7)->pending()->create(); $this->actingAs($this->admin); Volt::test('admin.widgets.pending-bookings') ->assertSee(__('widgets.view_all_pending', ['count' => 7])); }); test('pending bookings widget displays client name and consultation type', function () { $client = User::factory()->individual()->create(['full_name' => 'Test Client Name']); Consultation::factory()->pending()->free()->create([ 'user_id' => $client->id, ]); $this->actingAs($this->admin); Volt::test('admin.widgets.pending-bookings') ->assertSee('Test Client Name'); }); test('pending bookings widget handles missing user relationship gracefully', function () { // The cascade delete means if user is deleted, consultation is deleted too // This test verifies that the widget at least doesn't crash with the optional chaining // by checking that it handles the view rendering without errors $client = User::factory()->individual()->create(); $consultation = Consultation::factory()->pending()->create([ 'user_id' => $client->id, ]); $this->actingAs($this->admin); // Widget should render successfully with the client name Volt::test('admin.widgets.pending-bookings') ->assertSee($client->full_name); }); test('pending bookings widget shows badge count 99+ for large counts', function () { Consultation::factory()->count(105)->pending()->create(); $this->actingAs($this->admin); $component = Volt::test('admin.widgets.pending-bookings'); expect($component->viewData('pendingCount'))->toBe(105); }); // =========================================== // Today's Schedule Widget Tests // =========================================== test('today schedule widget shows only today approved consultations', function () { $client = User::factory()->individual()->create(['full_name' => 'Today Client']); // Today's approved - should show Consultation::factory()->approved()->create([ 'user_id' => $client->id, 'booking_date' => today(), 'booking_time' => '10:00:00', ]); // Tomorrow's approved - should NOT show Consultation::factory()->approved()->create([ 'booking_date' => today()->addDay(), ]); // Today's pending - should NOT show Consultation::factory()->pending()->create([ 'booking_date' => today(), ]); $this->actingAs($this->admin); $component = Volt::test('admin.widgets.todays-schedule'); $todaySchedule = $component->viewData('todaySchedule'); expect($todaySchedule)->toHaveCount(1); $component->assertSee('Today Client') ->assertSee('10:00 AM'); }); test('today schedule widget shows empty state when no consultations', function () { $this->actingAs($this->admin); Volt::test('admin.widgets.todays-schedule') ->assertSee(__('widgets.no_consultations_today')); }); test('admin can mark consultation as completed', function () { $consultation = Consultation::factory()->approved()->create([ 'booking_date' => today(), ]); $this->actingAs($this->admin); Volt::test('admin.widgets.todays-schedule') ->call('markComplete', $consultation->id) ->assertHasNoErrors(); expect($consultation->fresh()->status)->toBe(ConsultationStatus::Completed); }); test('admin can mark consultation as no-show', function () { $consultation = Consultation::factory()->approved()->create([ 'booking_date' => today(), ]); $this->actingAs($this->admin); Volt::test('admin.widgets.todays-schedule') ->call('markNoShow', $consultation->id) ->assertHasNoErrors(); expect($consultation->fresh()->status)->toBe(ConsultationStatus::NoShow); }); test('today schedule widget orders by time', function () { $client = User::factory()->individual()->create(); Consultation::factory()->approved()->create([ 'user_id' => $client->id, 'booking_date' => today(), 'booking_time' => '14:00:00', ]); Consultation::factory()->approved()->create([ 'user_id' => $client->id, 'booking_date' => today(), 'booking_time' => '09:00:00', ]); $this->actingAs($this->admin); $component = Volt::test('admin.widgets.todays-schedule'); $todaySchedule = $component->viewData('todaySchedule'); expect($todaySchedule->first()->booking_time)->toBe('09:00:00'); expect($todaySchedule->last()->booking_time)->toBe('14:00:00'); }); // =========================================== // Recent Timeline Updates Widget Tests // =========================================== test('recent updates widget shows last 5 updates', function () { $timeline = Timeline::factory()->create(); TimelineUpdate::factory()->count(7)->create(['timeline_id' => $timeline->id]); $this->actingAs($this->admin); $component = Volt::test('admin.widgets.recent-updates'); $recentUpdates = $component->viewData('recentUpdates'); expect($recentUpdates)->toHaveCount(5); }); test('recent updates widget shows empty state when no updates', function () { $this->actingAs($this->admin); Volt::test('admin.widgets.recent-updates') ->assertSee(__('widgets.no_recent_updates')); }); test('recent updates widget displays case name and client name', function () { $client = User::factory()->individual()->create(['full_name' => 'Case Owner']); $timeline = Timeline::factory()->create([ 'user_id' => $client->id, 'case_name' => 'Test Case Name', ]); TimelineUpdate::factory()->create([ 'timeline_id' => $timeline->id, 'update_text' => 'This is a test update text', ]); $this->actingAs($this->admin); Volt::test('admin.widgets.recent-updates') ->assertSee('Test Case Name') ->assertSee('Case Owner') ->assertSee('This is a test update text'); }); test('recent updates widget truncates long update text', function () { $timeline = Timeline::factory()->create(); TimelineUpdate::factory()->create([ 'timeline_id' => $timeline->id, 'update_text' => str_repeat('A', 100), ]); $this->actingAs($this->admin); $component = Volt::test('admin.widgets.recent-updates'); // Should show truncated text (50 chars) $component->assertDontSee(str_repeat('A', 100)); }); test('recent updates widget shows relative timestamp', function () { $timeline = Timeline::factory()->create(); $update = TimelineUpdate::factory()->create([ 'timeline_id' => $timeline->id, 'created_at' => now()->subHours(2), ]); $this->actingAs($this->admin); $component = Volt::test('admin.widgets.recent-updates'); // Check that the component has the update with a created_at timestamp $recentUpdates = $component->viewData('recentUpdates'); // Verify the timestamp is correctly set (approximately 2 hours ago) expect($recentUpdates->first()->created_at->diffInHours(now()))->toBeGreaterThanOrEqual(2); }); // =========================================== // Quick Actions Widget Tests // =========================================== test('quick actions widget displays action buttons', function () { $this->actingAs($this->admin); Volt::test('admin.widgets.quick-actions') ->assertSee(__('widgets.create_client')) ->assertSee(__('widgets.create_post')) ->assertSee(__('widgets.block_time_slot')); }); test('admin can block a time slot', function () { $this->actingAs($this->admin); Volt::test('admin.widgets.quick-actions') ->call('openBlockModal') ->set('blockDate', today()->format('Y-m-d')) ->set('blockStartTime', '09:00') ->set('blockEndTime', '10:00') ->set('blockReason', 'Personal appointment') ->call('blockTimeSlot') ->assertHasNoErrors(); expect(BlockedTime::count())->toBe(1); expect(BlockedTime::first()) ->block_date->toDateString()->toBe(today()->toDateString()) ->start_time->toBe('09:00') ->end_time->toBe('10:00') ->reason->toBe('Personal appointment'); }); test('block time slot validates required fields', function () { $this->actingAs($this->admin); Volt::test('admin.widgets.quick-actions') ->call('openBlockModal') ->set('blockDate', '') ->call('blockTimeSlot') ->assertHasErrors(['blockDate']); }); test('block time slot prevents past dates', function () { $this->actingAs($this->admin); Volt::test('admin.widgets.quick-actions') ->call('openBlockModal') ->set('blockDate', today()->subDay()->format('Y-m-d')) ->set('blockStartTime', '09:00') ->set('blockEndTime', '10:00') ->call('blockTimeSlot') ->assertHasErrors(['blockDate']); }); test('block time slot validates end time after start time', function () { $this->actingAs($this->admin); Volt::test('admin.widgets.quick-actions') ->call('openBlockModal') ->set('blockDate', today()->format('Y-m-d')) ->set('blockStartTime', '10:00') ->set('blockEndTime', '09:00') ->call('blockTimeSlot') ->assertHasErrors(['blockEndTime']); }); test('block time slot reason is optional', function () { $this->actingAs($this->admin); Volt::test('admin.widgets.quick-actions') ->call('openBlockModal') ->set('blockDate', today()->format('Y-m-d')) ->set('blockStartTime', '09:00') ->set('blockEndTime', '10:00') ->set('blockReason', '') ->call('blockTimeSlot') ->assertHasNoErrors(); expect(BlockedTime::first()->reason)->toBeNull(); }); test('block modal closes after successful submission', function () { $this->actingAs($this->admin); $component = Volt::test('admin.widgets.quick-actions') ->call('openBlockModal'); expect($component->get('showBlockModal'))->toBeTrue(); $component->set('blockDate', today()->format('Y-m-d')) ->set('blockStartTime', '09:00') ->set('blockEndTime', '10:00') ->call('blockTimeSlot'); expect($component->get('showBlockModal'))->toBeFalse(); }); // =========================================== // Notification Bell Tests // =========================================== test('notification bell shows pending count in header', function () { Consultation::factory()->count(5)->pending()->create(); $this->actingAs($this->admin) ->get(route('admin.dashboard')) ->assertSee('5'); }); test('notification bell not shown when no pending items', function () { $response = $this->actingAs($this->admin) ->get(route('admin.dashboard')); // Should still succeed, badge just won't be visible $response->assertSuccessful(); }); // =========================================== // Access Control Tests // =========================================== test('non-admin cannot access dashboard widgets', function () { $client = User::factory()->individual()->create(); $this->actingAs($client) ->get(route('admin.dashboard')) ->assertForbidden(); }); // =========================================== // Widget Integration Tests // =========================================== test('dashboard displays all widgets', function () { $this->actingAs($this->admin) ->get(route('admin.dashboard')) ->assertSuccessful() ->assertSee(__('widgets.quick_actions')) ->assertSee(__('widgets.pending_bookings')) ->assertSee(__('widgets.todays_schedule')) ->assertSee(__('widgets.recent_timeline_updates')); }); test('widgets handle concurrent data gracefully', function () { // Create mixed data $client = User::factory()->individual()->create(); $timeline = Timeline::factory()->create(['user_id' => $client->id]); Consultation::factory()->count(3)->pending()->create(['user_id' => $client->id]); Consultation::factory()->count(2)->approved()->create([ 'user_id' => $client->id, 'booking_date' => today(), ]); TimelineUpdate::factory()->count(3)->create(['timeline_id' => $timeline->id]); $this->actingAs($this->admin) ->get(route('admin.dashboard')) ->assertSuccessful(); });