diff --git a/client/src/app/quotes/components/QuoteItemsSortableDragHandleCell.test.tsx b/client/src/app/quotes/components/QuoteItemsSortableDragHandleCell.test.tsx new file mode 100644 index 0000000..e423717 --- /dev/null +++ b/client/src/app/quotes/components/QuoteItemsSortableDragHandleCell.test.tsx @@ -0,0 +1,77 @@ +import { act } from "react"; +import { createRoot } from "react-dom/client"; +import { QuoteItemsSortableDragHandleCell } from "./QuoteItemsSortableDragHandleCell"; +import { + QuoteItemsSortableTableRowContext, + QuoteItemsSortableTableRowContextValue, +} from "./QuoteItemsSortableTableRow"; + +declare global { + var IS_REACT_ACT_ENVIRONMENT: boolean; +} + +globalThis.IS_REACT_ACT_ENVIRONMENT = true; + +jest.mock("i18next", () => ({ t: (key: string) => key })); +jest.mock("@/ui", () => { + const React = require("react") as typeof import("react"); + + return { + Button: React.forwardRef>( + (props, ref) => + ); +}; diff --git a/client/src/app/quotes/components/QuoteItemsSortableTableRow.tsx b/client/src/app/quotes/components/QuoteItemsSortableTableRow.tsx index 192b32a..2266646 100644 --- a/client/src/app/quotes/components/QuoteItemsSortableTableRow.tsx +++ b/client/src/app/quotes/components/QuoteItemsSortableTableRow.tsx @@ -1,20 +1,29 @@ import { cn } from "@/lib/utils"; import { TableRow } from "@/ui/table"; -import { DraggableSyntheticListeners } from "@dnd-kit/core"; +import { DraggableAttributes, DraggableSyntheticListeners } from "@dnd-kit/core"; import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { CSSProperties, PropsWithChildren, createContext, useMemo } from "react"; import { QuoteItemsSortableProps } from "./QuoteItemsSortableDataTable"; -interface Context { - attributes: Record; +export interface QuoteItemsSortableTableRowContextValue { + attributes: DraggableAttributes; listeners: DraggableSyntheticListeners; ref(node: HTMLElement | null): void; + isDragging: boolean; } -export const QuoteItemsSortableTableRowContext = createContext({ - attributes: {}, +export const QuoteItemsSortableTableRowContext = createContext({ + attributes: { + role: "button", + tabIndex: 0, + "aria-disabled": false, + "aria-pressed": undefined, + "aria-roledescription": "sortable", + "aria-describedby": "", + }, listeners: undefined, ref() {}, + isDragging: false, }); function animateLayoutChanges(args: any) { @@ -52,8 +61,9 @@ export function QuoteItemsSortableTableRow({ attributes, listeners, ref: setActivatorNodeRef, + isDragging, }), - [attributes, listeners, setActivatorNodeRef] + [attributes, isDragging, listeners, setActivatorNodeRef] ); return ( diff --git a/client/src/app/quotes/hooks/useDetailColumns.tsx b/client/src/app/quotes/hooks/useDetailColumns.tsx index a3e68d2..804c5d9 100644 --- a/client/src/app/quotes/hooks/useDetailColumns.tsx +++ b/client/src/app/quotes/hooks/useDetailColumns.tsx @@ -1,12 +1,12 @@ import { DataTablaRowActionFunction, DataTableRowActions, - DataTableRowDragHandleCell, } from "@/components"; import { Checkbox } from "@/ui"; import { ColumnDef, Row } from "@tanstack/react-table"; import { useMemo } from "react"; +import { QuoteItemsSortableDragHandleCell } from "../components/QuoteItemsSortableDragHandleCell"; // eslint-disable-next-line @typescript-eslint/no-unused-vars export function useDetailColumns( @@ -35,7 +35,7 @@ export function useDetailColumns( ),*/ header: () => null, - cell: (info) => , + cell: () => , enableSorting: false, enableHiding: false, size: 40, diff --git a/client/src/ui/form.test.tsx b/client/src/ui/form.test.tsx new file mode 100644 index 0000000..8f3cc59 --- /dev/null +++ b/client/src/ui/form.test.tsx @@ -0,0 +1,101 @@ +import { act, useEffect, useRef } from "react"; +import { createRoot } from "react-dom/client"; +import { FormProvider, UseFormReturn, useForm } from "react-hook-form"; +import { FormField } from "./form"; + +type TestFormValues = { + items: Array<{ quantity: 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" }], + }, + }); + + 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"); + }); +}); diff --git a/client/src/ui/form.tsx b/client/src/ui/form.tsx index b295211..d1657c5 100644 --- a/client/src/ui/form.tsx +++ b/client/src/ui/form.tsx @@ -34,7 +34,7 @@ const FormField = < }: ControllerProps) => { return ( - + ); };