Skip to content

Commit

Permalink
feat(test): react hook form tests & stories (#2931)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
wingkwong authored May 4, 2024
1 parent 76f4dd8 commit 633f9d2
Show file tree
Hide file tree
Showing 11 changed files with 638 additions and 116 deletions.
105 changes: 104 additions & 1 deletion packages/components/autocomplete/__tests__/autocomplete.test.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
});
});
3 changes: 2 additions & 1 deletion packages/components/autocomplete/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
110 changes: 79 additions & 31 deletions packages/components/autocomplete/stories/autocomplete.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -733,15 +773,6 @@ export const DisabledOptions = {
},
};

export const WithDescription = {
render: MirrorTemplate,

args: {
...defaultProps,
description: "Select your favorite animal",
},
};

export const LabelPlacement = {
render: LabelPlacementTemplate,

Expand Down Expand Up @@ -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,

Expand Down Expand Up @@ -847,67 +899,63 @@ export const WithValidation = {
},
};

export const IsInvalid = {
render: Template,
export const WithSections = {
render: WithSectionsTemplate,

args: {
...defaultProps,
isInvalid: true,
variant: "bordered",
defaultSelectedKey: "dog",
errorMessage: "Please select a valid animal",
},
};

export const Controlled = {
render: ControlledTemplate,
export const WithCustomSectionsStyles = {
render: WithCustomSectionsStylesTemplate,

args: {
...defaultProps,
},
};

export const CustomSelectorIcon = {
render: Template,
export const WithAriaLabel = {
render: WithAriaLabelTemplate,

args: {
...defaultProps,
disableSelectorIconRotation: true,
selectorIcon: <SelectorIcon />,
label: "Select an animal 🐹",
"aria-label": "Select an animal",
},
};

export const CustomItems = {
render: CustomItemsTemplate,
export const WithReactHookForm = {
render: WithReactHookFormTemplate,

args: {
...defaultProps,
},
};

export const WithSections = {
render: WithSectionsTemplate,
export const Controlled = {
render: ControlledTemplate,

args: {
...defaultProps,
},
};

export const WithCustomSectionsStyles = {
render: WithCustomSectionsStylesTemplate,
export const CustomSelectorIcon = {
render: Template,

args: {
...defaultProps,
disableSelectorIconRotation: true,
selectorIcon: <SelectorIcon />,
},
};

export const WithAriaLabel = {
render: WithAriaLabelTemplate,
export const CustomItems = {
render: CustomItemsTemplate,

args: {
...defaultProps,
label: "Select an animal 🐹",
"aria-label": "Select an animal",
},
};

Expand Down
74 changes: 73 additions & 1 deletion packages/components/checkbox/__tests__/checkbox.test.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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);
});
});
Loading

0 comments on commit 633f9d2

Please sign in to comment.