complete story 3.3 with qa tests
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
use App\Models\BlockedTime;
|
||||
use App\Models\Consultation;
|
||||
use App\Models\WorkingHour;
|
||||
use Carbon\Carbon;
|
||||
use Livewire\Volt\Volt;
|
||||
|
||||
beforeEach(function () {
|
||||
// Setup working hours for weekdays (Sunday-Thursday)
|
||||
foreach ([0, 1, 2, 3, 4] as $day) {
|
||||
WorkingHour::factory()->create([
|
||||
'day_of_week' => $day,
|
||||
'start_time' => '09:00',
|
||||
'end_time' => '17:00',
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
it('displays current month by default', function () {
|
||||
$currentMonth = now()->translatedFormat('F Y');
|
||||
|
||||
Volt::test('availability-calendar')
|
||||
->assertSee($currentMonth);
|
||||
});
|
||||
|
||||
it('navigates to next month', function () {
|
||||
$nextMonth = now()->addMonth()->translatedFormat('F Y');
|
||||
|
||||
Volt::test('availability-calendar')
|
||||
->call('nextMonth')
|
||||
->assertSee($nextMonth);
|
||||
});
|
||||
|
||||
it('prevents navigating to past months', function () {
|
||||
Volt::test('availability-calendar')
|
||||
->assertSet('year', now()->year)
|
||||
->assertSet('month', now()->month)
|
||||
->call('previousMonth')
|
||||
->assertSet('year', now()->year)
|
||||
->assertSet('month', now()->month);
|
||||
});
|
||||
|
||||
it('handles year rollover when navigating months', function () {
|
||||
Carbon::setTestNow(Carbon::create(2024, 12, 15));
|
||||
|
||||
Volt::test('availability-calendar')
|
||||
->assertSet('year', 2024)
|
||||
->assertSet('month', 12)
|
||||
->call('nextMonth')
|
||||
->assertSet('year', 2025)
|
||||
->assertSet('month', 1);
|
||||
|
||||
Carbon::setTestNow();
|
||||
});
|
||||
|
||||
it('loads available slots when date is selected', function () {
|
||||
$sunday = Carbon::now()->next(Carbon::SUNDAY)->format('Y-m-d');
|
||||
|
||||
Volt::test('availability-calendar')
|
||||
->call('selectDate', $sunday)
|
||||
->assertSet('selectedDate', $sunday)
|
||||
->assertNotSet('availableSlots', []);
|
||||
});
|
||||
|
||||
it('does not select unavailable dates', function () {
|
||||
// Friday is a non-working day in our setup
|
||||
$friday = Carbon::now()->next(Carbon::FRIDAY)->format('Y-m-d');
|
||||
|
||||
Volt::test('availability-calendar')
|
||||
->call('selectDate', $friday)
|
||||
->assertSet('selectedDate', null)
|
||||
->assertSet('availableSlots', []);
|
||||
});
|
||||
|
||||
it('clears selection when navigating months', function () {
|
||||
$sunday = Carbon::now()->next(Carbon::SUNDAY)->format('Y-m-d');
|
||||
|
||||
Volt::test('availability-calendar')
|
||||
->call('selectDate', $sunday)
|
||||
->assertSet('selectedDate', $sunday)
|
||||
->call('nextMonth')
|
||||
->assertSet('selectedDate', null)
|
||||
->assertSet('availableSlots', []);
|
||||
});
|
||||
|
||||
it('displays RTL layout for Arabic locale', function () {
|
||||
app()->setLocale('ar');
|
||||
|
||||
Volt::test('availability-calendar')
|
||||
->assertSeeHtml('dir="rtl"');
|
||||
});
|
||||
|
||||
it('does not select blocked dates', function () {
|
||||
$sunday = Carbon::now()->next(Carbon::SUNDAY);
|
||||
BlockedTime::factory()->allDay()->create([
|
||||
'block_date' => $sunday->toDateString(),
|
||||
]);
|
||||
|
||||
Volt::test('availability-calendar')
|
||||
->call('selectDate', $sunday->toDateString())
|
||||
->assertSet('selectedDate', null);
|
||||
});
|
||||
|
||||
it('does not select fully booked dates', function () {
|
||||
$sunday = Carbon::now()->next(Carbon::SUNDAY);
|
||||
$slots = ['09:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00'];
|
||||
|
||||
foreach ($slots as $slot) {
|
||||
Consultation::factory()->approved()->create([
|
||||
'booking_date' => $sunday->toDateString(),
|
||||
'booking_time' => $slot,
|
||||
]);
|
||||
}
|
||||
|
||||
Volt::test('availability-calendar')
|
||||
->call('selectDate', $sunday->toDateString())
|
||||
->assertSet('selectedDate', null);
|
||||
});
|
||||
|
||||
it('shows available legend items', function () {
|
||||
Volt::test('availability-calendar')
|
||||
->assertSee(__('booking.available'))
|
||||
->assertSee(__('booking.partial'))
|
||||
->assertSee(__('booking.unavailable'));
|
||||
});
|
||||
|
||||
it('allows selecting partially booked dates', function () {
|
||||
$sunday = Carbon::now()->next(Carbon::SUNDAY);
|
||||
Consultation::factory()->approved()->create([
|
||||
'booking_date' => $sunday->toDateString(),
|
||||
'booking_time' => '10:00',
|
||||
]);
|
||||
|
||||
Volt::test('availability-calendar')
|
||||
->call('selectDate', $sunday->toDateString())
|
||||
->assertSet('selectedDate', $sunday->toDateString())
|
||||
->assertNotSet('availableSlots', []);
|
||||
});
|
||||
@@ -19,6 +19,10 @@ pest()->extend(Tests\TestCase::class)
|
||||
->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
|
||||
->in('Unit/Models');
|
||||
|
||||
pest()->extend(Tests\TestCase::class)
|
||||
->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
|
||||
->in('Unit/Services');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expectations
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\ConsultationStatus;
|
||||
use App\Models\BlockedTime;
|
||||
use App\Models\Consultation;
|
||||
use App\Models\WorkingHour;
|
||||
use App\Services\AvailabilityService;
|
||||
use Carbon\Carbon;
|
||||
|
||||
beforeEach(function () {
|
||||
// Setup default working hours (Sunday-Thursday, 9am-5pm)
|
||||
foreach ([0, 1, 2, 3, 4] as $day) {
|
||||
WorkingHour::factory()->create([
|
||||
'day_of_week' => $day,
|
||||
'start_time' => '09:00',
|
||||
'end_time' => '17:00',
|
||||
'is_active' => true,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
describe('getDateStatus', function () {
|
||||
it('returns "past" for yesterday', function () {
|
||||
$service = new AvailabilityService;
|
||||
$yesterday = Carbon::yesterday();
|
||||
|
||||
expect($service->getDateStatus($yesterday))->toBe('past');
|
||||
});
|
||||
|
||||
it('returns "closed" for non-working days (weekends)', function () {
|
||||
$service = new AvailabilityService;
|
||||
// Friday is day 5, Saturday is day 6 - both are weekends in our setup
|
||||
$friday = Carbon::now()->next(Carbon::FRIDAY);
|
||||
|
||||
expect($service->getDateStatus($friday))->toBe('closed');
|
||||
});
|
||||
|
||||
it('returns "blocked" for fully blocked date', function () {
|
||||
$sunday = Carbon::now()->next(Carbon::SUNDAY);
|
||||
BlockedTime::factory()->create([
|
||||
'block_date' => $sunday->toDateString(),
|
||||
'start_time' => null,
|
||||
'end_time' => null,
|
||||
]);
|
||||
|
||||
$service = new AvailabilityService;
|
||||
|
||||
expect($service->getDateStatus($sunday))->toBe('blocked');
|
||||
});
|
||||
|
||||
it('returns "full" when all slots are booked', function () {
|
||||
$sunday = Carbon::now()->next(Carbon::SUNDAY);
|
||||
$slots = ['09:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00'];
|
||||
|
||||
foreach ($slots as $slot) {
|
||||
Consultation::factory()->approved()->create([
|
||||
'booking_date' => $sunday->toDateString(),
|
||||
'booking_time' => $slot,
|
||||
]);
|
||||
}
|
||||
|
||||
$service = new AvailabilityService;
|
||||
|
||||
expect($service->getDateStatus($sunday))->toBe('full');
|
||||
});
|
||||
|
||||
it('returns "partial" when some slots are booked', function () {
|
||||
$sunday = Carbon::now()->next(Carbon::SUNDAY);
|
||||
Consultation::factory()->approved()->create([
|
||||
'booking_date' => $sunday->toDateString(),
|
||||
'booking_time' => '10:00',
|
||||
]);
|
||||
|
||||
$service = new AvailabilityService;
|
||||
|
||||
expect($service->getDateStatus($sunday))->toBe('partial');
|
||||
});
|
||||
|
||||
it('returns "available" for working day with no bookings', function () {
|
||||
$sunday = Carbon::now()->next(Carbon::SUNDAY);
|
||||
$service = new AvailabilityService;
|
||||
|
||||
expect($service->getDateStatus($sunday))->toBe('available');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAvailableSlots', function () {
|
||||
it('returns empty array for non-working days', function () {
|
||||
$service = new AvailabilityService;
|
||||
// Friday is day 5 - a non-working day in our setup
|
||||
$friday = Carbon::now()->next(Carbon::FRIDAY);
|
||||
|
||||
expect($service->getAvailableSlots($friday))->toBe([]);
|
||||
});
|
||||
|
||||
it('excludes booked consultation times', function () {
|
||||
$sunday = Carbon::now()->next(Carbon::SUNDAY);
|
||||
Consultation::factory()->pending()->create([
|
||||
'booking_date' => $sunday->toDateString(),
|
||||
'booking_time' => '10:00',
|
||||
]);
|
||||
|
||||
$service = new AvailabilityService;
|
||||
$slots = $service->getAvailableSlots($sunday);
|
||||
|
||||
expect($slots)->not->toContain('10:00');
|
||||
});
|
||||
|
||||
it('excludes blocked time ranges', function () {
|
||||
$sunday = Carbon::now()->next(Carbon::SUNDAY);
|
||||
BlockedTime::factory()->timeRange('14:00', '16:00')->create([
|
||||
'block_date' => $sunday->toDateString(),
|
||||
]);
|
||||
|
||||
$service = new AvailabilityService;
|
||||
$slots = $service->getAvailableSlots($sunday);
|
||||
|
||||
expect($slots)->not->toContain('14:00');
|
||||
expect($slots)->not->toContain('15:00');
|
||||
});
|
||||
|
||||
it('includes pending and approved consultations as booked', function () {
|
||||
$sunday = Carbon::now()->next(Carbon::SUNDAY);
|
||||
|
||||
Consultation::factory()->pending()->create([
|
||||
'booking_date' => $sunday->toDateString(),
|
||||
'booking_time' => '09:00',
|
||||
]);
|
||||
|
||||
Consultation::factory()->approved()->create([
|
||||
'booking_date' => $sunday->toDateString(),
|
||||
'booking_time' => '10:00',
|
||||
]);
|
||||
|
||||
// Cancelled should NOT block the slot
|
||||
Consultation::factory()->create([
|
||||
'booking_date' => $sunday->toDateString(),
|
||||
'booking_time' => '11:00',
|
||||
'status' => ConsultationStatus::Cancelled,
|
||||
]);
|
||||
|
||||
$service = new AvailabilityService;
|
||||
$slots = $service->getAvailableSlots($sunday);
|
||||
|
||||
expect($slots)->not->toContain('09:00');
|
||||
expect($slots)->not->toContain('10:00');
|
||||
expect($slots)->toContain('11:00');
|
||||
});
|
||||
|
||||
it('returns all slots blocked when full day is blocked', function () {
|
||||
$sunday = Carbon::now()->next(Carbon::SUNDAY);
|
||||
BlockedTime::factory()->allDay()->create([
|
||||
'block_date' => $sunday->toDateString(),
|
||||
]);
|
||||
|
||||
$service = new AvailabilityService;
|
||||
$slots = $service->getAvailableSlots($sunday);
|
||||
|
||||
expect($slots)->toBe([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMonthAvailability', function () {
|
||||
it('returns status for every day in the month', function () {
|
||||
$service = new AvailabilityService;
|
||||
$availability = $service->getMonthAvailability(2025, 1);
|
||||
|
||||
expect($availability)->toHaveCount(31);
|
||||
});
|
||||
|
||||
it('handles year rollover correctly (December to January)', function () {
|
||||
$service = new AvailabilityService;
|
||||
|
||||
$december = $service->getMonthAvailability(2024, 12);
|
||||
$january = $service->getMonthAvailability(2025, 1);
|
||||
|
||||
expect($december)->toHaveKey('2024-12-31');
|
||||
expect($january)->toHaveKey('2025-01-01');
|
||||
});
|
||||
|
||||
it('returns correct status types', function () {
|
||||
$service = new AvailabilityService;
|
||||
$availability = $service->getMonthAvailability(now()->year, now()->month);
|
||||
|
||||
$validStatuses = ['past', 'closed', 'blocked', 'full', 'partial', 'available'];
|
||||
|
||||
foreach ($availability as $status) {
|
||||
expect($validStatuses)->toContain($status);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user