Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a30b4bd44 | |||
| 90f6a30b74 | |||
| 727e1db632 |
+114
-64
@@ -20,125 +20,175 @@ GroomBook Web is the React 19 PWA frontend for the GroomBook pet grooming manage
|
|||||||
- GroomBook API service is running and healthy
|
- GroomBook API service is running and healthy
|
||||||
- Required test data exists (clients, pets, appointments, services, staff)
|
- Required test data exists (clients, pets, appointments, services, staff)
|
||||||
|
|
||||||
## 4. Test Cases
|
## 4. Auth Base URL Resolution
|
||||||
|
|
||||||
### 4.1 Authentication UI
|
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 |
|
| # | Scenario | Steps | Expected |
|
||||||
|---|----------|-------|----------|
|
|---|----------|-------|----------|
|
||||||
| TC-WEB-4.1.1 | Login page loads | Navigate to UAT URL | Login form is displayed with OIDC provider button(s) |
|
| TC-WEB-5.1.1 | Login page loads | Navigate to UAT URL | Login form is displayed with OIDC provider button(s) |
|
||||||
| TC-WEB-4.1.2 | OIDC redirect | Click OIDC login button | Redirected to OIDC provider, then back to app with session established |
|
| TC-WEB-5.1.2 | OIDC redirect | Click OIDC login button | Redirected to OIDC provider, then back to app with session established |
|
||||||
| TC-WEB-4.1.3 | Logout | Click logout button | Session cleared, redirected to login page |
|
| TC-WEB-5.1.3 | Logout | Click logout button | Session cleared, redirected to login page |
|
||||||
| TC-WEB-4.1.4 | Session indicator | After successful login | User info/initials visible in UI indicating active session |
|
| TC-WEB-5.1.4 | Session indicator | After successful login | User info/initials visible in UI indicating active session |
|
||||||
|
|
||||||
### 4.2 Dashboard
|
### 5.2 Authentication — VITE_API_URL Set
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|---|----------|-------|----------|
|
|---|----------|-------|----------|
|
||||||
| TC-WEB-4.2.1 | Dashboard loads after login | Complete authentication | Dashboard page loads without errors |
|
| 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-WEB-4.2.2 | Key metrics visible | View dashboard | Revenue, appointments, clients, and other key metrics displayed |
|
| 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-WEB-4.2.3 | No blank state | On fresh login | Dashboard shows meaningful data, not empty/blank state |
|
| 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 |
|
||||||
|
|
||||||
### 4.3 Client Management UI
|
### 5.3 Authentication — VITE_API_URL Unset (Fallback)
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|---|----------|-------|----------|
|
|---|----------|-------|----------|
|
||||||
| TC-WEB-4.3.1 | Client list loads | Navigate to Clients section | List of clients is displayed |
|
| 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-WEB-4.3.2 | Create client | Click "New Client", fill form, submit | Client created successfully, appears in list |
|
| 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-WEB-4.3.3 | Edit client | Click on client, modify details, save | Client updated successfully |
|
| 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-WEB-4.3.4 | Search clients | Enter search term in search box | List filters to matching clients |
|
|
||||||
| TC-WEB-4.3.5 | Archive client | Click archive on client record | Client marked as archived, removed from active list |
|
|
||||||
|
|
||||||
### 4.4 Pet Management UI
|
### 5.4 Session Persistence
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|---|----------|-------|----------|
|
|---|----------|-------|----------|
|
||||||
| TC-WEB-4.4.1 | Pet profiles visible | Open client details | All pets for client displayed with basic info |
|
| TC-AUTH-5.4.1 | Session persists across page reload | Sign in, reload page | Session remains active |
|
||||||
| TC-WEB-4.4.2 | Add pet | Click "Add Pet", fill form, submit | Pet created and linked to client |
|
| TC-AUTH-5.4.2 | Session clears on sign-out | Sign in, sign out | User is logged out, redirected to login |
|
||||||
| TC-WEB-4.4.3 | Edit pet details | Click on pet, modify details, save | Pet updated successfully |
|
|
||||||
| TC-WEB-4.4.4 | Grooming history view | View pet profile | Past appointments/grooming sessions displayed |
|
|
||||||
|
|
||||||
### 4.5 Appointment Scheduling UI
|
### 5.5 Dashboard
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|---|----------|-------|----------|
|
|---|----------|-------|----------|
|
||||||
| TC-WEB-4.5.1 | Calendar view loads | Navigate to Appointments | Calendar view displays appointments |
|
| TC-WEB-5.5.1 | Dashboard loads after login | Complete authentication | Dashboard page loads without errors |
|
||||||
| TC-WEB-4.5.2 | Create booking | Click "New Appointment", fill details, submit | Appointment created and appears on calendar |
|
| TC-WEB-5.5.2 | Key metrics visible | View dashboard | Revenue, appointments, clients, and other key metrics displayed |
|
||||||
| TC-WEB-4.5.3 | Modify appointment | Click on appointment, change details, save | Appointment updated successfully |
|
| TC-WEB-5.5.3 | No blank state | On fresh login | Dashboard shows meaningful data, not empty/blank state |
|
||||||
| TC-WEB-4.5.4 | Cancel appointment | Click cancel on appointment | Appointment marked as cancelled |
|
|
||||||
| TC-WEB-4.5.5 | Appointment groups | View grouped appointments | Related appointments display as group |
|
|
||||||
|
|
||||||
### 4.6 Service Management UI
|
### 5.6 Client Management UI
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|---|----------|-------|----------|
|
|---|----------|-------|----------|
|
||||||
| TC-WEB-4.6.1 | Service catalog loads | Navigate to Services | List of available services displayed |
|
| TC-WEB-5.6.1 | Client list loads | Navigate to Clients section | List of clients is displayed |
|
||||||
| TC-WEB-4.6.2 | Create service | Click "New Service", fill form, submit | Service created successfully |
|
| TC-WEB-5.6.2 | Create client | Click "New Client", fill form, submit | Client created successfully, appears in list |
|
||||||
| TC-WEB-4.6.3 | Edit service | Click on service, modify details, save | Service updated successfully |
|
| 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 |
|
||||||
|
|
||||||
### 4.7 Staff Management UI
|
### 5.7 Pet Management UI
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|---|----------|-------|----------|
|
|---|----------|-------|----------|
|
||||||
| TC-WEB-4.7.1 | Staff list loads | Navigate to Staff | List of staff members displayed |
|
| TC-WEB-5.7.1 | Pet profiles visible | Open client details | All pets for client displayed with basic info |
|
||||||
| TC-WEB-4.7.2 | Role display | View staff member | Staff role/permissions clearly visible |
|
| 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 |
|
||||||
|
|
||||||
### 4.8 Invoicing & Payments UI
|
### 5.8 Appointment Scheduling UI
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|---|----------|-------|----------|
|
|---|----------|-------|----------|
|
||||||
| TC-WEB-4.8.1 | Invoice list loads | Navigate to Invoices | List of invoices displayed with status |
|
| TC-WEB-5.8.1 | Calendar view loads | Navigate to Appointments | Calendar view displays appointments |
|
||||||
| TC-WEB-4.8.2 | Payment flow | Click "Pay" on unpaid invoice, complete payment | Payment processed, invoice marked as paid |
|
| TC-WEB-5.8.2 | Create booking | Click "New Appointment", fill details, submit | Appointment created and appears on calendar |
|
||||||
| TC-WEB-4.8.3 | Receipts view | View paid invoice | Receipt/payment details displayed |
|
| 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 |
|
||||||
|
|
||||||
### 4.9 Customer Portal UI
|
### 5.9 Service Management UI
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|---|----------|-------|----------|
|
|---|----------|-------|----------|
|
||||||
| TC-WEB-4.9.1 | Client-facing view | Log in as client persona | Customer portal UI displayed |
|
| TC-WEB-5.9.1 | Service catalog loads | Navigate to Services | List of available services displayed |
|
||||||
| TC-WEB-4.9.2 | Appointment list | View client portal appointments | List of client's appointments visible |
|
| TC-WEB-5.9.2 | Create service | Click "New Service", fill form, submit | Service created successfully |
|
||||||
| TC-WEB-4.9.3 | Confirm appointment | Click confirm on pending appointment | Appointment status updated to confirmed |
|
| TC-WEB-5.9.3 | Edit service | Click on service, modify details, save | Service updated successfully |
|
||||||
| TC-WEB-4.9.4 | Cancel appointment | Click cancel on appointment | Appointment marked as cancelled |
|
|
||||||
|
|
||||||
### 4.10 Reports UI
|
### 5.10 Staff Management UI
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|---|----------|-------|----------|
|
|---|----------|-------|----------|
|
||||||
| TC-WEB-4.10.1 | Revenue charts | Navigate to Reports | Revenue charts display with data |
|
| TC-WEB-5.10.1 | Staff list loads | Navigate to Staff | List of staff members displayed |
|
||||||
| TC-WEB-4.10.2 | Utilization graphs | View reports | Staff/resource utilization graphs visible |
|
| TC-WEB-5.10.2 | Role display | View staff member | Staff role/permissions clearly visible |
|
||||||
|
|
||||||
### 4.11 Settings UI
|
### 5.11 Invoicing & Payments UI
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|---|----------|-------|----------|
|
|---|----------|-------|----------|
|
||||||
| TC-WEB-4.11.1 | Configuration page | Navigate to Settings | Settings page loads without errors |
|
| TC-WEB-5.11.1 | Invoice list loads | Navigate to Invoices | List of invoices displayed with status |
|
||||||
| TC-WEB-4.11.2 | Form interactions | Modify settings, save | Settings saved successfully, changes reflected |
|
| 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 |
|
||||||
|
|
||||||
### 4.12 Navigation
|
### 5.12 Customer Portal UI
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|---|----------|-------|----------|
|
|---|----------|-------|----------|
|
||||||
| TC-WEB-4.12.1 | Sidebar/menu links | Click navigation items | Each section loads correctly |
|
| TC-WEB-5.12.1 | Client-facing view | Log in as client persona | Customer portal UI displayed |
|
||||||
| TC-WEB-4.12.2 | All sections reachable | Navigate through all menu items | All sections accessible, no 404 errors |
|
| TC-WEB-5.12.2 | Appointment list loads | Sign in as `uat-customer@groombook.dev`, open **Appointments** | List of the customer's appointments renders. **No** "Failed to load appointments" error and **no** Retry button. (GRO-2180) |
|
||||||
| TC-WEB-4.12.3 | No broken links | Test all navigation paths | All links work, no broken routes |
|
| TC-WEB-5.12.3 | Date/time display | Inspect each appointment card | Each card shows a human-readable date and time derived from the API `startTime` (e.g. "Mon, Jun 1, 2026" / "10:00 AM"); no `undefined` or blank date/time. (GRO-2180) |
|
||||||
|
| TC-WEB-5.12.4 | Book New reachable | On the loaded Appointments view (non-readonly), look for the **Book New** button | "Book New" button is visible and opens the booking modal. (GRO-2180) |
|
||||||
|
| TC-WEB-5.12.5 | Upcoming/Past split | Toggle the **Upcoming** and **Past** tabs | Future appointments appear under Upcoming; completed/cancelled/past appear under Past. (GRO-2180) |
|
||||||
|
| TC-WEB-5.12.6 | Confirm appointment | Click confirm on pending appointment | Appointment status updated to confirmed |
|
||||||
|
| TC-WEB-5.12.7 | Cancel appointment | Click cancel on appointment | Appointment marked as cancelled |
|
||||||
|
| TC-WEB-5.12.8 | Reschedule display | Open **Reschedule** on an upcoming appointment | Summary header shows the current appointment's date and time (from `startTime`); no `undefined`. (GRO-2180) |
|
||||||
|
|
||||||
### 4.13 Mobile / PWA
|
### 5.13 Reports UI
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|---|----------|-------|----------|
|
|---|----------|-------|----------|
|
||||||
| TC-WEB-4.13.1 | Responsive at 390x844 | Resize viewport to mobile dimensions | Layout adapts correctly, no horizontal scroll |
|
| TC-WEB-5.13.1 | Revenue charts | Navigate to Reports | Revenue charts display with data |
|
||||||
| TC-WEB-4.13.2 | PWA install prompt | Load app on supported browser | Install prompt appears when criteria met |
|
| TC-WEB-5.13.2 | Utilization graphs | View reports | Staff/resource utilization graphs visible |
|
||||||
| TC-WEB-4.13.3 | Touch interactions | Use touch gestures on mobile | All interactions work with touch input |
|
|
||||||
|
|
||||||
### 4.14 Error & Empty States
|
### 5.14 Settings UI
|
||||||
|
|
||||||
| # | Scenario | Steps | Expected |
|
| # | Scenario | Steps | Expected |
|
||||||
|---|----------|-------|----------|
|
|---|----------|-------|----------|
|
||||||
| TC-WEB-4.14.1 | Form validation | Submit form with invalid data | Appropriate validation errors displayed |
|
| TC-WEB-5.14.1 | Configuration page | Navigate to Settings | Settings page loads without errors |
|
||||||
| TC-WEB-4.14.2 | Missing data | Navigate to section with no data | Empty state message displayed, not blank page |
|
| TC-WEB-5.14.2 | Form interactions | Modify settings, save | Settings saved successfully, changes reflected |
|
||||||
| TC-WEB-4.14.3 | Error boundaries | Trigger error condition | Friendly error message displayed, app doesn't crash |
|
|
||||||
|
|
||||||
## 5. Pass/Fail Criteria
|
### 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 |
|
||||||
|
|
||||||
|
## 6. Pass/Fail Criteria
|
||||||
|
|
||||||
**Pass:**
|
**Pass:**
|
||||||
- All test cases execute without errors
|
- All test cases execute without errors
|
||||||
@@ -152,7 +202,7 @@ GroomBook Web is the React 19 PWA frontend for the GroomBook pet grooming manage
|
|||||||
- Screenshot or screen recording of failure
|
- Screenshot or screen recording of failure
|
||||||
- Error details from browser console or network tab
|
- Error details from browser console or network tab
|
||||||
|
|
||||||
## 6. Update Policy
|
## 7. Update Policy
|
||||||
|
|
||||||
**Any PR that changes user-facing behaviour MUST update this file.**
|
**Any PR that changes user-facing behaviour MUST update this file.**
|
||||||
|
|
||||||
@@ -161,4 +211,4 @@ When modifying the GroomBook Web application in ways that affect the user interf
|
|||||||
2. Add new test cases for new features or flows
|
2. Add new test cases for new features or flows
|
||||||
3. Modify existing test cases if behaviour changes
|
3. Modify existing test cases if behaviour changes
|
||||||
4. Remove test cases for deprecated features
|
4. Remove test cases for deprecated features
|
||||||
5. Reference the updated section(s) in the PR description (e.g., "Updated UAT_PLAYBOOK.md §4.5 — new appointment group feature")
|
5. Reference the updated section(s) in the PR description (e.g., "Updated UAT_PLAYBOOK.md §5.5 — new appointment group feature")
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||||
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
||||||
import { parseTimeTo24Hour, isUpcoming, CustomerNotesSection, ConfirmationSection } from "../portal/sections/Appointments.tsx";
|
import { parseTimeTo24Hour, isUpcoming, getAppointmentStart, formatAppointmentDate, formatAppointmentTime, CustomerNotesSection, ConfirmationSection } from "../portal/sections/Appointments.tsx";
|
||||||
|
|
||||||
const UPCOMING_APPT = {
|
const UPCOMING_APPT = {
|
||||||
id: "appt-1",
|
id: "appt-1",
|
||||||
@@ -29,6 +29,26 @@ const PAST_APPT = {
|
|||||||
status: "completed" as const,
|
status: "completed" as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// GRO-2180: the /api/portal/appointments contract returns ISO startTime/endTime
|
||||||
|
// and no date/time fields. These fixtures mirror that shape exactly.
|
||||||
|
const API_UPCOMING_APPT = {
|
||||||
|
id: "appt-api-1",
|
||||||
|
petId: "pet-1",
|
||||||
|
serviceId: "service-1",
|
||||||
|
groomerId: null,
|
||||||
|
startTime: "2099-01-01T10:00:00.000Z",
|
||||||
|
endTime: "2099-01-01T10:45:00.000Z",
|
||||||
|
status: "confirmed" as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
const API_PAST_APPT = {
|
||||||
|
...API_UPCOMING_APPT,
|
||||||
|
id: "appt-api-2",
|
||||||
|
startTime: "2020-01-01T10:00:00.000Z",
|
||||||
|
endTime: "2020-01-01T10:45:00.000Z",
|
||||||
|
status: "completed" as const,
|
||||||
|
};
|
||||||
|
|
||||||
describe("parseTimeTo24Hour", () => {
|
describe("parseTimeTo24Hour", () => {
|
||||||
it("converts AM times correctly", () => {
|
it("converts AM times correctly", () => {
|
||||||
expect(parseTimeTo24Hour("9:00 AM")).toBe("09:00:00");
|
expect(parseTimeTo24Hour("9:00 AM")).toBe("09:00:00");
|
||||||
@@ -42,6 +62,13 @@ describe("parseTimeTo24Hour", () => {
|
|||||||
expect(parseTimeTo24Hour("11:00 PM")).toBe("23:00:00");
|
expect(parseTimeTo24Hour("11:00 PM")).toBe("23:00:00");
|
||||||
expect(parseTimeTo24Hour("12:00 PM")).toBe("12:00:00");
|
expect(parseTimeTo24Hour("12:00 PM")).toBe("12:00:00");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GRO-2180 regression: must not throw on undefined/empty input.
|
||||||
|
it("returns a safe default for missing input", () => {
|
||||||
|
expect(() => parseTimeTo24Hour(undefined)).not.toThrow();
|
||||||
|
expect(parseTimeTo24Hour(undefined)).toBe("00:00:00");
|
||||||
|
expect(parseTimeTo24Hour("")).toBe("00:00:00");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("isUpcoming", () => {
|
describe("isUpcoming", () => {
|
||||||
@@ -60,6 +87,63 @@ describe("isUpcoming", () => {
|
|||||||
it("returns false for completed appointments", () => {
|
it("returns false for completed appointments", () => {
|
||||||
expect(isUpcoming({ ...UPCOMING_APPT, status: "completed" })).toBe(false);
|
expect(isUpcoming({ ...UPCOMING_APPT, status: "completed" })).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GRO-2180 regression: the API contract uses ISO startTime with no date/time.
|
||||||
|
// Previously isUpcoming threw a TypeError on this shape, breaking the page.
|
||||||
|
it("does not throw on the API startTime/endTime shape", () => {
|
||||||
|
expect(() => isUpcoming(API_UPCOMING_APPT)).not.toThrow();
|
||||||
|
expect(() => isUpcoming(API_PAST_APPT)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true for future appointments using startTime", () => {
|
||||||
|
expect(isUpcoming(API_UPCOMING_APPT)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for past appointments using startTime", () => {
|
||||||
|
expect(isUpcoming(API_PAST_APPT)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false (not throw) when neither startTime nor date is present", () => {
|
||||||
|
const { startTime, endTime, ...noDate } = API_UPCOMING_APPT;
|
||||||
|
void startTime;
|
||||||
|
void endTime;
|
||||||
|
expect(() => isUpcoming(noDate)).not.toThrow();
|
||||||
|
expect(isUpcoming(noDate)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getAppointmentStart / display helpers (GRO-2180)", () => {
|
||||||
|
it("resolves the start instant from ISO startTime", () => {
|
||||||
|
const start = getAppointmentStart(API_UPCOMING_APPT);
|
||||||
|
expect(start).not.toBeNull();
|
||||||
|
expect(start?.toISOString()).toBe("2099-01-01T10:00:00.000Z");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to legacy date + time when startTime is absent", () => {
|
||||||
|
const start = getAppointmentStart(UPCOMING_APPT);
|
||||||
|
expect(start).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when there is no usable date", () => {
|
||||||
|
const { startTime, endTime, ...noDate } = API_UPCOMING_APPT;
|
||||||
|
void startTime;
|
||||||
|
void endTime;
|
||||||
|
expect(getAppointmentStart(noDate)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("formats date/time without throwing on the API shape", () => {
|
||||||
|
expect(() => formatAppointmentDate(API_UPCOMING_APPT)).not.toThrow();
|
||||||
|
expect(() => formatAppointmentTime(API_UPCOMING_APPT)).not.toThrow();
|
||||||
|
expect(formatAppointmentDate(API_UPCOMING_APPT)).not.toBe("");
|
||||||
|
expect(formatAppointmentTime(API_UPCOMING_APPT)).not.toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty display strings when there is no usable date", () => {
|
||||||
|
const { startTime, endTime, ...noDate } = API_UPCOMING_APPT;
|
||||||
|
void startTime;
|
||||||
|
void endTime;
|
||||||
|
expect(formatAppointmentDate(noDate)).toBe("");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("CustomerNotesSection", () => {
|
describe("CustomerNotesSection", () => {
|
||||||
|
|||||||
@@ -6,8 +6,14 @@ export interface Appointment {
|
|||||||
petId: string;
|
petId: string;
|
||||||
serviceId: string;
|
serviceId: string;
|
||||||
groomerId: string | null;
|
groomerId: string | null;
|
||||||
date: string;
|
// The /api/portal/appointments contract returns ISO `startTime`/`endTime`.
|
||||||
time: string;
|
// `date`/`time` are the legacy display shape, still produced locally by some
|
||||||
|
// flows (e.g. test fixtures), so both shapes are optional and code reads
|
||||||
|
// `startTime` first, falling back to `date` + `time`.
|
||||||
|
startTime?: string;
|
||||||
|
endTime?: string;
|
||||||
|
date?: string;
|
||||||
|
time?: string;
|
||||||
status: 'scheduled' | 'confirmed' | 'pending' | 'waitlisted' | 'completed' | 'cancelled' | 'no-show';
|
status: 'scheduled' | 'confirmed' | 'pending' | 'waitlisted' | 'completed' | 'cancelled' | 'no-show';
|
||||||
petName?: string;
|
petName?: string;
|
||||||
serviceName?: string;
|
serviceName?: string;
|
||||||
@@ -61,7 +67,8 @@ export function formatDate(dateStr: string): string {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseTimeTo24Hour(time: string): string {
|
export function parseTimeTo24Hour(time: string | null | undefined): string {
|
||||||
|
if (!time) return '00:00:00';
|
||||||
const parts = time.split(' ');
|
const parts = time.split(' ');
|
||||||
const hoursMinutes = parts[0] ?? '';
|
const hoursMinutes = parts[0] ?? '';
|
||||||
const period = parts[1] ?? '';
|
const period = parts[1] ?? '';
|
||||||
@@ -74,10 +81,41 @@ export function parseTimeTo24Hour(time: string): string {
|
|||||||
return `${hours24.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:00`;
|
return `${hours24.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:00`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve an appointment's start instant from either the API contract shape
|
||||||
|
* (ISO `startTime`) or the legacy `date` + `time` shape. Returns null when no
|
||||||
|
* usable date is present or the value is unparseable, so callers never throw.
|
||||||
|
*/
|
||||||
|
export function getAppointmentStart(appt: Appointment): Date | null {
|
||||||
|
const raw = appt.startTime
|
||||||
|
? appt.startTime
|
||||||
|
: appt.date
|
||||||
|
? `${appt.date}T${parseTimeTo24Hour(appt.time)}`
|
||||||
|
: null;
|
||||||
|
if (!raw) return null;
|
||||||
|
const parsed = new Date(raw);
|
||||||
|
return isNaN(parsed.getTime()) ? null : parsed;
|
||||||
|
}
|
||||||
|
|
||||||
export function isUpcoming(appt: Appointment): boolean {
|
export function isUpcoming(appt: Appointment): boolean {
|
||||||
const now = new Date();
|
const start = getAppointmentStart(appt);
|
||||||
const apptDate = new Date(`${appt.date}T${parseTimeTo24Hour(appt.time)}`);
|
if (!start) return false;
|
||||||
return apptDate > now && appt.status !== 'cancelled' && appt.status !== 'completed';
|
return start > new Date() && appt.status !== 'cancelled' && appt.status !== 'completed';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Display date string, preferring the ISO `startTime` contract shape. */
|
||||||
|
export function formatAppointmentDate(appt: Appointment): string {
|
||||||
|
const start = getAppointmentStart(appt);
|
||||||
|
return start ? formatDate(start.toISOString()) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Display time string, preferring the ISO `startTime` contract shape. */
|
||||||
|
export function formatAppointmentTime(appt: Appointment): string {
|
||||||
|
const start = getAppointmentStart(appt);
|
||||||
|
if (start) {
|
||||||
|
return start.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
|
||||||
|
}
|
||||||
|
return appt.time ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_COLORS: Record<string, string> = {
|
const STATUS_COLORS: Record<string, string> = {
|
||||||
@@ -288,11 +326,11 @@ function AppointmentCard({
|
|||||||
<div className="flex items-center gap-3 text-xs text-stone-500 mt-0.5">
|
<div className="flex items-center gap-3 text-xs text-stone-500 mt-0.5">
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Calendar size={12} />
|
<Calendar size={12} />
|
||||||
{formatDate(appt.date)}
|
{formatAppointmentDate(appt)}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<Clock size={12} />
|
<Clock size={12} />
|
||||||
{appt.time}
|
{formatAppointmentTime(appt)}
|
||||||
</span>
|
</span>
|
||||||
<span>with {appt.groomerName || 'First Available'}</span>
|
<span>with {appt.groomerName || 'First Available'}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -646,7 +684,7 @@ export function RescheduleFlow({
|
|||||||
{appt.petName || 'Pet'} — {appt.serviceName || 'Service'}
|
{appt.petName || 'Pet'} — {appt.serviceName || 'Service'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-stone-500 mt-0.5">
|
<p className="text-stone-500 mt-0.5">
|
||||||
{formatDate(appt.date)} at {appt.time} with{' '}
|
{formatAppointmentDate(appt)} at {formatAppointmentTime(appt)} with{' '}
|
||||||
{appt.groomerName || 'First Available'}
|
{appt.groomerName || 'First Available'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user