complete story 3.2 with qa tests

This commit is contained in:
Naser Mansour
2025-12-26 18:43:26 +02:00
parent e679a45933
commit 43df24c7cd
18 changed files with 1483 additions and 74 deletions
+518
View File
@@ -0,0 +1,518 @@
<?php
use App\Enums\ConsultationStatus;
use App\Models\AdminLog;
use App\Models\BlockedTime;
use App\Models\Consultation;
use App\Models\User;
use Livewire\Volt\Volt;
beforeEach(function () {
$this->admin = User::factory()->admin()->create();
});
// ===========================================
// Access Tests
// ===========================================
test('admin can access blocked times page', function () {
$this->actingAs($this->admin)
->get(route('admin.settings.blocked-times'))
->assertOk();
});
test('non-admin cannot access blocked times page', function () {
$client = User::factory()->individual()->create();
$this->actingAs($client)
->get(route('admin.settings.blocked-times'))
->assertForbidden();
});
test('unauthenticated user cannot access blocked times page', function () {
$this->get(route('admin.settings.blocked-times'))
->assertRedirect(route('login'));
});
// ===========================================
// Create All-Day Block Tests
// ===========================================
test('admin can create an all-day block', function () {
$this->actingAs($this->admin);
$blockDate = today()->addDays(5)->format('Y-m-d');
$initialCount = BlockedTime::count();
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', $blockDate)
->set('is_all_day', true)
->set('reason', 'Holiday')
->call('save')
->assertHasNoErrors();
expect(BlockedTime::count())->toBe($initialCount + 1);
$blocked = BlockedTime::latest()->first();
expect($blocked)->not->toBeNull()
->and($blocked->isAllDay())->toBeTrue()
->and($blocked->reason)->toBe('Holiday');
});
// ===========================================
// Create Time-Range Block Tests
// ===========================================
test('admin can create a time-range block', function () {
$this->actingAs($this->admin);
$initialCount = BlockedTime::count();
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', today()->addDays(3)->format('Y-m-d'))
->set('is_all_day', false)
->set('start_time', '09:00')
->set('end_time', '12:00')
->set('reason', 'Morning meeting')
->call('save')
->assertHasNoErrors();
expect(BlockedTime::count())->toBe($initialCount + 1);
$blocked = BlockedTime::latest()->first();
expect($blocked)->not->toBeNull()
->and($blocked->isAllDay())->toBeFalse()
->and($blocked->start_time)->toContain('09:00')
->and($blocked->end_time)->toContain('12:00')
->and($blocked->reason)->toBe('Morning meeting');
});
test('admin can create block without reason', function () {
$this->actingAs($this->admin);
$initialCount = BlockedTime::count();
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', today()->addDays(2)->format('Y-m-d'))
->set('is_all_day', true)
->set('reason', '')
->call('save')
->assertHasNoErrors();
expect(BlockedTime::count())->toBe($initialCount + 1);
$blocked = BlockedTime::latest()->first();
expect($blocked)->not->toBeNull()
->and($blocked->reason)->toBeNull();
});
// ===========================================
// Edit Blocked Time Tests
// ===========================================
test('admin can edit an existing blocked time', function () {
$blocked = BlockedTime::factory()->allDay()->create([
'block_date' => today()->addDays(5),
'reason' => 'Original reason',
]);
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('openEditModal', $blocked->id)
->set('reason', 'Updated reason')
->call('save')
->assertHasNoErrors();
$blocked->refresh();
expect($blocked->reason)->toBe('Updated reason');
});
test('admin can change block from all-day to time-range', function () {
$blocked = BlockedTime::factory()->allDay()->create([
'block_date' => today()->addDays(5),
]);
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('openEditModal', $blocked->id)
->set('is_all_day', false)
->set('start_time', '14:00')
->set('end_time', '18:00')
->call('save')
->assertHasNoErrors();
$blocked->refresh();
expect($blocked->isAllDay())->toBeFalse()
->and($blocked->start_time)->toContain('14:00')
->and($blocked->end_time)->toContain('18:00');
});
test('admin can edit past blocked time date', function () {
$blocked = BlockedTime::factory()->past()->create();
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->call('openEditModal', $blocked->id)
->set('reason', 'Updated past block')
->call('save')
->assertHasNoErrors();
$blocked->refresh();
expect($blocked->reason)->toBe('Updated past block');
});
// ===========================================
// Delete Blocked Time Tests
// ===========================================
test('admin can delete a blocked time', function () {
$blocked = BlockedTime::factory()->create();
$blockedId = $blocked->id;
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('confirmDelete', $blockedId)
->call('delete')
->assertHasNoErrors();
expect(BlockedTime::find($blockedId))->toBeNull();
});
// ===========================================
// Validation Tests
// ===========================================
test('cannot create block with end time before start time', function () {
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', today()->addDays(5)->format('Y-m-d'))
->set('is_all_day', false)
->set('start_time', '14:00')
->set('end_time', '10:00')
->call('save')
->assertHasErrors(['end_time']);
});
test('cannot create block for past dates', function () {
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', today()->subDays(5)->format('Y-m-d'))
->set('is_all_day', true)
->call('save')
->assertHasErrors(['block_date']);
});
test('can create block for today', function () {
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', today()->format('Y-m-d'))
->set('is_all_day', true)
->call('save')
->assertHasNoErrors();
expect(BlockedTime::where('block_date', today())->exists())->toBeTrue();
});
test('reason field respects 255 character max length', function () {
$this->actingAs($this->admin);
$longReason = str_repeat('a', 256);
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', today()->addDays(5)->format('Y-m-d'))
->set('is_all_day', true)
->set('reason', $longReason)
->call('save')
->assertHasErrors(['reason']);
});
// ===========================================
// List View Tests
// ===========================================
test('list displays all blocked times sorted by date', function () {
BlockedTime::factory()->create(['block_date' => today()->addDays(10)]);
BlockedTime::factory()->create(['block_date' => today()->addDays(2)]);
BlockedTime::factory()->create(['block_date' => today()->addDays(5)]);
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->set('filter', 'all');
$blockedTimes = $component->viewData('blockedTimes');
expect($blockedTimes)->toHaveCount(3);
});
test('filter by upcoming shows only future blocks', function () {
BlockedTime::factory()->upcoming()->create();
BlockedTime::factory()->today()->create();
BlockedTime::factory()->past()->create();
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->set('filter', 'upcoming');
$blockedTimes = $component->viewData('blockedTimes');
expect($blockedTimes)->toHaveCount(2)
->and($blockedTimes->every(fn ($b) => $b->block_date->gte(today())))->toBeTrue();
});
test('filter by past shows only past blocks', function () {
BlockedTime::factory()->upcoming()->create();
BlockedTime::factory()->today()->create();
BlockedTime::factory()->past()->create();
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->set('filter', 'past');
$blockedTimes = $component->viewData('blockedTimes');
expect($blockedTimes)->toHaveCount(1)
->and($blockedTimes->every(fn ($b) => $b->block_date->lt(today())))->toBeTrue();
});
test('filter by all shows all blocks', function () {
BlockedTime::factory()->upcoming()->create();
BlockedTime::factory()->today()->create();
BlockedTime::factory()->past()->create();
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->set('filter', 'all');
$blockedTimes = $component->viewData('blockedTimes');
expect($blockedTimes)->toHaveCount(3);
});
test('list shows reason if provided', function () {
BlockedTime::factory()->create([
'block_date' => today()->addDays(5),
'reason' => 'Personal vacation',
]);
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->assertSee('Personal vacation');
});
// ===========================================
// Audit Log Tests
// ===========================================
test('audit log is created when blocked time is created', function () {
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', today()->addDays(5)->format('Y-m-d'))
->set('is_all_day', true)
->set('reason', 'Test')
->call('save');
$log = AdminLog::where('target_type', 'blocked_time')->first();
expect($log)->not->toBeNull()
->and($log->admin_id)->toBe($this->admin->id)
->and($log->action)->toBe('create');
});
test('audit log is created when blocked time is updated', function () {
$blocked = BlockedTime::factory()->create([
'block_date' => today()->addDays(5),
]);
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('openEditModal', $blocked->id)
->set('reason', 'Updated')
->call('save');
$log = AdminLog::where('target_type', 'blocked_time')
->where('action', 'update')
->first();
expect($log)->not->toBeNull()
->and($log->target_id)->toBe($blocked->id);
});
test('audit log is created when blocked time is deleted', function () {
$blocked = BlockedTime::factory()->create();
$blockedId = $blocked->id;
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('confirmDelete', $blockedId)
->call('delete');
$log = AdminLog::where('target_type', 'blocked_time')
->where('action', 'delete')
->first();
expect($log)->not->toBeNull()
->and($log->target_id)->toBe($blockedId);
});
// ===========================================
// Pending Booking Warning Tests
// ===========================================
test('warning displayed when blocking date with pending consultations', function () {
$client = User::factory()->individual()->create();
$blockDate = today()->addDays(5);
Consultation::factory()->create([
'user_id' => $client->id,
'booking_date' => $blockDate,
'booking_time' => '10:00:00',
'status' => ConsultationStatus::Pending,
]);
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', $blockDate->format('Y-m-d'))
->set('is_all_day', true)
->call('checkPendingBookings');
expect($component->get('pendingBookings'))->toHaveCount(1)
->and($component->get('pendingBookings')[0]['client'])->toBe($client->full_name);
});
test('warning shows pending bookings within time range', function () {
$client = User::factory()->individual()->create();
$blockDate = today()->addDays(5);
// Consultation within blocked range (10:00 is between 09:00 and 12:00)
Consultation::factory()->create([
'user_id' => $client->id,
'booking_date' => $blockDate,
'booking_time' => '10:00:00',
'status' => ConsultationStatus::Pending,
]);
// Consultation outside blocked range (15:00 is not between 09:00 and 12:00)
Consultation::factory()->create([
'user_id' => $client->id,
'booking_date' => $blockDate,
'booking_time' => '15:00:00',
'status' => ConsultationStatus::Pending,
]);
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', $blockDate->format('Y-m-d'))
->set('is_all_day', false)
->set('start_time', '09:00')
->set('end_time', '12:00')
->call('checkPendingBookings');
expect($component->get('pendingBookings'))->toHaveCount(1);
});
// ===========================================
// Modal Tests
// ===========================================
test('create modal opens with default values', function () {
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->call('openCreateModal');
expect($component->get('showModal'))->toBeTrue()
->and($component->get('editingId'))->toBeNull()
->and($component->get('is_all_day'))->toBeTrue()
->and($component->get('block_date'))->toBe(today()->format('Y-m-d'));
});
test('edit modal opens with existing values', function () {
$blocked = BlockedTime::factory()->timeRange('10:00', '14:00')->create([
'block_date' => today()->addDays(5),
'reason' => 'Test reason',
]);
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->call('openEditModal', $blocked->id);
expect($component->get('showModal'))->toBeTrue()
->and($component->get('editingId'))->toBe($blocked->id)
->and($component->get('is_all_day'))->toBeFalse()
->and($component->get('start_time'))->toBe('10:00')
->and($component->get('end_time'))->toBe('14:00')
->and($component->get('reason'))->toBe('Test reason');
});
test('modal closes and resets on close', function () {
$blocked = BlockedTime::factory()->create();
$this->actingAs($this->admin);
$component = Volt::test('admin.settings.blocked-times')
->call('openEditModal', $blocked->id)
->call('closeModal');
expect($component->get('showModal'))->toBeFalse()
->and($component->get('editingId'))->toBeNull();
});
// ===========================================
// Multiple Blocks on Same Date Tests
// ===========================================
test('multiple time-range blocks on same date handled correctly', function () {
$blockDate = today()->addDays(5);
BlockedTime::factory()->timeRange('09:00', '12:00')->create([
'block_date' => $blockDate,
]);
$this->actingAs($this->admin);
Volt::test('admin.settings.blocked-times')
->call('openCreateModal')
->set('block_date', $blockDate->format('Y-m-d'))
->set('is_all_day', false)
->set('start_time', '14:00')
->set('end_time', '17:00')
->call('save')
->assertHasNoErrors();
expect(BlockedTime::where('block_date', $blockDate)->count())->toBe(2);
});
+145
View File
@@ -0,0 +1,145 @@
<?php
use App\Models\BlockedTime;
test('isAllDay returns true when start_time and end_time are null', function () {
$blocked = BlockedTime::factory()->allDay()->create();
expect($blocked->isAllDay())->toBeTrue();
});
test('isAllDay returns false when times are set', function () {
$blocked = BlockedTime::factory()->timeRange('09:00', '12:00')->create();
expect($blocked->isAllDay())->toBeFalse();
});
test('scopeUpcoming filters to today and future dates', function () {
BlockedTime::factory()->create(['block_date' => today()]);
BlockedTime::factory()->create(['block_date' => today()->addDays(5)]);
BlockedTime::factory()->create(['block_date' => today()->subDays(5)]);
$upcoming = BlockedTime::upcoming()->get();
expect($upcoming)->toHaveCount(2)
->and($upcoming->pluck('block_date')->every(fn ($date) => $date->gte(today())))->toBeTrue();
});
test('scopePast filters to past dates only', function () {
BlockedTime::factory()->create(['block_date' => today()]);
BlockedTime::factory()->create(['block_date' => today()->addDays(5)]);
BlockedTime::factory()->create(['block_date' => today()->subDays(5)]);
$past = BlockedTime::past()->get();
expect($past)->toHaveCount(1)
->and($past->first()->block_date->lt(today()))->toBeTrue();
});
test('scopeForDate filters by exact date', function () {
$targetDate = today()->addDays(3);
BlockedTime::factory()->create(['block_date' => $targetDate]);
BlockedTime::factory()->create(['block_date' => today()]);
BlockedTime::factory()->create(['block_date' => today()->addDays(10)]);
$result = BlockedTime::forDate($targetDate)->get();
expect($result)->toHaveCount(1)
->and($result->first()->block_date->toDateString())->toBe($targetDate->toDateString());
});
test('blocksSlot returns true for all times when all-day block', function () {
$blocked = BlockedTime::factory()->allDay()->create();
expect($blocked->blocksSlot('09:00'))->toBeTrue()
->and($blocked->blocksSlot('12:00'))->toBeTrue()
->and($blocked->blocksSlot('17:00'))->toBeTrue()
->and($blocked->blocksSlot('23:59'))->toBeTrue();
});
test('blocksSlot returns true for times within blocked range', function () {
$blocked = BlockedTime::factory()->timeRange('09:00', '12:00')->create();
expect($blocked->blocksSlot('09:00'))->toBeTrue()
->and($blocked->blocksSlot('10:00'))->toBeTrue()
->and($blocked->blocksSlot('11:00'))->toBeTrue()
->and($blocked->blocksSlot('11:30'))->toBeTrue();
});
test('blocksSlot returns false for times outside blocked range', function () {
$blocked = BlockedTime::factory()->timeRange('09:00', '12:00')->create();
expect($blocked->blocksSlot('08:00'))->toBeFalse()
->and($blocked->blocksSlot('08:59'))->toBeFalse()
->and($blocked->blocksSlot('12:00'))->toBeFalse()
->and($blocked->blocksSlot('13:00'))->toBeFalse();
});
test('blocksSlot returns true at start time boundary', function () {
$blocked = BlockedTime::factory()->timeRange('09:00', '12:00')->create();
expect($blocked->blocksSlot('09:00'))->toBeTrue();
});
test('blocksSlot returns false at end time boundary', function () {
$blocked = BlockedTime::factory()->timeRange('09:00', '12:00')->create();
expect($blocked->blocksSlot('12:00'))->toBeFalse();
});
test('block_date is cast to date', function () {
$blocked = BlockedTime::factory()->create([
'block_date' => '2025-12-26',
]);
expect($blocked->block_date)->toBeInstanceOf(Carbon\Carbon::class);
});
test('reason can be null', function () {
$blocked = BlockedTime::factory()->create([
'reason' => null,
]);
expect($blocked->reason)->toBeNull();
});
test('reason can be set', function () {
$blocked = BlockedTime::factory()->withReason('Holiday vacation')->create();
expect($blocked->reason)->toBe('Holiday vacation');
});
test('factory allDay state creates full day block', function () {
$blocked = BlockedTime::factory()->allDay()->create();
expect($blocked->start_time)->toBeNull()
->and($blocked->end_time)->toBeNull()
->and($blocked->isAllDay())->toBeTrue();
});
test('factory timeRange state creates partial day block', function () {
$blocked = BlockedTime::factory()->timeRange('10:00', '14:00')->create();
expect($blocked->start_time)->toContain('10:00')
->and($blocked->end_time)->toContain('14:00')
->and($blocked->isAllDay())->toBeFalse();
});
test('factory upcoming state creates future date', function () {
$blocked = BlockedTime::factory()->upcoming()->create();
expect($blocked->block_date->gt(today()))->toBeTrue();
});
test('factory past state creates past date', function () {
$blocked = BlockedTime::factory()->past()->create();
expect($blocked->block_date->lt(today()))->toBeTrue();
});
test('factory today state creates today date', function () {
$blocked = BlockedTime::factory()->today()->create();
expect($blocked->block_date->toDateString())->toBe(today()->toDateString());
});