libra/tests/Feature/Admin/QuickActionsPanelTest.php

434 lines
14 KiB
PHP

<?php
use App\Enums\ConsultationStatus;
use App\Models\BlockedTime;
use App\Models\Consultation;
use App\Models\Timeline;
use App\Models\TimelineUpdate;
use App\Models\User;
use Livewire\Volt\Volt;
beforeEach(function () {
$this->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();
});