24-6: booking page and booking success — implemented, QA Review

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Naser Mansour
2026-10-31 18:51:15 +02:00
parent b1ac864573
commit 7637294593
11 changed files with 238 additions and 192 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
--- ---
id: 24-6 id: 24-6
title: 24-6 Booking Page and Booking Success title: 24-6 Booking Page and Booking Success
status: Ready for Dev status: QA Review
date: 2026-10-31 date: 2026-10-31
parent: 24 parent: 24
--- ---
+16
View File
@@ -0,0 +1,16 @@
# Manual tests — 24-6 Booking Page and Booking Success
Automated (passed 2026-10-31, `tests/Feature/Design/BookingPageRedesignTest.php`, 7 tests): `/booking` shows section head, forest card
(contact, static balance), month calendar inside `.book-r`; slot selection reveals fields and the full flow books exactly once and
redirects; every validation error renders under its field; logged-in client sees the message and no form; calendar days carry the
hook classes with `wire:click` intact; CSS scoped; `/booking/success` shows balance, title, message and Home with no `data-flux-`
output in `<main>`. Whole Design test directory (70 tests), `tests/Feature/Client` and `GuestBookingTest` run (1 pre-existing
stale-copy failure there). UI loop: no horizontal scroll at 390/768/1440 on both pages.
Flagged, not changed (spec): `/booking` has no `<h1>` (the section-head component, per spec, renders the title as an `<h2>`); UI loop reports h1Count 0 there. One added key `site.captcha_refresh` (aria-label of the captcha refresh button, which previously had none).
- [ ] `/booking` as a guest: forest card + month calendar; today (or any open day) has a gold dot, past days are dim, picking a day then a time reveals the form.
- [ ] Complete a booking through this page (confirm step, Confirm): arrives at `/booking/success`; cancel the test booking afterwards.
- [ ] Wrong captcha / short summary / missing field: each error appears under its field.
- [ ] Logged in as a client: `/booking` shows the logged-in message and link, no form; `/client/consultations/book` calendar looks as before (not restyled).
- [ ] `/booking/success`: balance, title, message, Home button; 390px no sideways scroll.
+1 -1
View File
@@ -142,7 +142,7 @@
| 24-3 | 24-3 Signature Balance Component | QA Review | | 24-3 | 24-3 Signature Balance Component | QA Review |
| 24-4 | 24-4 Home Page Sections (Hero to Latest Articles) | QA Review | | 24-4 | 24-4 Home Page Sections (Hero to Latest Articles) | QA Review |
| 24-5 | 24-5 Home Booking Card (Compact Booking and Day Strip) | QA Review | | 24-5 | 24-5 Home Booking Card (Compact Booking and Day Strip) | QA Review |
| 24-6 | 24-6 Booking Page and Booking Success | Ready for Dev | | 24-6 | 24-6 Booking Page and Booking Success | QA Review |
| 24-7 | 24-7 About Page | Ready for Dev | | 24-7 | 24-7 About Page | Ready for Dev |
| 24-8 | 24-8 Articles and Legal Pages | Ready for Dev | | 24-8 | 24-8 Articles and Legal Pages | Ready for Dev |
| 24-9 | 24-9 Auth Pages Type and Controls | Ready for Dev | | 24-9 | 24-9 Auth Pages Type and Controls | Ready for Dev |
+1
View File
@@ -71,4 +71,5 @@ return [
'booking_time' => 'الوقت', 'booking_time' => 'الوقت',
'booking_submit' => 'اطلب هذا الموعد', 'booking_submit' => 'اطلب هذا الموعد',
'booking_none' => 'لا توجد مواعيد متاحة حالياً', 'booking_none' => 'لا توجد مواعيد متاحة حالياً',
'captcha_refresh' => 'سؤال جديد',
]; ];
+1
View File
@@ -71,4 +71,5 @@ return [
'booking_time' => 'Time', 'booking_time' => 'Time',
'booking_submit' => 'Request this appointment', 'booking_submit' => 'Request this appointment',
'booking_none' => 'No times are available at the moment', 'booking_none' => 'No times are available at the moment',
'captcha_refresh' => 'New question',
]; ];
+18
View File
@@ -314,6 +314,24 @@ body.site .book-r .btn+.btn{margin-top:10px}
body.site .f2{grid-template-columns:1fr} body.site .f2{grid-template-columns:1fr}
} }
/* ---------- Booking page and success (24-6) ---------- */
body.site .book-r .picked{display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;padding:14px 18px;margin-bottom:18px;border-radius:14px;background:#fff;border:1px solid var(--line)}
body.site .book-r .picked b{display:block;font-family:var(--display);font-size:1.15rem}
body.site .book-r .picked span{display:block;font-size:.92rem;color:var(--muted)}
body.site .book-r .picked .btn{width:auto;margin:0;min-height:44px;padding:8px 18px}
body.site .book-r .cap-row{display:flex;gap:10px;align-items:center}
body.site .book-r .cap-row .refresh{width:auto;margin:0;min-height:48px;padding:0 16px}
body.site .book-r .site-cal-nav{min-width:44px;min-height:44px;border-radius:999px;border:1.5px solid var(--line);background:#fff;color:var(--ink)}
body.site .book-r .site-cal-day{min-width:44px;min-height:44px;border-radius:10px;position:relative;font-weight:500}
body.site .book-r .site-cal-day.is-available{background:#fff;color:var(--ink);box-shadow:inset 0 0 0 1px var(--line)}
body.site .book-r .site-cal-day.is-available::after{content:"";position:absolute;inset-inline:0;bottom:4px;margin-inline:auto;width:6px;height:6px;border-radius:50%;background:var(--gold)}
body.site .book-r .site-cal-day.is-available:hover{box-shadow:inset 0 0 0 1.5px var(--gold)}
body.site .book-r .site-cal-day.is-selected{background:var(--forest);color:var(--cream);box-shadow:none}
body.site .book-r .site-cal-day.is-unavailable{background:transparent;color:var(--muted);opacity:.5;cursor:default}
body.site .success-card{max-width:560px;margin-inline:auto;background:var(--cream);border:1px solid var(--line);border-radius:20px;box-shadow:var(--shadow-2);padding:48px 40px;text-align:center}
body.site .success-card h1{font-size:clamp(2rem,3.4vw,2.6rem);margin-bottom:12px}
body.site .success-card p{color:var(--muted);margin:0 0 26px}
/* ---------- Reveal (hidden only when JavaScript runs) ---------- */ /* ---------- Reveal (hidden only when JavaScript runs) ---------- */
body.site.js [data-reveal]{opacity:0;transform:translateY(28px);transition:opacity .9s var(--ease),transform .9s var(--ease)} body.site.js [data-reveal]{opacity:0;transform:translateY(28px);transition:opacity .9s var(--ease),transform .9s var(--ease)}
body.site.js [data-reveal].in{opacity:1;transform:none} body.site.js [data-reveal].in{opacity:1;transform:none}
@@ -239,13 +239,13 @@ new class extends Component
@else @else
<!-- Calendar Header --> <!-- Calendar Header -->
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<flux:button size="sm" wire:click="previousMonth" variant="outline"> <flux:button size="sm" wire:click="previousMonth" variant="outline" class="site-cal-nav">
<flux:icon name="chevron-{{ app()->getLocale() === 'ar' ? 'right' : 'left' }}" class="size-5" /> <flux:icon name="chevron-{{ app()->getLocale() === 'ar' ? 'right' : 'left' }}" class="size-5" />
</flux:button> </flux:button>
<flux:heading size="lg" class="text-body">{{ $monthName }}</flux:heading> <flux:heading size="lg" class="text-body">{{ $monthName }}</flux:heading>
<flux:button size="sm" wire:click="nextMonth" variant="outline"> <flux:button size="sm" wire:click="nextMonth" variant="outline" class="site-cal-nav">
<flux:icon name="chevron-{{ app()->getLocale() === 'ar' ? 'left' : 'right' }}" class="size-5" /> <flux:icon name="chevron-{{ app()->getLocale() === 'ar' ? 'left' : 'right' }}" class="size-5" />
</flux:button> </flux:button>
</div> </div>
@@ -268,7 +268,10 @@ new class extends Component
<button <button
wire:click="selectDate('{{ $dayData['date'] }}')" wire:click="selectDate('{{ $dayData['date'] }}')"
@class([ @class([
'calendar-cell rounded-lg text-center transition-colors font-medium min-h-[44px] text-white', 'calendar-cell site-cal-day rounded-lg text-center transition-colors font-medium min-h-[44px] text-white',
'is-available' => in_array($dayData['status'], ['available', 'partial']),
'is-selected' => $selectedDate === $dayData['date'],
'is-unavailable' => in_array($dayData['status'], ['past', 'closed', 'blocked', 'full', 'user_booked']),
'bg-[#4A5D23]/70 hover:bg-[#4A5D23]/85' => $dayData['status'] === 'available', 'bg-[#4A5D23]/70 hover:bg-[#4A5D23]/85' => $dayData['status'] === 'available',
'bg-[#D4A84B]/70 hover:bg-[#D4A84B]/85' => $dayData['status'] === 'partial', 'bg-[#D4A84B]/70 hover:bg-[#D4A84B]/85' => $dayData['status'] === 'partial',
'bg-sky-400/50 cursor-not-allowed' => $dayData['status'] === 'user_booked', 'bg-sky-400/50 cursor-not-allowed' => $dayData['status'] === 'user_booked',
@@ -8,20 +8,13 @@ new #[Layout('components.layouts.public')] class extends Component
// //
}; ?> }; ?>
<div class="wrap py-8"> <section class="sec">
<div class="max-w-2xl mx-auto py-16 px-4 text-center"> <div class="wrap">
<flux:icon name="check-circle" class="w-16 h-16 mx-auto text-green-500 mb-6" /> <div class="success-card">
<x-site.balance mode="static" style="width:120px;height:120px;margin:0 auto 18px" />
<flux:heading size="xl" class="mb-4 text-body"> <h1>{{ __('booking.success_title') }}</h1>
{{ __('booking.success_title') }} <p>{{ __('booking.success_message') }}</p>
</flux:heading> <a class="btn btn-ghost" href="{{ route('home') }}" wire:navigate>{{ __('site.nav_home') }}</a>
<p class="text-zinc-600 mb-6">
{{ __('booking.success_message') }}
</p>
<flux:button href="{{ route('home') }}">
{{ __('navigation.home') }}
</flux:button>
</div> </div>
</div> </div>
</section>
+79 -153
View File
@@ -276,181 +276,107 @@ new #[Layout('components.layouts.public')] class extends Component
</form> </form>
@endif @endif
@else @else
<div class="wrap py-8"> <section class="sec" style="padding-top:56px">
<div class="max-w-4xl mx-auto py-8 px-4"> <div class="wrap">
<flux:heading size="xl" class="mb-6 text-body"> <x-site.section-head :label="__('site.art_booking')" :title="__('booking.request_consultation')" :lede="__('site.booking_lede')" />
{{ __('booking.request_consultation') }}
</flux:heading>
<div class="book">
<div class="book-l on-dark">
<div class="contact">
<a href="tel:+970599353502"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M5 4h4l2 5-2.5 1.5a11 11 0 0 0 5 5L15 13l5 2v4a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2"/></svg><span dir="ltr">{{ __('footer.phone') }}</span></a>
<a href="mailto:{{ __('footer.email') }}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg><span>{{ __('footer.email') }}</span></a>
<span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true"><path d="M12 21s7-6.2 7-12a7 7 0 0 0-14 0c0 5.8 7 12 7 12z"/><circle cx="12" cy="9" r="2.5"/></svg><span>{{ __('footer.address') }}</span></span>
</div>
<x-site.balance mode="static" style="width:190px;height:190px;margin-top:22px" />
</div>
<div class="book-r">
@if($isLoggedIn) @if($isLoggedIn)
{{-- Logged-in user message --}} <div class="book-r-note">
<div class="bg-white rounded-lg shadow-sm border border-amber-200 p-8 text-center"> <h3>{{ __('booking.logged_in_title') }}</h3>
<div class="mb-6"> <p>{{ __('booking.logged_in_message') }}</p>
<div class="mx-auto w-16 h-16 bg-amber-100 rounded-full flex items-center justify-center mb-4"> <a class="btn btn-gold" href="{{ route('client.consultations.book') }}">{{ __('booking.go_to_dashboard') }}</a>
<flux:icon name="user-circle" class="w-8 h-8 text-amber-600" /> <p class="note">{{ __('booking.or_logout') }}</p>
</div>
<flux:heading size="lg" class="text-body mb-2">
{{ __('booking.logged_in_title') }}
</flux:heading>
<p class="text-zinc-600 max-w-md mx-auto">
{{ __('booking.logged_in_message') }}
</p>
</div>
<div class="flex flex-col sm:flex-row gap-3 justify-center">
<flux:button
href="{{ route('client.consultations.book') }}"
variant="primary"
class="min-h-[44px]"
>
<flux:icon name="calendar" class="w-4 h-4 me-2" />
{{ __('booking.go_to_dashboard') }}
</flux:button>
</div>
<p class="mt-6 text-sm text-zinc-500">
{{ __('booking.or_logout') }}
</p>
</div> </div>
@else @else
@if(session('success')) @if(session('success'))
<flux:callout variant="success" class="mb-6"> <p class="ok" role="status">{{ session('success') }}</p>
{{ session('success') }}
</flux:callout>
@endif @endif
@if(!$selectedDate || !$selectedTime) @if(!$selectedDate || !$selectedTime)
{{-- Step 1: Calendar Selection --}} <p class="note" style="text-align:start;margin:0 0 14px">{{ __('booking.select_date_time') }}</p>
{{-- Booking Expectations --}}
<x-booking-expectations class="mb-6" />
<p class="mb-4 text-zinc-600">
{{ __('booking.select_date_time') }}
</p>
<livewire:availability-calendar /> <livewire:availability-calendar />
@else @else
{{-- Step 2+: Contact Form & Confirmation --}} <div class="picked">
<div class="bg-amber-50 p-4 rounded-lg mb-6 border border-amber-200">
<div class="flex justify-between items-center">
<div> <div>
<p class="font-semibold text-zinc-900"> <b>{{ __('booking.selected_time') }}</b>
{{ __('booking.selected_time') }} <span>{{ \Carbon\Carbon::parse($selectedDate)->translatedFormat('l, d M Y') }}</span>
</p> <span dir="ltr">{{ \Carbon\Carbon::parse($selectedTime)->format('g:i A') }}</span>
<p class="text-zinc-600">
{{ \Carbon\Carbon::parse($selectedDate)->translatedFormat('l, d M Y') }}
</p>
<p class="text-zinc-600">
{{ \Carbon\Carbon::parse($selectedTime)->format('g:i A') }}
</p>
</div>
<flux:button size="sm" wire:click="clearSelection">
{{ __('common.change') }}
</flux:button>
</div> </div>
<button type="button" class="btn btn-ghost" wire:click="clearSelection">{{ __('common.change') }}</button>
</div> </div>
@if(!$showConfirmation) @if(!$showConfirmation)
{{-- Contact Form --}} <div class="fld">
<div class="space-y-4"> <label for="bp-name">{{ __('booking.guest_name') }} *</label>
<flux:field> <input id="bp-name" type="text" wire:model="guestName" autocomplete="name">
<flux:label class="required">{{ __('booking.guest_name') }}</flux:label> @error('guestName') <p class="err">{{ $message }}</p> @enderror
<flux:input wire:model="guestName" type="text" />
<flux:error name="guestName" />
</flux:field>
<flux:field>
<flux:label class="required">{{ __('booking.guest_email') }}</flux:label>
<flux:input wire:model="guestEmail" type="email" />
<flux:error name="guestEmail" />
</flux:field>
<flux:field>
<flux:label class="required">{{ __('booking.guest_phone') }}</flux:label>
<flux:input wire:model="guestPhone" type="tel" />
<flux:error name="guestPhone" />
</flux:field>
<flux:field>
<flux:label class="required">{{ __('booking.problem_summary') }}</flux:label>
<flux:textarea
wire:model="problemSummary"
rows="6"
placeholder="{{ __('booking.problem_summary_placeholder') }}"
/>
<flux:description>{{ __('booking.problem_summary_help') }}</flux:description>
<flux:error name="problemSummary" />
</flux:field>
{{-- Custom Captcha --}}
<flux:field>
<flux:label class="required">
{{ app()->getLocale() === 'ar' ? $captchaQuestion['question_ar'] : $captchaQuestion['question'] }}
</flux:label>
<div class="flex gap-2">
<flux:input wire:model="captchaAnswer" type="text" class="w-32" />
<flux:button size="sm" wire:click="refreshCaptcha" type="button">
<flux:icon name="arrow-path" class="w-4 h-4" />
</flux:button>
</div> </div>
<flux:error name="captchaAnswer" /> <div class="fld">
</flux:field> <label for="bp-email">{{ __('booking.guest_email') }} *</label>
<input id="bp-email" type="email" dir="ltr" wire:model="guestEmail" autocomplete="email">
<flux:button @error('guestEmail') <p class="err">{{ $message }}</p> @enderror
wire:click="showConfirm" </div>
class="w-full sm:w-auto min-h-[44px]" <div class="fld">
wire:loading.attr="disabled" <label for="bp-phone">{{ __('booking.guest_phone') }} *</label>
> <input id="bp-phone" type="tel" dir="ltr" wire:model="guestPhone" autocomplete="tel">
@error('guestPhone') <p class="err">{{ $message }}</p> @enderror
</div>
<div class="fld">
<label for="bp-summary">{{ __('booking.problem_summary') }} *</label>
<textarea id="bp-summary" rows="5" wire:model="problemSummary" placeholder="{{ __('booking.problem_summary_placeholder') }}"></textarea>
<p class="help">{{ __('booking.problem_summary_help') }}</p>
@error('problemSummary') <p class="err">{{ $message }}</p> @enderror
</div>
<div class="fld cap">
<label for="bp-captcha">{{ app()->getLocale() === 'ar' ? $captchaQuestion['question_ar'] : $captchaQuestion['question'] }} *</label>
<div class="cap-row">
<input id="bp-captcha" inputmode="numeric" dir="ltr" wire:model="captchaAnswer" style="max-width:140px">
<button type="button" class="btn btn-ghost refresh" wire:click="refreshCaptcha" aria-label="{{ __('site.captcha_refresh') }}">&#8635;</button>
</div>
@error('captchaAnswer') <p class="err">{{ $message }}</p> @enderror
</div>
<button type="button" class="btn btn-gold" wire:click="showConfirm" wire:loading.attr="disabled">
<span wire:loading.remove wire:target="showConfirm">{{ __('booking.continue') }}</span> <span wire:loading.remove wire:target="showConfirm">{{ __('booking.continue') }}</span>
<span wire:loading wire:target="showConfirm">{{ __('common.loading') }}</span> <span wire:loading wire:target="showConfirm">{{ __('common.loading') }}</span>
</flux:button> </button>
</div>
@else @else
{{-- Confirmation Step --}} <h3>{{ __('booking.confirm_booking') }}</h3>
<flux:callout> <p class="note" style="text-align:start">{{ __('booking.confirm_message') }}</p>
<flux:heading size="sm" class="text-body">{{ __('booking.confirm_booking') }}</flux:heading> <dl class="summary">
<p class="text-zinc-600">{{ __('booking.confirm_message') }}</p> <dt>{{ __('booking.guest_name') }}</dt><dd>{{ $guestName }}</dd>
<dt>{{ __('booking.guest_email') }}</dt><dd dir="ltr">{{ $guestEmail }}</dd>
<div class="mt-4 space-y-2"> <dt>{{ __('booking.guest_phone') }}</dt><dd dir="ltr">{{ $guestPhone }}</dd>
<p><strong>{{ __('booking.guest_name') }}:</strong> {{ $guestName }}</p> <dt>{{ __('booking.date') }}</dt><dd>{{ \Carbon\Carbon::parse($selectedDate)->translatedFormat('l, d M Y') }}</dd>
<p><strong>{{ __('booking.guest_email') }}:</strong> {{ $guestEmail }}</p> <dt>{{ __('booking.time') }}</dt><dd dir="ltr">{{ \Carbon\Carbon::parse($selectedTime)->format('g:i A') }}</dd>
<p><strong>{{ __('booking.guest_phone') }}:</strong> {{ $guestPhone }}</p> <dt>{{ __('booking.duration') }}</dt><dd>45 {{ __('common.minutes') }}</dd>
<p><strong>{{ __('booking.date') }}:</strong> <dt>{{ __('booking.problem_summary') }}</dt><dd>{{ $problemSummary }}</dd>
{{ \Carbon\Carbon::parse($selectedDate)->translatedFormat('l, d M Y') }}</p> </dl>
<p><strong>{{ __('booking.time') }}:</strong> @error('selectedTime')
{{ \Carbon\Carbon::parse($selectedTime)->format('g:i A') }}</p> <p class="err" role="alert">{{ $message }}</p>
<p><strong>{{ __('booking.duration') }}:</strong> 45 {{ __('common.minutes') }}</p> @enderror
</div> <button type="button" class="btn btn-gold" wire:click="submit" wire:loading.attr="disabled">
<div class="mt-4">
<p><strong>{{ __('booking.problem_summary') }}:</strong></p>
<p class="mt-1 text-sm text-zinc-600">{{ $problemSummary }}</p>
</div>
</flux:callout>
<div class="flex flex-col sm:flex-row gap-3 mt-4">
<flux:button wire:click="$set('showConfirmation', false)" class="w-full sm:w-auto min-h-[44px]">
{{ __('common.back') }}
</flux:button>
<flux:button
wire:click="submit"
variant="primary"
wire:loading.attr="disabled"
class="w-full sm:w-auto min-h-[44px]"
>
<span wire:loading.remove wire:target="submit">{{ __('booking.submit_request') }}</span> <span wire:loading.remove wire:target="submit">{{ __('booking.submit_request') }}</span>
<span wire:loading wire:target="submit">{{ __('common.submitting') }}</span> <span wire:loading wire:target="submit">{{ __('common.submitting') }}</span>
</flux:button> </button>
<button type="button" class="btn btn-ghost" wire:click="$set('showConfirmation', false)">{{ __('common.back') }}</button>
@endif
@endif
@endif
</div>
</div> </div>
@error('selectedTime') <x-booking-expectations class="mt-10" />
<flux:callout variant="danger" class="mt-4">
{{ $message }}
</flux:callout>
@enderror
@endif
@endif
@endif
</div>
</div> </div>
</section>
@endif @endif
@@ -0,0 +1,88 @@
<?php
use App\Models\Consultation;
use App\Models\User;
use App\Models\WorkingHour;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\RateLimiter;
use Livewire\Volt\Volt;
beforeEach(function () {
Mail::fake();
RateLimiter::clear('guest-booking:127.0.0.1');
foreach (range(0, 6) as $dow) {
WorkingHour::factory()->create(['day_of_week' => $dow, 'is_active' => true, 'start_time' => '09:00', 'end_time' => '13:00']);
}
});
test('booking page shows the section head, the forest card and the month calendar', function () {
$html = $this->get('/booking')->assertOk()->getContent();
expect($html)
->toContain('class="sec-head"')
->toContain('class="book"')
->toContain('book-l on-dark')
->toContain('data-balance="static"')
->toContain('tel:+970599353502')
->toContain('book-r')
->toContain('calendar-grid')
->not->toContain('class="days"');
expect(strpos($html, 'book-r'))->toBeLessThan(strpos($html, 'calendar-grid'));
});
test('selecting a slot reveals the fields and the full flow books once', function () {
$date = now()->addDay()->format('Y-m-d');
$c = Volt::test('pages.booking')
->call('selectSlot', $date, '09:00')
->assertSee('wire:model="guestName"', false)
->assertSee('wire:model="guestEmail"', false)
->assertSee('wire:model="guestPhone"', false)
->assertSee('wire:model="problemSummary"', false)
->assertSee('wire:model="captchaAnswer"', false)
->set('guestName', 'Test Guest')->set('guestEmail', 'g@example.com')->set('guestPhone', '+970599000000')
->set('problemSummary', 'A sufficiently long description of my case.')
->set('captchaAnswer', session('captcha_answer'))
->call('showConfirm')->assertSet('showConfirmation', true)
->call('submit')->assertRedirect(route('booking.success'));
expect(Consultation::count())->toBe(1);
});
test('validation errors render under their fields', function () {
$date = now()->addDay()->format('Y-m-d');
Volt::test('pages.booking')->call('selectSlot', $date, '09:00')
->call('showConfirm')
->assertHasErrors(['guestName', 'guestEmail', 'guestPhone', 'problemSummary', 'captchaAnswer'])
->assertSeeHtml('class="err"');
});
test('logged in clients see the message and no form on the booking page', function () {
$html = $this->actingAs(User::factory()->client()->create())->get('/booking')->assertOk()->getContent();
expect($html)->toContain(__('booking.logged_in_title'))->toContain(route('client.consultations.book'))
->not->toContain('wire:model="guestName"');
});
test('month calendar days carry hook classes without changing behaviour', function () {
$html = Volt::test('availability-calendar')->html();
expect($html)->toContain('site-cal-day')->toContain('is-available')->toContain('site-cal-nav')->toContain('wire:click="selectDate(');
});
test('calendar css is scoped to the public site', function () {
$css = file_get_contents(resource_path('css/public.css'));
expect($css)->toContain('body.site .book-r .site-cal-day')->toContain('body.site .book-r .site-cal-day.is-selected');
});
test('success page shows the balance, title, message and home button without flux output', function () {
$html = $this->get('/booking/success')->assertOk()->getContent();
expect($html)->toContain('data-balance="static"')->toContain(__('booking.success_title'))->toContain(__('booking.success_message'))
->toContain(__('site.nav_home'));
preg_match('~<main.*?</main>~s', $html, $main);
expect($main[0])->not->toContain('data-flux-');
});
+1 -1
View File
@@ -145,7 +145,7 @@ test('home page has the booking card with the finale balance', function () {
test('the booking page itself is not compact', function () { test('the booking page itself is not compact', function () {
$html = $this->get('/booking')->assertOk()->getContent(); $html = $this->get('/booking')->assertOk()->getContent();
expect($html)->not->toContain('book-r')->toContain('calendar-grid'); expect($html)->not->toContain('class="days"')->not->toContain('wire:submit="showConfirm"')->toContain('calendar-grid');
}); });
test('booking card keys exist in both languages', function () { test('booking card keys exist in both languages', function () {