434 lines
14 KiB
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();
|
|
});
|