Files
web/UAT_PLAYBOOK.md
T
Flea Flicker fe565861b9
CI / Test (push) Successful in 20s
CI / Lint & Typecheck (push) Successful in 25s
CI / Build & Push Docker Image (push) Successful in 20s
Promote uat → main (PROD): GRO-2319 portal StatusBadge palette (#71)
Co-authored-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
Co-committed-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
2026-06-10 08:57:46 +00:00

55 KiB
Raw Blame History

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)

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.12f Live StatusBadge palette — no-show / pending / waitlisted (GRO-2319)

These cases exercise the full StatusBadge palette as it is now produced live by the seeded UAT customer (uat-customer@groombook.dev), not just unit-rendered.

# Scenario Steps Expected
TC-WEB-5.12.26 No-show badge (item 1) Sign in as uat-customer@groombook.dev, open AppointmentsPast tab, find the seeded no_show appointment A styled yellow "No-show" badge renders (bg-yellow-100 text-yellow-700) — not a raw gray no_show label. The DB no_show (underscore) status is normalized to the no-show palette key.
TC-WEB-5.12.27 Pending derivation (item 2) On the Upcoming tab, find the seeded upcoming appointment whose confirmationStatus is pending (groomer-unconfirmed) The card's top-row badge reads amber "Pending" (derived from confirmationStatus), even though the raw appointment status is scheduled.
TC-WEB-5.12.28 Confirmed not overridden On the Upcoming tab, find the seeded confirmed appointment (confirmationStatus = confirmed) Badge still reads green "Confirmed" — the pending derivation does not override a confirmed appointment.
TC-WEB-5.12.29 Waitlisted card (item 2) On the Upcoming tab, find the seeded waitlist entry for the customer A card renders with a blue "Waitlisted" badge, a dashed muted border, and the subtext "You're on the waitlist — we'll let you know if a spot opens." The Confirm / Reschedule / Cancel / Notes actions are not shown for this entry (it is not a booked appointment).

GRO-2319 note: the DB appointment_status enum cannot represent pending or waitlisted, so those badges are derived in the portal: pending from an upcoming appointment's confirmationStatus, and waitlisted from active waitlist_entries surfaced by GET /api/portal/appointments as synthetic cards. The no_showno-show key normalization fixes the cosmetic badge mismatch (item 1).

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
TC-WEB-5.12.25 Slow-wizard submit succeeds (GRO-2234) Sign in as uat-customer@groombook.dev, open Appointments, click "Book New", then deliberately pace the wizard (pet → service → groomer → date/slot → review) so that >2 minutes elapse before clicking "Confirm Booking". Submit returns success — no "Failed to book appointment. Please try again." error. In DevTools → Network, if the first POST /api/portal/waitlist returns 401, a POST /api/portal/session-from-auth fires immediately after and the booking is retried once with the fresh X-Impersonation-Session-Id, then returns 201. The appointment appears in the Upcoming list.

GRO-2234 note: A deliberately-paced Book New wizard could outlive the portal impersonation session, so the final POST /api/portal/waitlist returned 401 {"error":"Unauthorized"} ("Failed to book appointment"). The web fix adds a transparent one-shot re-mint: on a 401 from the waitlist submit, BookingFlow calls POST /api/portal/session-from-auth (the Better Auth cookie is still valid) and retries the submit once with the fresh session id. The companion API fix (groombook/api GRO-2234) adds bounded sliding expiration so active sessions rarely lapse in the first place.

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 15 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:
    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 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.

5.28 Route Planner Page (GRO-2158)

The admin Route Planner lives at /admin/routes. It shows a groomer's geocoded appointment stops for a chosen date on a react-leaflet / OpenStreetMap map (numbered pins + a connecting polyline), a stop-list panel, a travel-time/distance summary, a route status badge, and an Optimize button wired to POST /api/routes/optimize. Leaflet is loaded via a dynamic import so it ships as a separate code-split chunk. Groomers are auto-filtered to their own route (no groomer selector); managers/receptionists pick a groomer.

Pre-conditions:

  • Sign in to /admin as a manager (e.g. uat-manager) and, separately, as a groomer (uat-groomer).
  • At least one groomer has appointments on the test date whose clients have geocoded addresses.
# Scenario Steps Expected
TC-WEB-5.28.1 Page loads and is reachable from nav 1. Sign in as a manager. 2. Click Routes in the admin nav. URL is /admin/routes. The "Route Planner" heading, a Date picker, a Groomer selector, and an Optimize button render. No console errors.
TC-WEB-5.28.2 Leaflet map is code-split 1. Open DevTools → Network (JS filter). 2. Load /admin/reports first, confirm no RouteMap chunk loads. 3. Navigate to /admin/routes. A separate RouteMap-*.js chunk (and RouteMap-*.css) is fetched only when the Routes page renders, not on other admin pages.
TC-WEB-5.28.3 Map shows numbered pins + polyline Select a groomer + date that has a built route with ≥2 geocoded stops. The OSM map renders with one numbered pin per stop (1, 2, 3…) and a polyline connecting them in order. Tile attribution to OpenStreetMap is visible.
TC-WEB-5.28.4 Stop-list panel cards Inspect the panel beside the map. Each stop card shows the stop number, client name, appointment time, address, and travel time from the previous stop (stop 1 shows "Start of route").
TC-WEB-5.28.5 Summary + status badge Inspect the summary bar and badge. Stops count, total travel time, and total distance (km) are shown. A status badge reads one of Draft / Optimized / In progress / Completed matching the route's status.
TC-WEB-5.28.6 Optimize button Click Optimize. A POST /api/routes/optimize with { staffId, date } fires. On success the map, stop order, summary, and status badge refresh. Any skipped (non-geocoded) clients surface as a warning.
TC-WEB-5.28.7 Groomer role auto-filter Sign in as a groomer and open /admin/routes. No groomer selector is shown. The page loads the signed-in groomer's own route for the selected date. The groomer cannot view another groomer's route.
TC-WEB-5.28.8 Empty / no-route state Select a date with no appointments. The map area and stop panel show a friendly empty state ("No stops…"). No crash; Optimize is still clickable.

5.29 Route Planner — Drag-to-Reorder & Re-optimize (GRO-2159)

The stop-list panel is drag-sortable (@dnd-kit). Each stop card has a grab handle (⠿). Dropping a stop in a new position calls PATCH /api/routes/:routeId/reorder with { stopOrder: [routeStopId…] } (full first-to-last order); the UI updates optimistically and rolls back on error. The server recomputes per-leg travel, buffers, totals and tight-schedule conflict flags, and the panel/map/summary adopt the response. A "tight schedule" warning is shown on any stop whose gap is shorter than its travel + buffer. After a manual reorder a hint with a Re-optimize button appears (re-runs POST /api/routes/optimize). Drag works via mouse (desktop), press-and-hold touch (mobile groomers), and keyboard (focus handle → Space → arrows → Space).

Test Case Description Steps Expected Result
TC-WEB-5.29.1 Drag handle present Open /admin/routes for a route with ≥2 stops. Each stop card shows a grab handle (⠿) with an accessible label "Drag to reorder ".
TC-WEB-5.29.2 Reorder persists Drag a stop to a new position and drop it. A PATCH /api/routes/:routeId/reorder fires with the new stopOrder (every stop id once, new order). Stop numbers, the map polyline order, and travel-from-previous labels refresh to match.
TC-WEB-5.29.3 Optimistic update + rollback Simulate a failing reorder (e.g. server returns an error / offline). The list shows the new order immediately, then reverts to the prior order when the PATCH fails, and an error message is shown. No stuck/partial order.
TC-WEB-5.29.4 Tight-schedule warning re-evaluated Reorder so two stops are too close together. The affected stop card shows "⚠ Tight schedule — travel + buffer may exceed the gap" (red border) after the server recomputes; warnings clear on a roomier order.
TC-WEB-5.29.5 Re-optimize button After a manual drag reorder, locate the hint banner. A "Stops reordered manually…" hint with a Re-optimize button appears. Clicking it fires POST /api/routes/optimize and the hint clears once the optimized route loads. The hint is absent before any manual reorder.
TC-WEB-5.29.6 Touch / mobile drag On a touch device (or mobile emulation), press-and-hold a stop's handle (~200ms) then drag. The stop lifts and can be dropped in a new position; page scroll is not hijacked by a quick swipe. Reorder persists as in 5.29.2.
TC-WEB-5.29.7 Groomer reorders own route Sign in as a groomer, reorder stops on the own route. Reorder succeeds (groomer is authorized for their own route).

5.30 Route Planner — Navigation Export & Offline (GRO-2160)

When a route has stops, an export panel offers Open in Google Maps and Open in Apple Maps buttons. Each fetches GET /api/routes/:routeId/export/google-maps (or /apple-maps) and opens the returned deep-link URL in the device's maps app (Google Maps https://www.google.com/maps/dir/?..., Apple Maps maps://...). The page detects the device OS (iOS / Android / desktop) and renders the most relevant button prominently (filled) with the other as a secondary outline button; on iOS Apple Maps leads, otherwise Google Maps leads. Offline support: the existing Workbox NetworkFirst rule caches /api/routes/* responses (24h TTL) so a previously-loaded route still renders without network; a CacheFirst rule (osm-tiles, 7-day TTL, 400 entries) caches OpenStreetMap tiles. On every route load and after each optimize/reorder, the page pre-warms the OSM tiles covering the route's bounding box (zooms 1214, capped at 80 tiles) so the map is viewable offline. The layout is responsive: below 768px the map/stop-list stack to one column, the map shrinks, and the export buttons go full-width.

Test Case Description Steps Expected Result
TC-WEB-5.30.1 Export buttons render Open /admin/routes for a route with ≥1 stop. An export panel shows both Open in Google Maps and Open in Apple Maps buttons. Buttons are absent when there are no stops.
TC-WEB-5.30.2 Google Maps deep link Click Open in Google Maps. A GET /api/routes/:routeId/export/google-maps fires and the returned https://www.google.com/maps/dir/?... URL opens (new tab / Google Maps app) with origin, destination, and waypoints in route order.
TC-WEB-5.30.3 Apple Maps deep link On iOS (or emulation), click Open in Apple Maps. A GET /api/routes/:routeId/export/apple-maps fires and the returned maps://... URL opens Apple Maps with the route chained +to:.
TC-WEB-5.30.4 Platform-aware prominence Open the page on an iPhone (or iOS UA emulation) vs Android/desktop. On iOS the Apple Maps button is the prominent (filled) one and Google Maps is the secondary (outline); on Android/desktop Google Maps is prominent and Apple Maps secondary. Both buttons are always available.
TC-WEB-5.30.5 Export error handling Trigger an export that errors (e.g. route exceeds the platform waypoint cap). The pre-opened tab is closed and an inline error message is shown; no silent failure.
TC-WEB-5.30.6 Offline route data Load a route online, then in DevTools → Network set Offline and reload /admin/routes for the same groomer/date. The route data still loads from the api-cache (NetworkFirst fallback); stops, summary, and badge render without network.
TC-WEB-5.30.7 Offline map tiles After viewing/optimizing a route online, go Offline and view the same route. The OSM map tiles for the route area render from the osm-tiles CacheFirst cache (pre-warmed); the map is not blank in the route's vicinity.
TC-WEB-5.30.8 Responsive mobile layout Open the page at a phone width (≤768px, e.g. 390px). Map and stop-list stack into a single column, the map height shrinks, and the export buttons span full width. No horizontal scroll; controls remain usable with a thumb.

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")