570 lines
59 KiB
Markdown
570 lines
59 KiB
Markdown
# 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.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 `Appointments` → **Past** 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_show` → `no-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 | 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, <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 from SSO bridge routes to OOBE (GRO-2359) | 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 post-auth handler mounts the **OOBE** (`src/portal/OOBE.tsx`) — a centred card titled **"Welcome — let's set up your portal"** with form fields for name (prefilled from the Better Auth session), phone, address, and notes. The legacy "Portal access not configured" card is **not** rendered on the new-user path. No redirect loop, no portal chrome. |
|
||
| TC-WEB-5.25.6 | OOBE form submission creates the portal (GRO-2359) | From TC-WEB-5.25.5, fill in the OOBE form and click **Create my portal**. | `POST /api/portal/clients-from-auth` is called with `{ name, phone, address, notes }`; the email is taken from the Better Auth session (the API binds the new client row to the SSO identity). The page reloads to `/`, the bridge re-runs, and the user lands in their portal dashboard. DevTools → Network shows `POST /api/portal/clients-from-auth` → 201 followed by `POST /api/portal/session-from-auth` → 201. |
|
||
| TC-WEB-5.25.6b | OOBE handles portal selection (409 from clients-from-auth) (GRO-2359) | 1. Sign in via SSO with an email that already exists in `clients` (e.g. a previously deleted-then-recreated account). 2. Land on OOBE. 3. Click **Create my portal**. | The API returns 409 "A customer record with this email already exists". The OOBE re-enables the submit button and shows the portal-selection message: "A customer record with this email already exists. Please contact your groomer to link your account." The shared signOut() button remains reachable so the user can exit if needed. |
|
||
| TC-WEB-5.25.6c | OOBE uses the shared signOut() handler (GRO-2358, GRO-2359) | From TC-WEB-5.25.5, click **Sign out** in the OOBE footer. | The same shared `signOut()` from `lib/auth-client` fires (same handler as `AdminLayout` and the no-access card); browser navigates to `/login`; the Authentik session cookie is cleared. The handler always navigates to `/login` — even if the network call to `/api/auth/sign-out` fails — so a transient auth-server hiccup never leaves the user trapped on an authenticated screen. |
|
||
| TC-WEB-5.25.6d | OOBE is mountable from a direct deep-link (GRO-2359) | 1. Sign in via SSO as any customer. 2. In a new tab, navigate to `https://uat.groombook.dev/onboarding`. | The OOBE form mounts (the App.tsx `/onboarding` route resolves before the CustomerPortal `!sessionId` guards). The submit, signOut, and field-validation behaviour are identical to the post-auth mount. |
|
||
| TC-WEB-5.25.6e | Deleted-portal deep-link still reaches the no-access card (GRO-2358, GRO-2359) | 1. Sign in via SSO as a customer whose `clients` row was disabled/deleted by the groomer. 2. Land on a portal sub-route with `?noAccess=deleted-portal` (e.g. visit `https://uat.groombook.dev/appointments?noAccess=deleted-portal` directly). | The no-access card renders (the deep-link deleted-portal case — the OOBE is reserved for first-time creation). The shared signOut() from GRO-2358 is wired identically. This proves the no-access card is still reachable for non-new-user failure modes and the CMPO "no-trap" invariant holds across the auth boundary. |
|
||
| TC-WEB-5.25.6f | In-portal chrome sidebar exposes a Sign out button (GRO-2373) | 1. Complete TC-WEB-5.25.1 (SSO sign-in as a customer). 2. From the portal chrome, look at the sidebar footer (the section below the navigation links, where "Customer Portal v1.0" sits). 3. Locate the **Sign out** button (a stone-grey button above the version label, with a LogOut icon). 4. Click it. | A **Sign out** button is present in the sidebar footer (not buried in the Settings page, not hidden in a dropdown — it's visible on every portal sub-route, including Home, Appointments, My Pets, Report Cards, Billing, Messages, Settings). Clicking it fires the same shared `signOut()` from `lib/auth-client` (same handler as the OOBE footer, the no-access card, and `AdminLayout`'s top-bar "Logout"); `POST /api/auth/sign-out` → 200 `{"success":true}`; the browser navigates to `/login`; the Better Auth / Authentik session cookie is cleared. Proves the CMPO "no-trap" invariant (originally established in GRO-2355) holds on the third authenticated surface — the in-portal chrome — which the GRO-2358 P1 fix did not cover. |
|
||
| 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 <client>". |
|
||
| 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 12–14, 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") |