import { act, useEffect, useRef } from "react"; import { createRoot } from "react-dom/client"; import { FormProvider, UseFormReturn, useForm } from "react-hook-form"; import { FormTextField } from "@/components/Forms/FormTextField"; import { FormField } from "./form"; type TestFormValues = { items: Array<{ quantity: string }>; title: string; }; declare global { var IS_REACT_ACT_ENVIRONMENT: boolean; } globalThis.IS_REACT_ACT_ENVIRONMENT = true; type DynamicQuantityFieldProps = { name: "items.0.quantity" | "items.1.quantity"; onMount: () => void; }; const DynamicQuantityField = ({ name, onMount }: DynamicQuantityFieldProps) => { const onMountRef = useRef(onMount); useEffect(() => { onMountRef.current(); }, []); return ( } /> ); }; type FormHarnessProps = DynamicQuantityFieldProps & { onFormReady: (form: UseFormReturn) => void; }; const FormHarness = ({ name, onMount, onFormReady }: FormHarnessProps) => { const form = useForm({ defaultValues: { items: [{ quantity: "1" }, { quantity: "2" }], title: "before", }, }); useEffect(() => { onFormReady(form); }, [form, onFormReady]); return ( ); }; const ControlledTextFieldHarness = ({ onFormReady }: Pick) => { const form = useForm({ defaultValues: { items: [], title: "before", }, }); useEffect(() => { onFormReady(form); }, [form, onFormReady]); return ( ); }; describe("FormField", () => { let container: HTMLDivElement; let root: ReturnType; beforeEach(() => { container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); }); afterEach(() => { act(() => root.unmount()); container.remove(); }); it("resubscribes a Controller when its name changes without remounting its parent", () => { const onMount = jest.fn(); let form: UseFormReturn | undefined; const renderHarness = (name: DynamicQuantityFieldProps["name"]) => { root.render( (form = value)} /> ); }; act(() => renderHarness("items.0.quantity")); const inputBeforeRename = container.querySelector("input[aria-label='quantity']") as HTMLInputElement; expect(inputBeforeRename.value).toBe("1"); expect(onMount).toHaveBeenCalledTimes(1); act(() => renderHarness("items.1.quantity")); expect(onMount).toHaveBeenCalledTimes(1); expect((container.querySelector("input[aria-label='quantity']") as HTMLInputElement).value).toBe("2"); act(() => { form?.setValue("items.1.quantity", "5"); }); expect((container.querySelector("input[aria-label='quantity']") as HTMLInputElement).value).toBe("5"); expect(form?.getValues("items.1.quantity")).toBe("5"); }); it("updates React Hook Form through a Controller without an external register spread", () => { let form: UseFormReturn | undefined; act(() => { root.render( (form = value)} />); }); const input = container.querySelector("input[name='title']") as HTMLInputElement; const valueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set; act(() => { valueSetter?.call(input, "after"); input.dispatchEvent(new Event("input", { bubbles: true })); }); expect(input.value).toBe("after"); expect(form?.getValues("title")).toBe("after"); }); });