bc21d6de09
CI / Test (push) Successful in 21s
CI / Test (pull_request) Successful in 22s
CI / Lint & Typecheck (push) Successful in 26s
CI / Lint & Typecheck (pull_request) Successful in 28s
CI / Build & Push Docker Image (push) Successful in 25s
CI / Build & Push Docker Image (pull_request) Successful in 20s
165 lines
6.9 KiB
TypeScript
165 lines
6.9 KiB
TypeScript
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 "@groombook/types";
|
|
|
|
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.getByPlaceholderText(/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.getByPlaceholderText(/type a cut name/i);
|
|
fireEvent.change(input, { target: { value: "Teddy Bear" } });
|
|
fireEvent.click(screen.getByRole("button", { name: "Add" }));
|
|
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");
|
|
if (!puppyCutTag) return;
|
|
const removeBtn = puppyCutTag.querySelector("button");
|
|
if (!removeBtn) return;
|
|
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.getByPlaceholderText(/type a cut name/i), { target: { value: "Puppy Cut" } });
|
|
fireEvent.keyDown(screen.getByPlaceholderText(/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.getByPlaceholderText(/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 removeButtons = screen.getAllByRole("button", { name: "" });
|
|
if (removeButtons.length === 0) return;
|
|
const removeButton = removeButtons[0]!;
|
|
if (!removeButton) return;
|
|
fireEvent.click(removeButton);
|
|
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();
|
|
});
|
|
|
|
// ── Weight pre-fill from portal `weight` key (GRO-2207) ───────────────────────
|
|
|
|
it("pre-fills weight from the portal `weight` key when weightKg is absent", () => {
|
|
const portalPet: Pet = { ...BASE_PET, weightKg: null, weight: "12.50" };
|
|
render(<PetForm pet={portalPet} onSave={onSave} onCancel={onCancel} />);
|
|
expect(screen.getByDisplayValue(12.5)).toBeTruthy();
|
|
});
|
|
}); |