The /api/portal/appointments contract returns ISO `startTime`/`endTime`
and no `date`/`time` fields. `isUpcoming()` read `appt.date`/`appt.time`
and called `parseTimeTo24Hour(undefined)` → `undefined.split(' ')` →
TypeError. The throw was swallowed by the fetch `try/catch`, surfacing
"Failed to load appointments" and making "Book New" unreachable for
every signed-in customer.
- Add `getAppointmentStart()` helper: prefers ISO `startTime`, falls
back to legacy `date` + `time`, returns null on missing/unparseable
input so callers never throw.
- Rewrite `isUpcoming()` on top of the helper.
- Add `formatAppointmentDate()` / `formatAppointmentTime()` and use them
at all date/time display sites (list row + RescheduleFlow header).
- Guard `parseTimeTo24Hour(undefined)`.
- Mark `date`/`time` optional and add `startTime`/`endTime` to the
`Appointment` type to match the API contract.
- Tests: API-shape fixtures + regression guards (no throw on startTime
shape, undefined-safe parse, helper resolution/formatting).
- Update UAT_PLAYBOOK.md §5.12 (customer portal appointments).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
40 KiB
UAT Playbook — GroomBook Web
1. Overview
GroomBook Web is the React 19 PWA frontend for the GroomBook pet grooming management platform. Built with Vite, it provides the UI for client/pet management, appointment scheduling, invoicing, staff management, and the customer portal. Extracted from the groombook/app monorepo.
2. Environments
| Environment | URL | Purpose |
|---|---|---|
| Dev | https://dev.groombook.dev |
Development environment for daily development |
| UAT | https://uat.groombook.dev |
User Acceptance Testing environment |
| Prod | https://demo.groombook.app |
Production/demo environment |
3. Pre-conditions
- UAT environment is accessible and running
- Test accounts are seeded with appropriate personas (manager, staff, client)
- OIDC authentication is configured and functional
- GroomBook API service is running and healthy
- Required test data exists (clients, pets, appointments, services, staff)
4. Auth Base URL Resolution
The auth client resolves its API base URL based on the VITE_API_URL environment variable:
- When
VITE_API_URLis set: Uses the configured URL as the auth base URL. - When
VITE_API_URLis unset: Falls back towindow.location.origin.
This allows the app to work correctly in both:
- Dev/PR deployments: Where
VITE_API_URLis explicitly set to the deployed API endpoint. - Local development: Where
VITE_API_URLis not set, using the same origin as the web app.
Auth Client Configuration (src/lib/auth-client.ts)
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_API_URL ?? "",
});
export const { signIn, signOut, useSession, changePassword } = authClient;
5. Test Cases
5.1 Authentication UI
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.1.1 | Login page loads | Navigate to UAT URL | Login form is displayed with OIDC provider button(s) |
| TC-WEB-5.1.2 | OIDC redirect | Click OIDC login button | Redirected to OIDC provider, then back to app with session established |
| TC-WEB-5.1.3 | Logout | Click logout button | Session cleared, redirected to login page |
| TC-WEB-5.1.4 | Session indicator | After successful login | User info/initials visible in UI indicating active session |
| TC-WEB-5.1.5 | Unauthenticated /login renders the form (GRO-2011) |
In a private/incognito window with no session cookie, navigate to UAT /login |
React root mounts; the GroomBook sign-in card with the OIDC button is visible. Network tab shows /api/auth/get-session 200, /api/setup/status 200, and the login form is rendered (NOT a blank white viewport). |
| TC-WEB-5.1.6 | Swallowed render error surfaces in DOM (GRO-2094) | Trigger a render-time exception in the React tree (e.g. via temporary throw in a child component on a test build) and load /login in a clean context |
Either the login form renders normally (happy path) OR the top-level ErrorBoundary testid error-boundary is visible with a populated error-boundary-message pre block showing the exception name/message/stack. NEVER a blank <div id="root"> with no error indicator. Browser console must contain either zero render errors or a [ErrorBoundary] line plus the raw exception. |
| TC-WEB-5.1.7 | Global error and unhandledrejection listeners are wired (GRO-2094) |
In a clean browser context, load /login, then trigger setTimeout(() => { throw new Error("synthetic") }, 0) from the console and Promise.reject(new Error("synthetic-promise")) |
Browser console shows [window.error] and [unhandledrejection] log lines with the thrown values. Confirms global listeners are active in production. |
5.2 Authentication — VITE_API_URL Set
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-AUTH-5.2.1 | Auth client uses configured API URL | Configure VITE_API_URL=https://api.example.com, load app |
Auth client sends requests to https://api.example.com |
| TC-AUTH-5.2.2 | Sign-in flow with configured API | Sign in when VITE_API_URL is set |
Auth requests go to configured URL |
| TC-AUTH-5.2.3 | Sign-out flow with configured API | Sign out when VITE_API_URL is set |
Auth requests go to configured URL |
5.3 Authentication — VITE_API_URL Unset (Fallback)
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-AUTH-5.3.1 | Auth client falls back to window.location.origin | Do not set VITE_API_URL, load app |
Auth client uses window.location.origin as base URL |
| TC-AUTH-5.3.2 | Sign-in on localhost | Load app without VITE_API_URL on localhost:3000 |
Auth client uses http://localhost:3000 as base URL |
| TC-AUTH-5.3.3 | Sign-in on dev environment | Load app without VITE_API_URL on https://dev.groombook.dev |
Auth client uses https://dev.groombook.dev as base URL |
| TC-AUTH-5.3.4 | SSO cookie set after Authentik callback (GRO-1592) | Complete Authentik SSO login on UAT without VITE_API_URL set |
__Secure-better-auth.session_token cookie is present in browser; subsequent /api/* calls include the cookie and return 200 |
5.4 Session Persistence
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-AUTH-5.4.1 | Session persists across page reload | Sign in, reload page | Session remains active |
| TC-AUTH-5.4.2 | Session clears on sign-out | Sign in, sign out | User is logged out, redirected to login |
5.4.1 SSO Login Journey (Authentik OIDC end-to-end)
| # | Scenario | Steps | Pass Criteria | Fail Criteria |
|---|---|---|---|---|
| TC-WEB-SSO-1 | Sign-in page shows SSO button | Navigate to app root URL | Sign-in page displayed with "Sign in with SSO" button visible | No SSO button, 403 before page loads |
| TC-WEB-SSO-2 | Click SSO redirects to Authentik | Click "Sign in with SSO" button | Browser redirected to Authentik login at auth.farh.net | No redirect, error shown, button does nothing |
| TC-WEB-SSO-3 | Valid OIDC credentials authenticate | At Authentik, enter valid credentials and authenticate | Redirected back to app with active session | Redirect loop, 403, session not established |
| TC-WEB-SSO-4 | Post-login dashboard accessible | After SSO flow completes, dashboard loads | Dashboard displays correctly with user identity shown | Blank page, 403, session not active |
| TC-WEB-SSO-5 | User identity displayed correctly | After SSO login, check header/nav | User name/email/initials shown in nav, role reflected in UI | No user indicator, wrong user shown |
5.4.2 OOBE Flow Post-Login
| # | Scenario | Steps | Pass Criteria | Fail Criteria |
|---|---|---|---|---|
| TC-WEB-OOBE-1 | Fresh DB shows setup wizard | On fresh DB (no super user), navigate to app | Setup wizard / OOBE screen displayed | Regular login page shown instead of setup |
| TC-WEB-OOBE-2 | Configure OIDC via setup | During OOBE, configure OIDC auth provider via /api/setup/auth-provider | OIDC configured successfully, no 403 | 403 during setup, config rejected |
| TC-WEB-OOBE-3 | Setup completes and redirects | Complete OOBE setup with business name | Redirected to app dashboard as super user, setup bypassed on reload | Setup errors, wrong redirect, setup reappears |
| TC-WEB-OOBE-4 | Admin panel accessible after setup | After completing OOBE, navigate to admin panel | Admin features accessible | 403 on admin panel, insufficient permissions |
| TC-WEB-OOBE-5 | SSO login during OOBE does not interfere | During fresh OOBE, attempt SSO login before completing setup | SSO login redirected appropriately, setup can still complete | Auto-provision creates staff prematurely, setup flow broken |
5.5 Dashboard
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.5.1 | Dashboard loads after login | Complete authentication | Dashboard page loads without errors |
| TC-WEB-5.5.2 | Key metrics visible | View dashboard | Revenue, appointments, clients, and other key metrics displayed |
| TC-WEB-5.5.3 | No blank state | On fresh login | Dashboard shows meaningful data, not empty/blank state |
5.6 Client Management UI
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.6.1 | Client list loads | Navigate to Clients section | List of clients is displayed |
| TC-WEB-5.6.2 | Create client | Click "New Client", fill form, submit | Client created successfully, appears in list |
| TC-WEB-5.6.3 | Edit client | Click on client, modify details, save | Client updated successfully |
| TC-WEB-5.6.4 | Search clients | Enter search term in search box | List filters to matching clients |
| TC-WEB-5.6.5 | Archive client | Click archive on client record | Client marked as archived, removed from active list |
5.7 Pet Management UI
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.7.1 | Pet profiles visible | Open client details | All pets for client displayed with basic info |
| TC-WEB-5.7.2 | Add pet | Click "Add Pet", fill form, submit | Pet created and linked to client |
| TC-WEB-5.7.3 | Edit pet details | Click on pet, modify details, save | Pet updated successfully |
| TC-WEB-5.7.4 | Grooming history view | View pet profile | Past appointments/grooming sessions displayed |
| TC-WEB-5.7.5 | Add pet with size/coat | Create pet with Size Category and Coat Type filled | Size and coat type persisted, visible on pet profile |
| TC-WEB-5.7.6 | Edit pet size/coat | Edit existing pet, change size/coat dropdowns | Updated values saved to pet record |
| TC-WEB-5.7.7 | Size/coat optional | Create pet without selecting size or coat | Pet created successfully, fields remain unset |
5.8.1 Buffer Rules Management UI (GRO-1173)
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.8.2 | Buffer rules section visible | Navigate to Settings | "Buffer Rules" section shown with description |
| TC-WEB-5.8.3 | Create buffer rule | Click "+ Add Rule", select service and buffer minutes, submit | Rule appears in list, matches service/size/coat |
| TC-WEB-5.8.4 | Edit buffer minutes inline | Click Edit on a rule, change minutes, click Save | New buffer value reflected in list |
| TC-WEB-5.8.5 | Delete buffer rule | Click Delete, confirm | Rule removed from list |
| TC-WEB-5.8.6 | Create rule with size/coat | Create rule with Size Category or Coat Type specified | Rule shows size/coat tags in list |
| TC-WEB-5.8.7 | Empty state | Navigate to Settings with no rules | "No buffer rules configured yet" message shown |
5.8 Appointment Scheduling UI
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.8.1 | Calendar view loads | Navigate to Appointments | Calendar view displays appointments |
| TC-WEB-5.8.2 | Create booking | Click "New Appointment", fill details, submit | Appointment created and appears on calendar |
| TC-WEB-5.8.3 | Modify appointment | Click on appointment, change details, save | Appointment updated successfully |
| TC-WEB-5.8.4 | Cancel appointment | Click cancel on appointment | Appointment marked as cancelled |
| TC-WEB-5.8.5 | Appointment groups | View grouped appointments | Related appointments display as group |
5.9 Service Management UI
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.9.1 | Service catalog loads | Navigate to Services | List of available services displayed |
| TC-WEB-5.9.2 | Create service | Click "New Service", fill form, submit | Service created successfully |
| TC-WEB-5.9.3 | Edit service | Click on service, modify details, save | Service updated successfully |
| TC-WEB-5.9.4 | Create service with default buffer | Create service with "Default buffer time" filled | Buffer shown in service list and form after save |
| TC-WEB-5.9.5 | Edit service buffer | Open existing service, change default buffer minutes | Updated value persisted after save |
5.10 Staff Management UI
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.10.1 | Staff list loads | Navigate to Staff | List of staff members displayed |
| TC-WEB-5.10.2 | Role display | View staff member | Staff role/permissions clearly visible |
5.11 Invoicing & Payments UI
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.11.1 | Invoice list loads | Navigate to Invoices | List of invoices displayed with status |
| TC-WEB-5.11.2 | Payment flow | Click "Pay" on unpaid invoice, complete payment | Payment processed, invoice marked as paid |
| TC-WEB-5.11.3 | Receipts view | View paid invoice | Receipt/payment details displayed |
5.12 Customer Portal UI
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.12.1 | Client-facing view | Log in as client persona | Customer portal UI displayed |
| TC-WEB-5.12.2 | Appointment list loads | Sign in as uat-customer@groombook.dev, open Appointments |
List of the customer's appointments renders. No "Failed to load appointments" error and no Retry button. (GRO-2180) |
| TC-WEB-5.12.3 | Date/time display | Inspect each appointment card | Each card shows a human-readable date and time derived from the API startTime (e.g. "Mon, Jun 1, 2026" / "10:00 AM"); no undefined or blank date/time. (GRO-2180) |
| TC-WEB-5.12.4 | Book New reachable | On the loaded Appointments view (non-readonly), look for the Book New button | "Book New" button is visible and opens the booking modal. (GRO-2180) |
| TC-WEB-5.12.5 | Upcoming/Past split | Toggle the Upcoming and Past tabs | Future appointments appear under Upcoming; completed/cancelled/past appear under Past. (GRO-2180) |
| TC-WEB-5.12.6 | Confirm appointment | Click confirm on pending appointment | Appointment status updated to confirmed |
| TC-WEB-5.12.7 | Cancel appointment | Click cancel on appointment | Appointment marked as cancelled |
| TC-WEB-5.12.8 | Reschedule display | Open Reschedule on an upcoming appointment | Summary header shows the current appointment's date and time (from startTime); no undefined. (GRO-2180) |
5.12b Dynamic Portal Time Slots (GRO-1793, GRO-2105)
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.12.5 | BookingFlow dynamic slots | Open Book New, select pet and service, pick a date | GET /api/book/availability?serviceId=<selected>&date=<picked>; "Checking availability…" shown while loading; slot list rendered |
| TC-WEB-5.12.6 | BookingFlow slots match wizard | Compare BookingFlow slot times with public booking wizard for same date | Same slots displayed |
| TC-WEB-5.12.7 | BookingFlow error state | Mock API failure on availability fetch (4xx/5xx OR a 200 with non-array body) | "Failed to load time slots" error shown and the page stays interactive (no white screen) |
| TC-WEB-5.12.8 | BookingFlow no slots | Select date with no availability | "No available slots on this date" shown |
| TC-WEB-5.12.9 | RescheduleFlow dynamic slots | Open reschedule, pick a new date | GET /api/book/availability?serviceId=<appt.serviceId>&date=<picked>; loading state shown; slot list rendered |
| TC-WEB-5.12.10 | RescheduleFlow error state | Mock API failure on availability fetch (4xx/5xx OR a 200 with non-array body) | "Failed to load time slots" error shown and the page stays interactive (no white screen) |
| TC-WEB-5.12.11 | RescheduleFlow no slots | Select date with no availability | "No available slots on this date" shown |
GRO-2105 regression note: prior to the fix, both
BookingFlowandRescheduleFlowcalled/api/book/availabilitywith onlydate=…, so the API responded 400{error:"serviceId and date are required"}. The React handler then.map()'d that error object, throwingTypeError: ee.map is not a functionand wiping<div id="root">. The fix ensures both flows includeserviceIdin the query string and surface the API's error string (or "Failed to load time slots") instead of crashing.
5.12c Waitlist/Booking Status Badges (GRO-1795)
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.12.12 | Confirmed badge | View appointment card with confirmed status | Green "Confirmed" badge displayed |
| TC-WEB-5.12.13 | Pending badge | View appointment card with pending status | Amber "Pending" badge displayed |
| TC-WEB-5.12.14 | Waitlisted badge | View appointment card with waitlisted status | Blue "Waitlisted" badge displayed |
| TC-WEB-5.12.15 | Badge uses CSS classes | Inspect badge element | Badge uses CSS variable-based classes (e.g., bg-green-100, text-amber-600), not hardcoded colors |
| TC-WEB-5.12.16 | Badge status from data | Compare badge label to appointment.status field | Badge label matches the API appointment status exactly |
| TC-WEB-5.12.17 | Unknown status fallback | Render badge with unknown status value | Badge renders with the raw status string as label and fallback CSS class |
5.13 Reports UI
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.13.1 | Revenue charts | Navigate to Reports | Revenue charts display with data |
| TC-WEB-5.13.2 | Utilization graphs | View reports | Staff/resource utilization graphs visible |
5.14 Settings UI
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.14.1 | Configuration page | Navigate to Settings | Settings page loads without errors |
| TC-WEB-5.14.2 | Form interactions | Modify settings, save | Settings saved successfully, changes reflected |
5.15 Navigation
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.15.1 | Sidebar/menu links | Click navigation items | Each section loads correctly |
| TC-WEB-5.15.2 | All sections reachable | Navigate through all menu items | All sections accessible, no 404 errors |
| TC-WEB-5.15.3 | No broken links | Test all navigation paths | All links work, no broken routes |
5.16 Mobile / PWA
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.16.1 | Responsive at 390x844 | Resize viewport to mobile dimensions | Layout adapts correctly, no horizontal scroll |
| TC-WEB-5.16.2 | PWA install prompt | Load app on supported browser | Install prompt appears when criteria met |
| TC-WEB-5.16.3 | Touch interactions | Use touch gestures on mobile | All interactions work with touch input |
5.17 Error & Empty States
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.17.1 | Form validation | Submit form with invalid data | Appropriate validation errors displayed |
| TC-WEB-5.17.2 | Missing data | Navigate to section with no data | Empty state message displayed, not blank page |
| TC-WEB-5.17.3 | Error boundaries | Trigger error condition | Friendly error message displayed, app doesn't crash |
5.18 Pet Profile UI — Enhanced Fields (GRO-1178)
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.18.1 | Coat type displayed in Grooming tab | Open pet profile, go to Grooming tab | Coat type shown (e.g. "Curly", "Double") |
| TC-WEB-5.18.2 | Preferred cuts displayed | Open Grooming tab | Preferred cuts shown as tags/chips |
| TC-WEB-5.18.3 | Temperament score displayed (read-only) | Open Basic Info tab | 1–5 star display with score label "(N/5 · staff-set)" |
| TC-WEB-5.18.4 | Temperament flags displayed (read-only) | Open Basic Info tab | Flag chips shown (e.g. "Anxious", "Good with kids") |
| TC-WEB-5.18.5 | Medical alerts in Medical tab | Open Medical tab | Alert cards with type, description, severity badge |
| TC-WEB-5.18.6 | Medical alert severity badges | View Medical tab | Low=green, Medium=amber, High=red badges |
| TC-WEB-5.18.7 | Edit pet — coat type dropdown | Click Edit on pet, select coat type | Coat type persisted on save |
| TC-WEB-5.18.8 | Edit pet — add medical alert | Click Edit, add alert with type + severity, save | Alert appears in Medical tab after save |
| TC-WEB-5.18.9 | Edit pet — remove medical alert | Click Edit, remove an alert, save | Alert removed after save |
| TC-WEB-5.18.10 | Edit pet — add preferred cut (Enter) | Click Edit, type cut name, press Enter | Cut tag added; persists after save |
| TC-WEB-5.18.11 | Edit pet — remove preferred cut | Click Edit, click X on cut tag | Cut removed; not persisted after save |
| TC-WEB-5.18.12 | Medical alert validation | Click Edit, add alert with empty type, try to save | Error "Type is required"; form not submitted |
| TC-WEB-5.18.13 | Temperament fields read-only | View edit form for pet with temperament data | Temperament score and flags not editable (display only) |
5.19 Booking Wizard — Pet Size & Coat (GRO-1174)
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.19.1 | Pet size dropdown visible | Step 3 of booking wizard (pet details) | Pet size dropdown shown after breed field with options: Small, Medium, Large, X-Large |
| TC-WEB-5.19.2 | Coat type dropdown visible | Step 3 of booking wizard | Coat type dropdown shown after pet size with options: Smooth, Double, Curly, Wire, Long, Hairless |
| TC-WEB-5.19.3 | Size/coat pre-fill from URL | Navigate to booking with ?petSizeCategory=large&petCoatType=curly |
Fields pre-filled with provided values |
| TC-WEB-5.19.4 | Size/coat optional | Proceed through booking without selecting size/coat | Booking completes successfully |
| TC-WEB-5.19.5 | Confirmation shows appointment duration | Confirm booking step | Service duration shown as "X min appointment" (buffer not exposed) |
| TC-WEB-5.19.6 | Confirmation shows pet size/coat | Confirm booking with size/coat selected | Size and coat type shown on pet card in confirmation |
| TC-WEB-5.19.7 | Availability uses buffer for large/x-large | Select large or x-large size, check availability | Availability slots reflect service duration + buffer for large/x-large |
| TC-WEB-5.19.8 | Form reset clears size/coat | Complete booking, click "Book another" | Size and coat fields reset to empty |
| TC-WEB-5.19.9 | New pet record has size/coat | Complete booking, view created pet in admin | Pet record shows selected size and coat type |
5.20 Buffer Rules Management — Admin UI (GRO-1173)
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.20.1 | Buffer rules section loads | Navigate to Settings page (admin) | "Buffer Rules" section visible with "+ Add Rule" button |
| TC-WEB-5.20.2 | Add rule — required fields only | Click "+ Add Rule", select a service, enter buffer minutes, submit | Rule created, appears in list below |
| TC-WEB-5.20.3 | Add rule — with size category | Add rule, select service + size category + buffer minutes | Rule created with size tag shown in list |
| TC-WEB-5.20.4 | Add rule — with coat type | Add rule, select service + coat type + buffer minutes | Rule created with coat tag shown in list |
| TC-WEB-5.20.5 | Add rule — with both size and coat | Add rule, select service + size + coat + buffer minutes | Rule created with both tags shown |
| TC-WEB-5.20.6 | Validation — missing service | Submit form without selecting service | Error: "Service and valid buffer minutes are required" |
| TC-WEB-5.20.7 | Validation — zero buffer | Submit form with 0 buffer minutes | Error: "Service and valid buffer minutes are required" |
| TC-WEB-5.20.8 | Edit rule inline | Click "Edit" on a rule, change buffer value, click "Save" | Rule updated in list |
| TC-WEB-5.20.9 | Cancel edit | Click "Edit", then "Cancel" | Original value unchanged |
| TC-WEB-5.20.10 | Delete rule — confirmation | Click "Delete" on a rule | Confirmation prompt appears |
| TC-WEB-5.20.11 | Confirm delete | On confirmation prompt, click "Confirm" | Rule removed from list |
| TC-WEB-5.20.12 | Cancel delete | On confirmation prompt, click "Cancel" | Rule remains in list |
| TC-WEB-5.20.13 | Empty state | No rules exist | Message: "No buffer rules configured yet." |
| TC-WEB-5.20.14 | Toggle form | Click "+ Add Rule", then "Cancel" | Form hidden, no rule created |
5.21 Service Default Buffer Minutes (GRO-1173)
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.21.1 | Default buffer shown in table | Navigate to Services page | "Default Buffer" column visible in services table |
| TC-WEB-5.21.2 | New service default is 0 | Click "+ Add Service" | Default Buffer field pre-filled with 0 |
| TC-WEB-5.21.3 | Create service with buffer | Fill service form, set Default Buffer = 10, submit | Service created with 10 min default buffer |
| TC-WEB-5.21.4 | Edit service — view buffer | Edit an existing service | Current default buffer value shown in form |
| TC-WEB-5.21.5 | Update buffer on existing service | Edit service, change Default Buffer to 15, save | Buffer updated, table shows 15 min |
| TC-WEB-5.21.6 | Buffer field — zero allowed | Set Default Buffer to 0, save | Service saved with 0 (no default buffer) |
| TC-WEB-5.21.7 | Buffer field — integer only | Enter non-integer value | Field restricts to integer values |
5.22 Pet Profile — Size Category & Coat Type (GRO-1173)
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.22.1 | Size category dropdown visible | Open Add Pet or Edit Pet form (portal) | "Size Category" dropdown visible with options: Small, Medium, Large, X-Large |
| TC-WEB-5.22.2 | Coat type dropdown visible | Open Add Pet or Edit Pet form | "Coat Type" dropdown visible with options: Smooth, Double, Curly, Wire, Long, Hairless |
| TC-WEB-5.22.3 | Size and coat both optional | Submit pet form without selecting size or coat | Pet saved successfully |
| TC-WEB-5.22.4 | Save pet with size category | Select "Large", fill required fields, save | Pet saved with size = "large" |
| TC-WEB-5.22.5 | Save pet with coat type | Select "Curly", fill required fields, save | Pet saved with coat = "curly" |
| TC-WEB-5.22.6 | Size and coat persisted | Save pet with size + coat, edit again | Both fields retain their selected values |
| TC-WEB-5.22.7 | Clear size | Select size, then clear back to default | Size cleared on save |
5.23 Pet Profile — API Persistence & Save UX (GRO-1470)
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.23.1 | Save pet — API persistence | Edit a pet, change a field (e.g. coat type), click Save, reload the page | Changed field retained after reload (proves PATCH round-trip to server) |
| TC-WEB-5.23.2 | Save pet — error state | Trigger an API save failure (e.g. network error) | Error message displayed; edit form stays open; no data cleared |
| TC-WEB-5.23.3 | Save pet — saving indicator | Click Save | Spinner/indicator shown while request is in flight; form controls disabled |
5.24 Booking Funnel Analytics Events (GRO-1794)
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.24.1 | booking_step_service — public | Select a service in the public booking wizard | booking_step_service CustomEvent fires with detail.step="service" and detail.flow="public" |
| TC-WEB-5.24.2 | booking_step_time — public | Select a time slot and click Continue | booking_step_time fires with detail.step="time" and detail.flow="public" |
| TC-WEB-5.24.3 | booking_step_contact — public | Fill in contact/pet form, click "Review booking" | booking_step_contact fires with detail.step="contact" and detail.flow="public" |
| TC-WEB-5.24.4 | booking_step_submit — public | Confirm and submit the booking | booking_step_submit fires with detail.step="submit" and detail.flow="public" |
| TC-WEB-5.24.5 | booking_confirmed — public | Navigate to /booking-confirmed | booking_confirmed fires once on mount with detail.step="confirmed" and detail.flow="public" |
| TC-WEB-5.24.6 | booking_error — public | Navigate to /booking-error | booking_error fires once on mount with detail.step="error" and detail.flow="public" |
| TC-WEB-5.24.7 | booking_step_service — portal | Select a pet in the portal BookingFlow | booking_step_service fires with detail.step="service" and detail.flow="portal" |
| TC-WEB-5.24.8 | booking_step_time — portal | Pick a date and time in portal BookingFlow | booking_step_time fires with detail.step="time" and detail.flow="portal" |
| TC-WEB-5.24.9 | booking_step_contact — portal | Proceed from groomer selection to review screen | booking_step_contact fires with detail.step="groomer" and detail.flow="portal" |
| TC-WEB-5.24.10 | booking_step_submit — portal | Submit booking in portal BookingFlow | booking_step_submit fires with detail.step="submit" and detail.flow="portal" |
| TC-WEB-5.24.11 | booking_confirmed — portal | Portal booking request succeeds | Inline success state is shown and booking_confirmed fires with detail.step="confirmed" and detail.flow="portal" |
| TC-WEB-5.24.12 | No PII in analytics payloads | Fire each event and inspect detail object | Payload contains only: step, flow, timestamp — no names, emails, phone numbers, or pet names |
| TC-WEB-5.24.13 | No-op safe | Trigger analytics with window.dispatchEvent blocked (e.g. CSP) | No error thrown; booking flow completes normally |
5.25 Customer Portal — Better Auth SSO Bridge (GRO-1867)
These cases cover the CustomerPortal initialisation path that bridges an Authentik / Better Auth session into a portal session via POST /api/portal/session-from-auth. The bridge runs after the URL-impersonation (?sessionId=) and dev-user paths have been ruled out.
Pre-conditions:
- UAT is configured with Authentik SSO. The seeded customer Authentik password lives in the
authentik-uat-users-credentialsSecret in thegroombook-uatnamespace (keyuat_customer_password) — NOT inseed-uat-passwords:customer-password(that Secret holds the Better Auth email+password credential, a separate identity store; see GRO-2089). Pull the Authentik password at the start of every run:The Authentik user is provisioned by Terraform (CUSTOMER_AUTHENTIK=$(kubectl get secret authentik-uat-users-credentials -n groombook-uat \ -o jsonpath='{.data.uat_customer_password}' | base64 -d)infra/terraform/users.tf); thelifecycle.ignore_changes = [password]block means the password is set on initial creation and never auto-rotated, so the value held in the live Secret is the one Authentik itself has. If Authentik rejects it, the user was re-provisioned out-of-band via the Authentik admin UI and the Secret has drifted from the live identity — fix the Secret (or the admin-set password) and re-run. POST /api/portal/session-from-authfrom GRO-1866 is deployed on UAT.- Clear cookies and localStorage between cases unless otherwise noted.
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.25.1 | Authenticated customer reaches portal dashboard | 1. From clean state, navigate to UAT /login. 2. Click "Sign in with SSO" and complete Authentik flow with a seeded customer identity. 3. After callback, land on /. |
Portal dashboard renders. No redirect to /login. No impersonation banner. Top-right greeting reads "Hi, <FirstName>". |
| TC-WEB-5.25.2 | Bridge call sequence | Repeat TC-WEB-5.25.1 with DevTools → Network open and the All tab filtered to /api/. |
In order: GET /api/auth/get-session → 200. POST /api/portal/session-from-auth → 201 with body { sessionId, clientId, clientName }. |
| TC-WEB-5.25.3 | Subsequent portal calls use the bridged session ID | After TC-WEB-5.25.1 succeeds, navigate to Appointments, My Pets, Billing, Settings. Inspect any /api/portal/* request in DevTools → Network. |
Each portal API call carries an X-Impersonation-Session-Id header whose value equals the sessionId returned by session-from-auth (not a URL-param value). Each call returns 200 (or 404 for genuinely empty collections), never 401. |
| TC-WEB-5.25.4 | No impersonation chrome for the customer's own session | After TC-WEB-5.25.1, scan the portal UI. | No amber border around the page. No "STAFF VIEW" watermark. No "End Impersonation" button in the sidebar. The customer is themselves; only impersonation sessions started via ?sessionId= show the banner. |
| TC-WEB-5.25.5 | 404 fallback for authenticated user with no client record | 1. Sign in via SSO with an Authentik account whose email is not present in clients. 2. Land on /. |
POST /api/portal/session-from-auth returns 404. The portal renders a centred card titled "Portal access not configured" with the message about contacting the groomer and a Sign out button. No redirect loop, no portal chrome. |
| TC-WEB-5.25.6 | 404 fallback Sign-out escape hatch | From TC-WEB-5.25.5 click Sign out. | POST /api/auth/sign-out fires; browser navigates to /login; the Authentik session cookie is cleared. Reloading / no longer hits 404 (will show the login page). |
| TC-WEB-5.25.7 | Bridge precedence — impersonation URL wins | 1. Sign in via SSO as a customer. 2. Open a new tab to https://uat.groombook.dev/?sessionId=<a-valid-staff-impersonation-session-id>. |
The impersonation path runs; the amber banner appears for the impersonated client. The Better Auth bridge is not called on this load (session-from-auth absent in Network). |
| TC-WEB-5.25.8 | Bridge precedence — dev user wins | In dev mode (e.g. local) with localStorage["dev-user"] set to a client persona, navigate to /. |
The dev-session path runs (POST /api/portal/dev-session). The Better Auth bridge is not called (session-from-auth absent in Network). Staff dev users still redirect to /admin. |
| TC-WEB-5.25.9 | Staff Better Auth session does not run the customer bridge | Sign in via SSO with a staff identity. Navigate to /. |
App.tsx routing redirects to /admin. POST /api/portal/session-from-auth is not called. |
| TC-WEB-5.25.10 | Unauthenticated user is sent to login (no infinite loop) | Without signing in, navigate directly to /. |
App.tsx renders the LoginPage. CustomerPortal does not render. No session-from-auth request is made. |
| TC-WEB-5.25.11 | Session persists across reload via Better Auth cookie | After TC-WEB-5.25.1 succeeds, reload the page. | Portal dashboard re-renders. A fresh GET /api/auth/get-session + POST /api/portal/session-from-auth pair runs and yields 200/201. Greeting still reads "Hi, <FirstName>". |
5.27 Customer Portal — Authenticated HTML-route cold mount (GRO-2099)
These cases guard against the regression where a customer who had just completed SSO sign-in was bounced back to /login (with a blank React root) when navigating directly to /portal, /book, /schedule, or even /login itself. Root cause: Dashboard.tsx's !sessionId && !isImpersonating && !getDevUser() guard fired during the CustomerPortal's bootstrap — before the SSO bridge resolved portalSessionId — and redirected to /login. The fix: CustomerPortal now shows a loading state while the bootstrap is in flight, so the portal chrome and its !sessionId child guards do not mount prematurely. App.tsx additionally redirects an authenticated user at /login to / instead of rendering null.
Pre-conditions:
- TC-WEB-5.25.1 — TC-WEB-5.25.3 must pass on the build under test.
- Clear cookies and localStorage between cases.
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.27.1 | Authenticated customer lands on /portal after direct nav |
1. From clean state, complete TC-WEB-5.25.1 (SSO sign-in as a customer). 2. Land on /. 3. browser_navigate (full page load) directly to /portal. |
Final URL stays at /portal. The React root is non-empty. The portal dashboard renders with the customer's name. No Navigate to /login fires. |
| TC-WEB-5.27.2 | Authenticated customer lands on /book and /schedule after direct nav |
From TC-WEB-5.27.1, browser_navigate to /book then /schedule (one fresh navigation each). |
Each final URL stays at the navigated path. The portal chrome is visible. The page does not redirect to /login. |
| TC-WEB-5.27.3 | Authenticated customer at /login is auto-redirected to / |
From TC-WEB-5.27.1, browser_navigate to /login. |
The browser ends at / (not at a blank /login). The portal dashboard renders. No blank React root at /login. |
| TC-WEB-5.27.4 | Loading state is visible during the bootstrap, no portal chrome flash | 1. With the UAT build under test, open DevTools → Network and throttle to Slow 3G. 2. Sign in via SSO. 3. Land on /. |
A "Loading…" element (role="status") is briefly visible. The portal nav (Home / Appointments / etc.) is NOT visible during the loading window. No Navigate to /login fires during the bootstrap. |
| TC-WEB-5.27.5 | SSO bridge still runs and yields 201 | From TC-WEB-5.27.4 (or TC-WEB-5.27.1), inspect Network. | The same GET /api/auth/get-session (200) → POST /api/portal/session-from-auth (201) sequence from TC-WEB-5.25.2 still runs. The customer name appears in the greeting. |
| TC-WEB-5.27.6 | Unauthenticated direct nav to /portal still ends at /login (no regression) |
Clear cookies. browser_navigate to /portal. |
The portal briefly shows the loading state, then CustomerPortal's !session && !portalSessionId guard redirects to /login. The login form renders. No infinite loop. |
| TC-WEB-5.27.7 | Groomer SSO still works (no regression) | 1. From clean state, sign in via SSO as the groomer identity (uat-groomer). 2. Land on /. |
App.tsx's staff check redirects to /admin. The groomer nav renders. No CustomerPortal flash. No /portal redirect loop. |
| TC-WEB-5.27.8 | Impersonation session still works (no regression) | 1. With an active impersonation session, open /?sessionId=<id>. |
The amber "STAFF VIEW" chrome renders. The portal loads. No /login redirect. |
5.26 Customer Portal — RescheduleFlow under SSO Bridge (GRO-2012)
These cases guard against the regression where an SSO-bridge customer (no ?sessionId= URL param, no impersonation session) could trigger the RescheduleFlow and have RescheduleFlow receive sessionId={null}, which caused the internal /api/book/availability call to send X-Impersonation-Session-Id: (empty) and return 401. The fix: CustomerPortal now passes sessionId={session?.id ?? portalSessionId} to <RescheduleFlow> (matching the fallback renderSection() already used).
Pre-conditions:
- TC-WEB-5.25.1 — TC-WEB-5.25.3 must pass on the build under test.
- The seeded customer used has at least one upcoming, non-cancelled appointment with
status∈ {pending,confirmed}.
| # | Scenario | Steps | Expected |
|---|---|---|---|
| TC-WEB-5.26.1 | RescheduleFlow receives portalSessionId (no 401) | 1. Complete TC-WEB-5.25.1 (SSO sign-in as a customer). 2. From the dashboard, click Reschedule on the next-upcoming appointment. 3. In the RescheduleFlow modal, pick a future date. 4. Open DevTools → Network and filter to /api/. |
The GET /api/book/availability?date=<picked> request includes an X-Impersonation-Session-Id header whose value equals the sessionId from session-from-auth. The request returns 200. The time-slot list populates. No 401. |
| TC-WEB-5.26.2 | RescheduleFlow submit succeeds | From TC-WEB-5.26.1, pick a time slot and confirm. | POST /api/portal/appointments/<id>/reschedule (or the equivalent) includes the same X-Impersonation-Session-Id value. Returns 200. The modal closes and the appointment card reflects the new time. |
| TC-WEB-5.26.3 | Impersonation flow reschedule is unchanged (no regression) | 1. With an active impersonation session (?sessionId=<active>), load /. 2. Click Reschedule on an appointment. 3. Pick a date. |
GET /api/book/availability includes X-Impersonation-Session-Id equal to the impersonation sessionId (not portalSessionId). Returns 200. Behaves identically to the pre-fix build. |
| TC-WEB-5.26.4 | No X-Impersonation-Session-Id is empty / null |
From TC-WEB-5.26.1, inspect every /api/portal/* and /api/book/* request. |
No request has an empty or null X-Impersonation-Session-Id header. |
6. Pass/Fail Criteria
Pass:
- All test cases execute without errors
- Expected results match actual results for all scenarios
- No visual regressions compared to baseline
- No console errors or warnings in browser DevTools
Fail:
- Any unexpected result with severity
- Steps to reproduce provided
- Screenshot or screen recording of failure
- Error details from browser console or network tab
7. Update Policy
Any PR that changes user-facing behaviour MUST update this file.
When modifying the GroomBook Web application in ways that affect the user interface or user experience:
- Review all relevant test cases in this playbook
- Add new test cases for new features or flows
- Modify existing test cases if behaviour changes
- Remove test cases for deprecated features
- Reference the updated section(s) in the PR description (e.g., "Updated UAT_PLAYBOOK.md §5.5 — new appointment group feature")