import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { fireEvent, screen, waitFor } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { LocationContactsFieldArray } from "@/app/(protected)/locations/_components/location-contacts-field-array"; import { locationDefaultValues, locationSchema, type LocationFormValues, } from "@/domain/locations/schemas/location-schema"; import { renderWithProviders } from "@/test/test-utils"; const dataTransferStub = { setData: vi.fn(), effectAllowed: "none" as DataTransfer["effectAllowed"], }; function ContactsHarness({ initialContacts, onSubmit = vi.fn(), }: { initialContacts: LocationFormValues["contacts"]; onSubmit?: (values: LocationFormValues) => void; }) { const { control, handleSubmit, register, formState: { errors }, } = useForm({ resolver: zodResolver(locationSchema), defaultValues: { ...locationDefaultValues, name: "Dallas DC", address: "3811 Distribution Dr", city: "Dallas", state: "TX", zipCode: "70068", contactEmail: "jane@example.com", contacts: initialContacts, }, }); return (
); } function nameInputs(): HTMLInputElement[] { return screen.getAllByLabelText(/^Contact Name/) as HTMLInputElement[]; } /** 1-based handle position: `handleFor(2)` is the second row's drag handle. */ function handleFor(position: number): HTMLElement { return screen.getByRole("button", { name: `Reorder contact ${position}. Use arrow up or down to move.`, }); } describe("LocationContactsFieldArray", () => { it("marks the first row as Main and disables removal when only one remains", () => { renderWithProviders( , { withAuth: false }, ); expect(screen.getByText("Main")).toBeInTheDocument(); expect(screen.getByLabelText(/^Contact Name \(Main\)/)).toHaveValue("Jane"); expect(screen.getByLabelText("Remove contact 1")).toBeDisabled(); }); it("adds and removes contact rows", () => { renderWithProviders( , { withAuth: false }, ); fireEvent.click(screen.getByRole("button", { name: /add contact/i })); expect(nameInputs()).toHaveLength(2); expect(screen.getByLabelText("Remove contact 1")).toBeEnabled(); expect(screen.getByText("Main")).toBeInTheDocument(); fireEvent.click(screen.getByLabelText("Remove contact 2")); expect(nameInputs()).toHaveLength(1); expect(screen.getByLabelText("Remove contact 1")).toBeDisabled(); }); it("reorders with keyboard ArrowUp and keeps focus on the moved handle", async () => { renderWithProviders( , { withAuth: false }, ); expect(nameInputs().map((input) => input.value)).toEqual(["Jane", "Bob"]); const bobHandle = handleFor(2); bobHandle.focus(); fireEvent.keyDown(bobHandle, { key: "ArrowUp" }); expect(nameInputs().map((input) => input.value)).toEqual(["Bob", "Jane"]); await waitFor(() => { expect(document.activeElement).toBe(handleFor(1)); }); }); it("ignores ArrowDown on the last row", () => { renderWithProviders( , { withAuth: false }, ); fireEvent.keyDown(handleFor(2), { key: "ArrowDown" }); expect(nameInputs().map((input) => input.value)).toEqual(["Jane", "Bob"]); }); it("reorders with native drag events from the handle", () => { renderWithProviders( , { withAuth: false }, ); fireEvent.dragStart(handleFor(1), { dataTransfer: dataTransferStub }); fireEvent.drop(nameInputs()[1].closest("div") as HTMLElement, { dataTransfer: dataTransferStub, }); expect(nameInputs().map((input) => input.value)).toEqual(["Bob", "Jane"]); }); it("submits contacts in displayed order", async () => { const onSubmit = vi.fn(); renderWithProviders( , { withAuth: false }, ); fireEvent.keyDown(handleFor(2), { key: "ArrowUp" }); fireEvent.click(screen.getByRole("button", { name: "Save" })); await waitFor(() => { expect(onSubmit).toHaveBeenCalledTimes(1); }); expect(onSubmit.mock.calls[0][0]).toEqual( expect.objectContaining({ contacts: [ { id: 12, name: "Bob", phone: "(421) 433-0033" }, { id: 11, name: "Jane", phone: "(421) 433-0032" }, ], }), ); }); });