feat(test): react hook form tests & stories (#2931)

* feat(input): add Input with React Hook Form tests

* refactor(input): add missing types

* feat(checkbox): add checkbox with React Hook Form tests

* feat(select): add react-hook-form to dev dep

* feat(select): add react hook form story

* feat(select): react hook form tests

* fix(select): incorrect button reference

* feat(deps): add react-hook-form to dev dep in autocomplete

* feat(autocomplete): react hook form story

* feat(autocomplete): react hook form tests

* fix(autocomplete): rollback wrapper type

* feat(switch): add react hook form tests

* refactor(stories): reorder stories items
This commit is contained in:
աӄա 2024-05-05 00:18:33 +08:00 committed by GitHub
parent 76f4dd8e76
commit 633f9d208b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 648 additions and 126 deletions

View File

@ -1,6 +1,7 @@
import * as React from "react";
import {act, render} from "@testing-library/react";
import {render, renderHook, act} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {useForm} from "react-hook-form";
import {Autocomplete, AutocompleteItem, AutocompleteSection} from "../src";
import {Modal, ModalContent, ModalBody, ModalHeader, ModalFooter} from "../../modal/src";
@ -220,3 +221,105 @@ describe("Autocomplete", () => {
expect(autocomplete).toHaveAttribute("aria-expanded", "false");
});
});
describe("Autocomplete with React Hook Form", () => {
let autocomplete1: HTMLInputElement;
let autocomplete2: HTMLInputElement;
let autocomplete3: HTMLInputElement;
let submitButton: HTMLButtonElement;
let wrapper: any;
let onSubmit: () => void;
beforeEach(() => {
const {result} = renderHook(() =>
useForm({
defaultValues: {
withDefaultValue: "cat",
withoutDefaultValue: "",
requiredField: "",
},
}),
);
const {
handleSubmit,
register,
formState: {errors},
} = result.current;
onSubmit = jest.fn();
wrapper = render(
<form className="flex w-full max-w-xs flex-col gap-2" onSubmit={handleSubmit(onSubmit)}>
<Autocomplete
data-testid="autocomplete-1"
{...register("withDefaultValue")}
aria-label="Favorite Animal"
items={itemsData}
label="Favorite Animal"
>
{(item) => <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>}
</Autocomplete>
<Autocomplete
data-testid="autocomplete-2"
{...register("withoutDefaultValue")}
aria-label="Favorite Animal"
items={itemsData}
label="Favorite Animal"
>
{(item) => <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>}
</Autocomplete>
<Autocomplete
data-testid="autocomplete-3"
{...register("requiredField", {required: true})}
aria-label="Favorite Animal"
items={itemsData}
label="Favorite Animal"
>
{(item) => <AutocompleteItem key={item.value}>{item.label}</AutocompleteItem>}
</Autocomplete>
{errors.requiredField && <span className="text-danger">This field is required</span>}
<button data-testid="submit-button" type="submit">
Submit
</button>
</form>,
);
autocomplete1 = wrapper.getByTestId("autocomplete-1");
autocomplete2 = wrapper.getByTestId("autocomplete-2");
autocomplete3 = wrapper.getByTestId("autocomplete-3");
submitButton = wrapper.getByTestId("submit-button");
});
it("should work with defaultValues", () => {
expect(autocomplete1).toHaveValue("Cat");
expect(autocomplete2).toHaveValue("");
expect(autocomplete3).toHaveValue("");
});
it("should not submit form when required field is empty", async () => {
const user = userEvent.setup();
await user.click(submitButton);
expect(onSubmit).toHaveBeenCalledTimes(0);
});
it("should submit form when required field is not empty", async () => {
const user = userEvent.setup();
await user.click(autocomplete3);
expect(autocomplete3).toHaveAttribute("aria-expanded", "true");
let listboxItems = wrapper.getAllByRole("option");
await user.click(listboxItems[1]);
expect(autocomplete3).toHaveValue("Dog");
await user.click(submitButton);
expect(onSubmit).toHaveBeenCalledTimes(1);
});
});

View File

@ -74,7 +74,8 @@
"framer-motion": "^11.0.28",
"clean-package": "2.2.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
"react-dom": "^18.0.0",
"react-hook-form": "^7.51.3"
},
"clean-package": "../../../clean-package.config.json"
}

View File

@ -2,6 +2,7 @@ import type {ValidationResult} from "@react-types/shared";
import React, {Key} from "react";
import {Meta} from "@storybook/react";
import {useForm} from "react-hook-form";
import {autocomplete, input, button} from "@nextui-org/theme";
import {
Pokemon,
@ -686,6 +687,45 @@ const CustomStylesWithCustomItemsTemplate = ({color, ...args}: AutocompleteProps
);
};
const WithReactHookFormTemplate = (args: AutocompleteProps) => {
const {
register,
formState: {errors},
handleSubmit,
} = useForm({
defaultValues: {
withDefaultValue: "cat",
withoutDefaultValue: "",
requiredField: "",
},
});
const onSubmit = (data: any) => {
// eslint-disable-next-line no-console
console.log(data);
alert("Submitted value: " + JSON.stringify(data));
};
return (
<form className="flex w-full max-w-xs flex-col gap-2" onSubmit={handleSubmit(onSubmit)}>
<Autocomplete {...args} {...register("withDefaultValue")}>
{items}
</Autocomplete>
<Autocomplete {...args} {...register("withoutDefaultValue")}>
{items}
</Autocomplete>
<Autocomplete {...args} {...register("requiredField", {required: true})}>
{items}
</Autocomplete>
{errors.requiredField && <span className="text-danger">This field is required</span>}
<button className={button({class: "w-fit"})} type="submit">
Submit
</button>
</form>
);
};
export const Default = {
render: Template,
args: {
@ -733,15 +773,6 @@ export const DisabledOptions = {
},
};
export const WithDescription = {
render: MirrorTemplate,
args: {
...defaultProps,
description: "Select your favorite animal",
},
};
export const LabelPlacement = {
render: LabelPlacementTemplate,
@ -782,6 +813,27 @@ export const EndContent = {
},
};
export const IsInvalid = {
render: Template,
args: {
...defaultProps,
isInvalid: true,
variant: "bordered",
defaultSelectedKey: "dog",
errorMessage: "Please select a valid animal",
},
};
export const WithDescription = {
render: MirrorTemplate,
args: {
...defaultProps,
description: "Select your favorite animal",
},
};
export const WithoutScrollShadow = {
render: Template,
@ -847,15 +899,37 @@ export const WithValidation = {
},
};
export const IsInvalid = {
render: Template,
export const WithSections = {
render: WithSectionsTemplate,
args: {
...defaultProps,
},
};
export const WithCustomSectionsStyles = {
render: WithCustomSectionsStylesTemplate,
args: {
...defaultProps,
},
};
export const WithAriaLabel = {
render: WithAriaLabelTemplate,
args: {
...defaultProps,
label: "Select an animal 🐹",
"aria-label": "Select an animal",
},
};
export const WithReactHookForm = {
render: WithReactHookFormTemplate,
args: {
...defaultProps,
isInvalid: true,
variant: "bordered",
defaultSelectedKey: "dog",
errorMessage: "Please select a valid animal",
},
};
@ -885,32 +959,6 @@ export const CustomItems = {
},
};
export const WithSections = {
render: WithSectionsTemplate,
args: {
...defaultProps,
},
};
export const WithCustomSectionsStyles = {
render: WithCustomSectionsStylesTemplate,
args: {
...defaultProps,
},
};
export const WithAriaLabel = {
render: WithAriaLabelTemplate,
args: {
...defaultProps,
label: "Select an animal 🐹",
"aria-label": "Select an animal",
},
};
export const CustomStyles = {
render: CustomStylesTemplate,

View File

@ -1,6 +1,7 @@
import * as React from "react";
import {render, act} from "@testing-library/react";
import {render, renderHook, act} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {useForm} from "react-hook-form";
import {Checkbox, CheckboxProps} from "../src";
@ -128,3 +129,74 @@ describe("Checkbox", () => {
expect(onChange).toBeCalled();
});
});
describe("Checkbox with React Hook Form", () => {
let checkbox1: HTMLInputElement;
let checkbox2: HTMLInputElement;
let checkbox3: HTMLInputElement;
let submitButton: HTMLButtonElement;
let onSubmit: () => void;
beforeEach(() => {
const {result} = renderHook(() =>
useForm({
defaultValues: {
withDefaultValue: true,
withoutDefaultValue: false,
requiredField: false,
},
}),
);
const {
handleSubmit,
register,
formState: {errors},
} = result.current;
onSubmit = jest.fn();
render(
<form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
<Checkbox {...register("withDefaultValue")} />
<Checkbox {...register("withoutDefaultValue")} />
<Checkbox {...register("requiredField", {required: true})} />
{errors.requiredField && <span className="text-danger">This field is required</span>}
<button type="submit">Submit</button>
</form>,
);
checkbox1 = document.querySelector("input[name=withDefaultValue]")!;
checkbox2 = document.querySelector("input[name=withoutDefaultValue]")!;
checkbox3 = document.querySelector("input[name=requiredField]")!;
submitButton = document.querySelector("button")!;
});
it("should work with defaultValues", () => {
expect(checkbox1.checked).toBe(true);
expect(checkbox2.checked).toBe(false);
expect(checkbox3.checked).toBe(false);
});
it("should not submit form when required field is empty", async () => {
const user = userEvent.setup();
await user.click(submitButton);
expect(onSubmit).toHaveBeenCalledTimes(0);
});
it("should submit form when required field is not empty", async () => {
act(() => {
checkbox3.click();
});
expect(checkbox3.checked).toBe(true);
const user = userEvent.setup();
await user.click(submitButton);
expect(onSubmit).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,6 +1,7 @@
import * as React from "react";
import {render} from "@testing-library/react";
import {render, renderHook, fireEvent} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {useForm} from "react-hook-form";
import {Input} from "../src";
@ -146,3 +147,78 @@ describe("Input", () => {
expect(onClear).toHaveBeenCalledTimes(1);
});
});
describe("Input with React Hook Form", () => {
let input1: HTMLInputElement;
let input2: HTMLInputElement;
let input3: HTMLInputElement;
let submitButton: HTMLButtonElement;
let onSubmit: () => void;
beforeEach(() => {
const {result} = renderHook(() =>
useForm({
defaultValues: {
withDefaultValue: "wkw",
withoutDefaultValue: "",
requiredField: "",
},
}),
);
const {
handleSubmit,
register,
formState: {errors},
} = result.current;
onSubmit = jest.fn();
render(
<form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
<Input isClearable label="With default value" {...register("withDefaultValue")} />
<Input
data-testid="input-2"
label="Without default value"
{...register("withoutDefaultValue")}
/>
<Input
data-testid="input-3"
label="Required"
{...register("requiredField", {required: true})}
/>
{errors.requiredField && <span className="text-danger">This field is required</span>}
<button type="submit">Submit</button>
</form>,
);
input1 = document.querySelector("input[name=withDefaultValue]")!;
input2 = document.querySelector("input[name=withoutDefaultValue]")!;
input3 = document.querySelector("input[name=requiredField]")!;
submitButton = document.querySelector("button")!;
});
it("should work with defaultValues", () => {
expect(input1).toHaveValue("wkw");
expect(input2).toHaveValue("");
expect(input3).toHaveValue("");
});
it("should not submit form when required field is empty", async () => {
const user = userEvent.setup();
await user.click(submitButton);
expect(onSubmit).toHaveBeenCalledTimes(0);
});
it("should submit form when required field is not empty", async () => {
fireEvent.change(input3, {target: {value: "updated"}});
const user = userEvent.setup();
await user.click(submitButton);
expect(onSubmit).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,6 +1,7 @@
import * as React from "react";
import {act, render} from "@testing-library/react";
import {render, renderHook, act} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {useForm} from "react-hook-form";
import {Select, SelectItem, SelectSection, type SelectProps} from "../src";
import {Modal, ModalContent, ModalHeader, ModalBody, ModalFooter} from "../../modal/src";
@ -397,3 +398,94 @@ describe("Select", () => {
});
});
});
describe("Select with React Hook Form", () => {
let select1: HTMLElement;
let select2: HTMLElement;
let select3: HTMLElement;
let submitButton: HTMLButtonElement;
let wrapper: any;
let onSubmit: () => void;
beforeEach(() => {
const {result} = renderHook(() =>
useForm({
defaultValues: {
withDefaultValue: "cat",
withoutDefaultValue: "",
requiredField: "",
},
}),
);
const {
register,
formState: {errors},
handleSubmit,
} = result.current;
onSubmit = jest.fn();
wrapper = render(
<form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
<Select data-testid="select-1" items={itemsData} {...register("withDefaultValue")}>
{(item) => <SelectItem key={item.value}>{item.label}</SelectItem>}
</Select>
<Select data-testid="select-2" items={itemsData} {...register("withoutDefaultValue")}>
{(item) => <SelectItem key={item.value}>{item.label}</SelectItem>}
</Select>
<Select
data-testid="select-3"
items={itemsData}
{...register("requiredField", {required: true})}
>
{(item) => <SelectItem key={item.value}>{item.label}</SelectItem>}
</Select>
{errors.requiredField && <span className="text-danger">This field is required</span>}
<button data-testid="submit-button" type="submit">
Submit
</button>
</form>,
);
select1 = wrapper.getByTestId("select-1");
select2 = wrapper.getByTestId("select-2");
select3 = wrapper.getByTestId("select-3");
submitButton = wrapper.getByTestId("submit-button");
});
it("should work with defaultValues", () => {
expect(select1).toHaveTextContent("Cat");
expect(select2).toHaveTextContent("");
expect(select3).toHaveTextContent("");
});
it("should not submit form when required field is empty", async () => {
const user = userEvent.setup();
await user.click(submitButton);
expect(onSubmit).toHaveBeenCalledTimes(0);
});
it("should submit form when required field is not empty", async () => {
const user = userEvent.setup();
await user.click(select3);
expect(select3).toHaveAttribute("aria-expanded", "true");
let listboxItems = wrapper.getAllByRole("option");
await user.click(listboxItems[1]);
expect(select3).toHaveTextContent("Dog");
await user.click(submitButton);
expect(onSubmit).toHaveBeenCalledTimes(1);
});
});

View File

@ -73,7 +73,8 @@
"@react-stately/data": "3.11.2",
"clean-package": "2.2.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
"react-dom": "^18.0.0",
"react-hook-form": "^7.51.3"
},
"clean-package": "../../../clean-package.config.json"
}

View File

@ -2,6 +2,7 @@
import type {ValidationResult} from "@react-types/shared";
import React, {ChangeEvent} from "react";
import {useForm} from "react-hook-form";
import {Meta} from "@storybook/react";
import {select, button} from "@nextui-org/theme";
import {PetBoldIcon, SelectorIcon} from "@nextui-org/shared-icons";
@ -585,6 +586,47 @@ const AsyncLoadingTemplate = ({color, variant, ...args}: SelectProps<Pokemon>) =
);
};
const WithReactHookFormTemplate = (args: SelectProps) => {
const {
register,
formState: {errors},
handleSubmit,
} = useForm({
defaultValues: {
withDefaultValue: "cat",
withoutDefaultValue: "",
requiredField: "",
},
});
const onSubmit = (data: any) => {
// eslint-disable-next-line no-console
console.log(data);
alert("Submitted value: " + JSON.stringify(data));
};
return (
<form className="flex w-full max-w-xs flex-col gap-2" onSubmit={handleSubmit(onSubmit)}>
<Select data-testid="select-1" {...args} {...register("withDefaultValue")}>
{items}
</Select>
<Select data-testid="select-2" {...args} {...register("withoutDefaultValue")}>
{items}
</Select>
<Select data-testid="select-3" {...args} {...register("requiredField", {required: true})}>
{items}
</Select>
{errors.requiredField && <span className="text-danger">This field is required</span>}
<button className={button({class: "w-fit"})} type="submit">
Submit
</button>
</form>
);
};
export const Default = {
render: MirrorTemplate,
@ -631,23 +673,15 @@ export const DisabledOptions = {
},
};
export const WithDescription = {
render: MirrorTemplate,
args: {
...defaultProps,
description: "Select your favorite animal",
},
};
export const WithoutLabel = {
export const IsInvalid = {
render: Template,
args: {
...defaultProps,
label: null,
"aria-label": "Select an animal",
placeholder: "Select an animal",
isInvalid: true,
variant: "bordered",
defaultSelectedKeys: ["dog"],
errorMessage: "Please select a valid animal",
},
};
@ -675,6 +709,26 @@ export const StartContent = {
},
};
export const WithDescription = {
render: MirrorTemplate,
args: {
...defaultProps,
description: "Select your favorite animal",
},
};
export const WithoutLabel = {
render: Template,
args: {
...defaultProps,
label: null,
"aria-label": "Select an animal",
placeholder: "Select an animal",
},
};
export const WithoutScrollShadow = {
render: Template,
@ -726,15 +780,62 @@ export const WithErrorMessageFunction = {
},
};
export const IsInvalid = {
render: Template,
export const WithChips = {
render: CustomItemsTemplate,
args: {
...defaultProps,
isInvalid: true,
variant: "bordered",
defaultSelectedKeys: ["dog"],
errorMessage: "Please select a valid animal",
selectionMode: "multiple",
isMultiline: true,
labelPlacement: "outside",
classNames: {
base: "max-w-xs",
trigger: "min-h-12 py-2",
},
renderValue: (items: SelectedItems<User>) => {
return (
<div className="flex flex-wrap gap-2">
{items.map((item) => (
<Chip key={item.key}>{item.data?.name}</Chip>
))}
</div>
);
},
},
};
export const WithSections = {
render: WithSectionsTemplate,
args: {
...defaultProps,
},
};
export const WithCustomSectionsStyles = {
render: WithCustomSectionsStylesTemplate,
args: {
...defaultProps,
},
};
export const WithAriaLabel = {
render: WithAriaLabelTemplate,
args: {
...defaultProps,
label: "Select an animal 🐹",
"aria-label": "Select an animal",
},
};
export const WithReactHookForm = {
render: WithReactHookFormTemplate,
args: {
...defaultProps,
},
};
@ -808,57 +909,6 @@ export const CustomRenderValue = {
},
};
export const WithChips = {
render: CustomItemsTemplate,
args: {
...defaultProps,
variant: "bordered",
selectionMode: "multiple",
isMultiline: true,
labelPlacement: "outside",
classNames: {
base: "max-w-xs",
trigger: "min-h-12 py-2",
},
renderValue: (items: SelectedItems<User>) => {
return (
<div className="flex flex-wrap gap-2">
{items.map((item) => (
<Chip key={item.key}>{item.data?.name}</Chip>
))}
</div>
);
},
},
};
export const WithSections = {
render: WithSectionsTemplate,
args: {
...defaultProps,
},
};
export const WithCustomSectionsStyles = {
render: WithCustomSectionsStylesTemplate,
args: {
...defaultProps,
},
};
export const WithAriaLabel = {
render: WithAriaLabelTemplate,
args: {
...defaultProps,
label: "Select an animal 🐹",
"aria-label": "Select an animal",
},
};
export const CustomStyles = {
render: CustomStylesTemplate,

View File

@ -1,5 +1,7 @@
import * as React from "react";
import {act, render} from "@testing-library/react";
import {render, renderHook, act} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import {useForm} from "react-hook-form";
import {Switch} from "../src";
@ -11,7 +13,7 @@ describe("Switch", () => {
});
it("ref should be forwarded", () => {
const ref = React.createRef<HTMLDivElement>();
const ref = React.createRef<HTMLInputElement>();
render(<Switch ref={ref} aria-label="switch" />);
expect(ref.current).not.toBeNull();
@ -198,3 +200,74 @@ describe("Switch", () => {
expect(wrapper.getByTestId("end-icon")).toBeInTheDocument();
});
});
describe("Switch with React Hook Form", () => {
let switch1: HTMLInputElement;
let switch2: HTMLInputElement;
let switch3: HTMLInputElement;
let submitButton: HTMLButtonElement;
let onSubmit: () => void;
beforeEach(() => {
const {result} = renderHook(() =>
useForm({
defaultValues: {
defaultTrue: true,
defaultFalse: false,
requiredField: false,
},
}),
);
const {
register,
formState: {errors},
handleSubmit,
} = result.current;
onSubmit = jest.fn();
render(
<form className="flex flex-col gap-4" onSubmit={handleSubmit(onSubmit)}>
<Switch {...register("defaultTrue")}>By default this switch is true</Switch>
<Switch {...register("defaultFalse")}>By default this switch is false</Switch>
<Switch {...register("requiredField", {required: true})}>This switch is required</Switch>
{errors.requiredField && <span className="text-danger">This switch is required</span>}
<button type="submit">Submit</button>
</form>,
);
switch1 = document.querySelector("input[name=defaultTrue]")!;
switch2 = document.querySelector("input[name=defaultFalse]")!;
switch3 = document.querySelector("input[name=requiredField]")!;
submitButton = document.querySelector("button")!;
});
it("should work with defaultValues", () => {
expect(switch1.checked).toBe(true);
expect(switch2.checked).toBe(false);
expect(switch3.checked).toBe(false);
});
it("should not submit form when required field is empty", async () => {
const user = userEvent.setup();
await user.click(submitButton);
expect(onSubmit).toHaveBeenCalledTimes(0);
});
it("should submit form when required field is not empty", async () => {
act(() => {
switch3.click();
});
expect(switch3.checked).toBe(true);
const user = userEvent.setup();
await user.click(submitButton);
expect(onSubmit).toHaveBeenCalledTimes(1);
});
});

View File

@ -221,6 +221,14 @@ export const WithIcons = {
},
};
export const WithReactHookForm = {
render: WithReactHookFormTemplate,
args: {
...defaultProps,
},
};
export const Controlled = {
render: ControlledTemplate,
@ -244,11 +252,3 @@ export const CustomWithHooks = {
...defaultProps,
},
};
export const WithReactHookForm = {
render: WithReactHookFormTemplate,
args: {
...defaultProps,
},
};

6
pnpm-lock.yaml generated
View File

@ -783,6 +783,9 @@ importers:
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
react-hook-form:
specifier: ^7.51.3
version: 7.51.3(react@18.2.0)
packages/components/avatar:
dependencies:
@ -2311,6 +2314,9 @@ importers:
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
react-hook-form:
specifier: ^7.51.3
version: 7.51.3(react@18.2.0)
packages/components/skeleton:
dependencies: