0361b84bd5
- Create PetProfileCard fetching from GET /api/pets/:id/profile-summary - Displays: pet photo/name/breed/age/weight, coat type badge, temperament score (1-5 dots) + flag badges, medical alerts (severity-colored), preferred cuts, recent visits, next appointment - Loading skeleton and error/empty states - Integrate into Appointments booking form after pet selection - Integrate into ClientDetailPage as expandable card per pet - Export PetProfileSummary + NextAppointment types in @groombook/types - Add PetProfileCard tests covering full data, empty data, loading, error Co-Authored-By: Paperclip <noreply@paperclip.ing>
158 lines
5.3 KiB
TypeScript
158 lines
5.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { render, screen, waitFor, within } from "@testing-library/react";
|
|
import { PetProfileCard } from "../components/PetProfileCard.js";
|
|
|
|
const FULL_SUMMARY = {
|
|
id: "pet-1",
|
|
name: "Buddy",
|
|
breed: "Golden Retriever",
|
|
dateOfBirth: "2022-03-15",
|
|
weightKg: 30.5,
|
|
coatType: "double",
|
|
temperamentScore: 4,
|
|
temperamentFlags: ["anxious", "friendly"],
|
|
medicalAlerts: [
|
|
{ id: "a1", type: "allergy", description: "Chicken allergy", severity: "high" },
|
|
{ id: "a2", type: "condition", description: "Hip dysplasia", severity: "medium" },
|
|
],
|
|
preferredCuts: ["teddy bear", "puppy cut"],
|
|
recentVisits: [
|
|
{ id: "v1", petId: "pet-1", appointmentId: "appt-1", staffId: "staff-1", cutStyle: "teddy bear", productsUsed: "oat shampoo", notes: "Good boy", groomedAt: "2025-05-01T10:00:00Z", createdAt: "2025-05-01T10:00:00Z" },
|
|
{ id: "v2", petId: "pet-1", appointmentId: "appt-2", staffId: "staff-2", cutStyle: "puppy cut", productsUsed: null, notes: null, groomedAt: "2025-04-01T10:00:00Z", createdAt: "2025-04-01T10:00:00Z" },
|
|
],
|
|
nextAppointment: { id: "appt-3", startTime: "2025-06-01T09:00:00Z", serviceName: "Full groom" },
|
|
};
|
|
|
|
const EMPTY_SUMMARY = {
|
|
id: "pet-2",
|
|
name: "Whiskers",
|
|
breed: null,
|
|
dateOfBirth: null,
|
|
weightKg: null,
|
|
coatType: null,
|
|
temperamentScore: null,
|
|
temperamentFlags: [],
|
|
medicalAlerts: [],
|
|
preferredCuts: [],
|
|
recentVisits: [],
|
|
nextAppointment: null,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
describe("PetProfileCard", () => {
|
|
it("shows loading skeleton while fetching", () => {
|
|
global.fetch = vi.fn(() => new Promise(() => {})) as unknown as typeof fetch;
|
|
render(<PetProfileCard petId="pet-1" />);
|
|
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
|
});
|
|
|
|
it("renders full profile data correctly", async () => {
|
|
global.fetch = vi.fn(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => FULL_SUMMARY,
|
|
} as Response)
|
|
) as unknown as typeof fetch;
|
|
|
|
render(<PetProfileCard petId="pet-1" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Buddy")).toBeInTheDocument();
|
|
});
|
|
expect(screen.getByText("Golden Retriever")).toBeInTheDocument();
|
|
// age computed from DOB
|
|
expect(screen.getByText(/yr/)).toBeInTheDocument();
|
|
// weight
|
|
expect(screen.getByText(/30.5 kg/)).toBeInTheDocument();
|
|
// coat type badge
|
|
expect(screen.getByText("double")).toBeInTheDocument();
|
|
// medical alerts
|
|
expect(screen.getByText("Chicken allergy")).toBeInTheDocument();
|
|
expect(screen.getByText("Hip dysplasia")).toBeInTheDocument();
|
|
// preferred cuts
|
|
expect(screen.getByText("teddy bear")).toBeInTheDocument();
|
|
});
|
|
|
|
it("displays severity-colored badges for medical alerts", async () => {
|
|
global.fetch = vi.fn(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => FULL_SUMMARY,
|
|
} as Response)
|
|
) as unknown as typeof fetch;
|
|
|
|
render(<PetProfileCard petId="pet-1" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Chicken allergy")).toBeInTheDocument();
|
|
});
|
|
|
|
const highAlert = screen.getByText("Chicken allergy").closest("span");
|
|
expect(highAlert).toHaveStyle({ color: "#dc2626" }); // high = red
|
|
|
|
const mediumAlert = screen.getByText("Hip dysplasia").closest("span");
|
|
expect(mediumAlert).toHaveStyle({ color: "#d97706" }); // medium = amber
|
|
});
|
|
|
|
it("handles empty pet data gracefully", async () => {
|
|
global.fetch = vi.fn(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => EMPTY_SUMMARY,
|
|
} as Response)
|
|
) as unknown as typeof fetch;
|
|
|
|
render(<PetProfileCard petId="pet-2" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText("Whiskers")).toBeInTheDocument();
|
|
});
|
|
// no section labels with no data
|
|
expect(screen.queryByText(/medical alerts/i)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(/preferred cuts/i)).not.toBeInTheDocument();
|
|
expect(screen.queryByText(/recent visits/i)).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("shows error message on fetch failure", async () => {
|
|
global.fetch = vi.fn(() => Promise.reject(new Error("network error"))) as unknown as typeof fetch;
|
|
|
|
render(<PetProfileCard petId="pet-1" />);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText(/failed to load/i)).toBeInTheDocument();
|
|
});
|
|
});
|
|
|
|
it("refetches when petId changes", async () => {
|
|
const fetchMock = vi.fn((url: string) => {
|
|
if ((url as string).includes("pet-1")) {
|
|
return Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ ...FULL_SUMMARY, name: "Buddy" }),
|
|
} as Response);
|
|
}
|
|
return Promise.resolve({
|
|
ok: true,
|
|
status: 200,
|
|
json: async () => ({ ...EMPTY_SUMMARY, name: "Whiskers" }),
|
|
} as Response);
|
|
}) as unknown as typeof fetch;
|
|
global.fetch = fetchMock;
|
|
|
|
const { rerender } = render(<PetProfileCard petId="pet-1" />);
|
|
await waitFor(() => expect(screen.getByText("Buddy")).toBeInTheDocument());
|
|
expect(fetchMock).toHaveBeenCalledWith("/api/pets/pet-1/profile-summary");
|
|
|
|
rerender(<PetProfileCard petId="pet-2" />);
|
|
await waitFor(() => expect(screen.getByText("Whiskers")).toBeInTheDocument());
|
|
expect(fetchMock).toHaveBeenCalledWith("/api/pets/pet-2/profile-summary");
|
|
});
|
|
});
|