# 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_URL` is set:** Uses the configured URL as the auth base URL.
- **When `VITE_API_URL` is unset:** Falls back to `window.location.origin`.

This allows the app to work correctly in both:
- **Dev/PR deployments:** Where `VITE_API_URL` is explicitly set to the deployed API endpoint.
- **Local development:** Where `VITE_API_URL` is not set, using the same origin as the web app.

### Auth Client Configuration (src/lib/auth-client.ts)

```typescript
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 | View client portal appointments | List of client's appointments visible — each card shows pet name, service, formatted date/time, and groomer (no "Failed to load appointments" error, no blank screen). "Book New" button is visible and clickable. See 5.12d. |
| TC-WEB-5.12.3 | Confirm appointment | Click confirm on pending appointment | Appointment status updated to confirmed |
| TC-WEB-5.12.4 | Cancel appointment | Click cancel on appointment | Appointment marked as cancelled |

#### 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 `BookingFlow` and
> `RescheduleFlow` called `/api/book/availability` with only `date=…`, so the
> API responded 400 `{error:"serviceId and date are required"}`. The React
> handler then `.map()`'d that error object, throwing `TypeError: ee.map is
> not a function` and wiping `<div id="root">`. The fix ensures both flows
> include `serviceId` in 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.12d Appointment API Shape Normalization (GRO-2180)

| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-5.12.18 | Portal appointments load (regression) | Sign in as `uat-customer@groombook.dev`, open `Appointments` | List renders without the "Failed to load appointments. Please try again." error; "Book New" button is visible and clickable |
| TC-WEB-5.12.19 | Card fields populated from API | Inspect an appointment card | Pet name, service, formatted date (e.g. "Mon, Jun 1, 2026"), time (e.g. "10:00 AM"), and groomer name render — derived from the API's `startTime`/`endTime`/nested `pet`/`staff` objects |
| TC-WEB-5.12.20 | Upcoming vs Past split | View both tabs | Future, non-cancelled/non-completed appointments appear under "Upcoming"; past/completed/cancelled under "Past" (classification uses absolute `startTime`) |
| TC-WEB-5.12.21 | Reschedule from card | Expand an upcoming appointment, click Reschedule, pick a date | `GET /api/book/availability?serviceId=<appt.serviceId>&date=<picked>` fires with a non-empty `serviceId` (sourced from the API's nested `service.id`) |

> **GRO-2180 regression note:** `/api/portal/appointments` returns ISO
> `startTime`/`endTime` and nested `pet`/`service`/`staff` objects, but the portal
> client `Appointment` type expected flat `date`/`time`/`petName` fields.
> `isUpcoming()` read `appt.date`/`appt.time` (both `undefined`), so
> `parseTimeTo24Hour(undefined)` threw `TypeError`, the `useEffect` `try/catch`
> set the error state, and the "Book New" button (only rendered in the success
> path) became unreachable. The fix normalizes the API response into the flat
> `Appointment` shape at the fetch boundary (`normalizeAppointment`), prefers the
> absolute `startTime` in `isUpcoming`, and hardens `parseTimeTo24Hour` against
> blank/undefined input.

#### 5.12e Book New `preferredTime` Formatting (GRO-2211, GRO-2213)

| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-WEB-5.12.22 | Slot buttons show formatted label | Sign in as `uat-customer@groombook.dev`, open `Appointments`, click "Book New", select a pet and service, pick a date with availability | Each time-slot button shows a human-readable label like `10:00 AM` (UTC), never a raw ISO timestamp (e.g. not `2026-06-09T10:00:00.000Z`) |
| TC-WEB-5.12.23 | Confirmation review shows formatted label | Continue the Book New wizard to the Review step | The "Date & Time" summary and the final confirmation both display the formatted slot label (e.g. `10:00 AM`), not a raw ISO string |
| TC-WEB-5.12.24 | Booking submit succeeds (regression) | Complete the Book New wizard and submit the request | Request succeeds with no `500` / `invalid input syntax for type time` error; the booking POST sends `preferredTime` as `HH:MM:SS` (e.g. `10:00:00`); the new appointment appears in the Upcoming list |

> **GRO-2211/GRO-2213 note:** The Book New wizard previously rendered the raw
> UTC ISO slot string as the button/confirmation label and submitted that same
> ISO value as `preferredTime`, which the API rejected with
> `invalid input syntax for type time` (HTTP 500). The fix adds shared UTC
> helpers `formatSlotLabel(slot)` (display → `10:00 AM`) and `slotToTime(slot)`
> (payload → `HH:MM:SS`) in `src/portal/sections/Appointments.tsx`, so the
> displayed label and the submitted `preferredTime` both derive from the same
> canonical UTC ISO slot. (The sibling `RescheduleFlow` `startTime` raw-ISO issue
> on a different endpoint is tracked separately and is out of scope here.)

### 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-credentials` Secret in the `groombook-uat` namespace (key `uat_customer_password`) — **NOT** in `seed-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:
  ```bash
  CUSTOMER_AUTHENTIK=$(kubectl get secret authentik-uat-users-credentials -n groombook-uat \
    -o jsonpath='{.data.uat_customer_password}' | base64 -d)
  ```
  The Authentik user is provisioned by Terraform (`infra/terraform/users.tf`); the `lifecycle.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-auth` from [GRO-1866](https://paperclip.farhoodlabs.com/GRO/issues/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, &lt;FirstName&gt;". |
| 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, &lt;FirstName&gt;". |

### 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:
1. Review all relevant test cases in this playbook
2. Add new test cases for new features or flows
3. Modify existing test cases if behaviour changes
4. Remove test cases for deprecated features
5. Reference the updated section(s) in the PR description (e.g., "Updated UAT_PLAYBOOK.md §5.5 — new appointment group feature")