GRO-1178: client-facing enhanced pet profile editor
- PetForm: coat type dropdown, temperament display (read-only), medical alerts editor (add/remove/severity), preferred cuts tag input - PetProfiles: Medical tab shows severity badges, Grooming tab shows coat type + preferred cuts, Basic Info tab shows temperament score/flags - PetForm.test: component tests for all new interactions - Shared types updated: MedicalAlert, CoatType, AlertSeverity added Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
import { PetForm } from "../portal/sections/PetForm.js";
|
||||
import type { Pet } from "../../../../packages/types/src/index.js";
|
||||
|
||||
const BASE_PET: Pet = {
|
||||
id: "pet-1",
|
||||
clientId: "client-1",
|
||||
name: "Buddy",
|
||||
species: "dog",
|
||||
breed: "Labrador",
|
||||
weightKg: 25,
|
||||
dateOfBirth: "2020-03-15T00:00:00.000Z",
|
||||
healthAlerts: null,
|
||||
groomingNotes: null,
|
||||
cutStyle: null,
|
||||
shampooPreference: null,
|
||||
specialCareNotes: null,
|
||||
customFields: {},
|
||||
coatType: null,
|
||||
preferredCuts: [],
|
||||
medicalAlerts: [],
|
||||
createdAt: "2024-01-01T00:00:00.000Z",
|
||||
updatedAt: "2024-01-01T00:00:00.000Z",
|
||||
};
|
||||
|
||||
describe("PetForm", () => {
|
||||
const onSave = vi.fn();
|
||||
const onCancel = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
onSave.mockClear();
|
||||
onCancel.mockClear();
|
||||
});
|
||||
|
||||
// ── Coat type ───────────────────────────────────────────────────────────────
|
||||
|
||||
it("allows coat type selection from dropdown", () => {
|
||||
render(<PetForm pet={BASE_PET} onSave={onSave} onCancel={onCancel} />);
|
||||
const select = screen.getByRole("combobox", { name: /coat type/i });
|
||||
fireEvent.change(select, { target: { value: "curly" } });
|
||||
expect((select as HTMLSelectElement).value).toBe("curly");
|
||||
});
|
||||
|
||||
it("persists coat type on save", () => {
|
||||
render(<PetForm pet={BASE_PET} onSave={onSave} onCancel={onCancel} />);
|
||||
fireEvent.change(screen.getByRole("combobox", { name: /coat type/i }), { target: { value: "double" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: /save/i }));
|
||||
expect(onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ coatType: "double" })
|
||||
);
|
||||
});
|
||||
|
||||
// ── Preferred cuts tag input ────────────────────────────────────────────────
|
||||
|
||||
it("adds a cut when Enter is pressed", () => {
|
||||
render(<PetForm pet={BASE_PET} onSave={onSave} onCancel={onCancel} />);
|
||||
const input = screen.getByPlaceholder(/type a cut name/i);
|
||||
fireEvent.change(input, { target: { value: "Puppy Cut" } });
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
expect(screen.getByText("Puppy Cut")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("adds a cut when the + button is clicked", () => {
|
||||
render(<PetForm pet={BASE_PET} onSave={onSave} onCancel={onCancel} />);
|
||||
const input = screen.getByPlaceholder(/type a cut name/i);
|
||||
fireEvent.change(input, { target: { value: "Teddy Bear" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: /add/i }));
|
||||
expect(screen.getByText("Teddy Bear")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("removes a cut when X is clicked", () => {
|
||||
const petWithCuts: Pet = {
|
||||
...BASE_PET,
|
||||
preferredCuts: ["Puppy Cut", "Teddy Bear"],
|
||||
};
|
||||
render(<PetForm pet={petWithCuts} onSave={onSave} onCancel={onCancel} />);
|
||||
const puppyCutSpans = screen.getAllByText("Puppy Cut");
|
||||
const puppyCutTag = puppyCutSpans[0].closest("span");
|
||||
const removeBtn = puppyCutTag?.querySelector("button") ?? screen.getAllByTitle("")[0];
|
||||
fireEvent.click(removeBtn);
|
||||
expect(screen.queryByText("Puppy Cut")).toBeNull();
|
||||
expect(screen.getByText("Teddy Bear")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("includes preferred cuts in save payload", () => {
|
||||
render(<PetForm pet={BASE_PET} onSave={onSave} onCancel={onCancel} />);
|
||||
fireEvent.change(screen.getByPlaceholder(/type a cut name/i), { target: { value: "Puppy Cut" } });
|
||||
fireEvent.keyDown(screen.getByPlaceholder(/type a cut name/i), { key: "Enter" });
|
||||
fireEvent.click(screen.getByRole("button", { name: /save/i }));
|
||||
expect(onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ preferredCuts: ["Puppy Cut"] })
|
||||
);
|
||||
});
|
||||
|
||||
// ── Medical alerts ───────────────────────────────────────────────────────────
|
||||
|
||||
it("adds a medical alert", () => {
|
||||
render(<PetForm pet={BASE_PET} onSave={onSave} onCancel={onCancel} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: /add alert/i }));
|
||||
expect(screen.getByPlaceholder(/alert type/i)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("removes a medical alert", () => {
|
||||
const petWithAlert: Pet = {
|
||||
...BASE_PET,
|
||||
medicalAlerts: [{ id: "alert-1", type: "Allergic to chicken", description: "Causes hives", severity: "high" }],
|
||||
};
|
||||
render(<PetForm pet={petWithAlert} onSave={onSave} onCancel={onCancel} />);
|
||||
const removeBtn = screen.getAllByRole("button", { name: "" })[0];
|
||||
fireEvent.click(removeBtn);
|
||||
expect(screen.queryByText("Allergic to chicken")).toBeNull();
|
||||
});
|
||||
|
||||
it("validates alert type is non-empty", () => {
|
||||
render(<PetForm pet={BASE_PET} onSave={onSave} onCancel={onCancel} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: /add alert/i }));
|
||||
fireEvent.click(screen.getByRole("button", { name: /save/i }));
|
||||
expect(screen.getByText(/type is required/i)).toBeTruthy();
|
||||
expect(onSave).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows medical alerts in save payload", () => {
|
||||
const petWithAlert: Pet = {
|
||||
...BASE_PET,
|
||||
medicalAlerts: [{ id: "alert-1", type: "Sensitive skin", description: "Use hypoallergenic shampoo only", severity: "medium" }],
|
||||
};
|
||||
render(<PetForm pet={petWithAlert} onSave={onSave} onCancel={onCancel} />);
|
||||
fireEvent.click(screen.getByRole("button", { name: /save/i }));
|
||||
expect(onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
medicalAlerts: expect.arrayContaining([
|
||||
expect.objectContaining({ type: "Sensitive skin", severity: "medium" }),
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// ── Temperament read-only display ─────────────────────────────────────────────
|
||||
|
||||
it("displays temperament score as read-only stars", () => {
|
||||
const petWithTemperament: Pet = {
|
||||
...BASE_PET,
|
||||
temperamentScore: 4,
|
||||
temperamentFlags: ["Anxious", "Good with kids"],
|
||||
};
|
||||
render(<PetForm pet={petWithTemperament} onSave={onSave} onCancel={onCancel} />);
|
||||
expect(screen.getByText("(/4/5)")).toBeTruthy();
|
||||
expect(screen.getByText("Anxious")).toBeTruthy();
|
||||
expect(screen.getByText("Good with kids")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user