React-hook-form and MUI TextField testing with react-testing-library - reactjs

I'm using react-hook-form with zod for schema validation and MUI for UI components.
I have this MUI TextField wrapped in Controll HOC from RHF:
<Controller
name="docRegistrationNumber"
control={control}
defaultValue=""
render={({
field: { ref, onChange, value, ...field },
}) => (
<TextField
label="docRegistrationNumber"
variant="outlined"
error={Boolean(errors.docRegistrationNumber)}
helperText={
errors.docRegistrationNumber?.message
}
inputRef={ref}
onChange={(event) => {
if (errors.docRegistrationNumber) {
trigger('docRegistrationNumber')
}
onChange(event.target.value)
}}
{...field}
value={value}
/>
)}
/>
I'm trying to test the input for error messages.
A use case would be: if the input text is longer than 10 characters, display a error message below the input field "Invalid input".
What I'm trying in my test file is the following:
test('Should display error message for text longer than 10 characters', async () => {
const input = screen.getByRole('textbox', {
name: /docRegistrationNumber/i,
})
await user.type(input, 'this text is longer than 10 characters')
await user.tab()
const errorMessage = await screen.findByText(/invalid input/i)
expect(errorMessage).toBeInTheDocument()
})
This does not seems to work. It cannot find element with that error text.
I tried using fireEvent to blur the input, but got same results.
I tried wrapping the expect in a waitFor block with no results.
This is my test setup:
const Component = () => {
const {
control,
formState: { errors },
} = useForm()
return (
<ContactAttributes
errors={errors}
control={control as any}
trigger={jest.fn()}
contactDetails={EMPTY_DETAILS_OBJECT}
/>
)
}
beforeEach(() => {
render(<Component />, { wrapper: BrowserRouter })
})
My guess is that the errors are not generated at test runtime. If I pass the error explicitly in the errors props I can query that error message.
My question is, how should I test this input?
In react-hook-form documentation I saw they test the whole form and check for errors only when submitting the form button.
Thank you!
Using userEvent instead of fireEvent
Wrapping the expectation in waitFor
My expectation was to be able to query the error message.

Related

React using Formik does not clear the data value in Material UI form

I'm using formik.resetForm() to remove values from text fields in a form after submitting the data.
...
const handleSubmitProduct = async (values: Object, resetForm: any) => {
... code to handle my form data ...
resetForm()
if (response.ok) {
console.debug(response.status)
} else {
console.error(response)
}
}
const validate = (values: Object) => {
const errors: any = {}
if (!values.product_name) {
errors.product_name = "Include name"
}
return errors
}
... initialValues defined ...
const formik = useFormik({
initialValues: initialValues,
validate,
onSubmit: (values: Object, { resetForm }) => {
console.debug(JSON.stringify(values, null, 2))
handleSubmitProduct(values, resetForm)
},
})
return (
<FormLabel>Display name</FormLabel>
<TextField
onChange={formik.handleChange}
id="product_name"
onBlur={formik.handleBlur}
error={formik.touched.product_name && Boolean(formik.errors.product_name)}
helperText={formik.touched.product_name && formik.errors.product_name}
/>
<Button onClick={() => formik.handleSubmit()} variant="contained">
Submit
</Button>
)
I know there are many other questions like this but mine is different where I know the underlying Formik resources for values, errors, touched have been cleared but the values are still present in the text boxes.
The issue is I know the underlying Formik objects are cleared because after I submit, the validation triggers and prompts me like there is no value in the text field.
I've tried
resetForm({values: {initialValues}}) has the same result
resetForm(initialValues) has the same result
Use action.resetForm({values: {initialValues}}) in the onSubmit() which same result
https://codesandbox.io/s/mui-formik-fr93hm?file=/src/MyComponent.js but this approach uses the <Formik /> as opposed to useFormik which would change up my entire page but I'm in process to try anyway
I think the problem is that value of TextField is not value of formik. so the TextField is not controlled and by chaning value of formik it won't change.
assigning value of formik to it will do what you want
value={formik.values.firstName}
like this :
<TextField
onChange={formik.handleChange}
id="product_name"
value={formik.values.firstName}
onBlur={formik.handleBlur}
error={formik.touched.product_name && Boolean(formik.errors.product_name)}
helperText={formik.touched.product_name && formik.errors.product_name}
/>

react MUI TextField inside react-hook-form Controller inside MUI Stepper-Dialog setValue not working

I have a Button that opens a MUI Dialog.
Inside the Dialog I have a MUI Stepper. My Form is split up into different parts. Some Inputs are required others are not.
//Example Input
<Controller
name="stateName"
control={control}
rules={{ required: true }}
render={({ field: { onChange, value } }) => (
<TextField
required
label="stateName"
variant="standard"
onChange={onChange}
value={value}
fullWidth
error={errors.stateName ? true : false}
helperText={errors.stateName ? "Pflichtfeld" : null}
/>
)}
/>
Full Example: https://codesandbox.io/s/gracious-tdd-dkzoqy
When I submit my form I add an entry to an existing list and display it alongside with an edit-Button.
If the edit-Button gets pressed I want to open the Dialog and have the Inputs filled with the values of the edited data.
I tried using react-hook-form setValue("field", value) but it is not working.
I also tried to pass the edit-object via Props to the nested form-steps and use setValue inside these components useEffect utilizing useFormContext() but it didn't work either.
How can I pass the values to the Inputs so they get correctly displayed in the Multi-Step-Form-Dialog?
Working CSB -> https://codesandbox.io/s/eloquent-chaum-znt71c?file=/src/App.tsx
In editHandler, state is a json string, so the simplest fix is to parse it into the object
const editHandler = (stateJSON: any) => {
const state = JSON.parse(stateJSON)
methods.reset(state);
But in submitHandler data is stringified, the submitHanlder should look smth like this:
const submitHandler = (data: any) => {
setContent(prevContent => [...prevContent,data] );
methods.reset();
setEditState(undefined);
setOpen(false);
};
Also checkout this out https://beta.reactjs.org/learn/updating-objects-in-state
and
how to avoid mutations https://www.educative.io/courses/simplifying-javascript-handy-guide/B6yY3r7vEDJ

Uncaught TypeError: fieldRef.focus is not a function error in React-hook-form setFocus with Reactstrap Input

I'm using react-hook-form to manage my form. I want my interface to scroll to the first error field on error submit but I'm getting this error every single time I use setFocus
This is my controller code
<Controller
name="first_name"
control={control}
render={({ field: { ref, ...field } }) => (
<Input
{...field}
innerRef={ref}
type="text"
id="first_name"
className={classnames({
'input-error': formState?.errors?.first_name?.message,
})}
autoComplete="nope"
/>
)}
/>
and this is my setFocus code block
useEffect(() => {
const firstError = (Object.keys(errors) as Array<keyof typeof errors>).reduce<keyof typeof errors | null>((field, a) => {
const fieldKey = field as keyof typeof errors;
return !!errors[fieldKey] ? fieldKey : a;
}, null);
console.log({ firstError });
if (firstError) {
setFocus(firstError);
}
}, [errors]);
I somehow managed to create a sandbox for it but it doesn't show the same error. It errors on exact same code though.
https://codesandbox.io/s/gallant-voice-vqqi47?file=/src/App.js
If you ever encounter this error, this was caused by React's Strict Mode in dev environment. It means that this error wont occur on live environment. If you want to test your code without this error, just temporarily remove the strict mode.
Refer to this thread here

React ref called multiple times

Having such simple React (with the react-hook-form library) form
import React, { useRef } from "react";
import { useForm } from "react-hook-form";
export default function App() {
const { register, handleSubmit, formState: { errors } } = useForm();
const firstNameRef1 = useRef(null);
const onSubmit = data => {
console.log("...onSubmit")
};
const { ref, ...rest } = register('firstName', {
required: " is required!",
minLength: {
value: 5,
message: "min length is <5"
}
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<hr/>
<input {...rest} name="firstName" ref={(e) => {
console.log("...ref")
ref(e)
firstNameRef1.current = e // you can still assign to ref
}} />
{errors.firstName && <span>This field is required!</span>}
<button>Submit</button>
</form>
);
}
I'm getting:
...ref
...ref
...onSubmit
...ref
...ref
in the console output after the Submit button click.
My question is why is there so many ...ref re-calls? Should't it just be the one and only ref call at all?
P.S.
When I've removed the formState: { errors } from the useForm destructuring line above the problem disappears - I'm getting only one ...ref in the console output as expected.
It is happening because you are calling ref(e) in your input tag. That's why it is calling it repeatedly. Try removing if from the function and then try again.
<input {...rest} name="firstName" ref={(e) => {
console.log("...ref")
ref(e) // <-- ****Remove this line of code*****
firstNameRef1.current = e // you can still assign to ref
}} />
My question is why is there so many ...ref re-calls?
This is happening because every time you render, you are creating a new function and passing it into the ref. It may have the same text as the previous function, but it's a new function. React sees this, assumes something has changed, and so calls your new function with the element.
Most of the time, getting ref callbacks on every render makes little difference. Ref callbacks tend to be pretty light-weight, just assigning to a variable. So unless it's causing you problems, i'd just leave it. But if you do need to reduce the callbacks, you can use a memoized function:
const example = useCallback((e) => {
console.log("...ref")
ref(e);
firstNameRef1.current = e
}, [])
// ...
<input {...rest} name="firstName" ref={example} />

Validating a child input type file with react-hook-form and Yup

I'm creating a form with a file upload with help of react-hook-form and Yup. I am trying to use the register method in my child component. When passing register as a prop (destructured in curly braces) the validation and submiting doesn't work. You can always submit the form and the submitted file object is empty.
Here's a sandbox link.
There are several of problems with your code.
1- register method returns an object with these properties:
{
onChange: function(){},
onBlur:function{},
ref: function(){}
}
when you define your input like this:
<input
{...register('photo')}
...
onChange={(event) => /*something*/}
/>
actually you are overrding the onChange method which has returned from register method and react-hook-form couldn't recognize the field change event. The solution to have your own onChange alongside with react-hook-form's onChange could be something like this:
const MyComp = ()=> {
const {onChange, ...registerParams} = register('photo');
...
return (
...
<input
{...params}
...
onChange={(event) => {
// do whatever you want
onChange(event)
}}
/>
);
}
2- When you delete the photo, you are just updating your local state, and you don't update photo field, so react-hook-form doesn't realize any change in your local state.
the problems in your ImageOne component could be solved by change it like this:
function ImageOne({ register, errors }) {
const [selectedImage, setSelectedImage] = useState(null);
const { onChange, ...params } = register("photo");
return (
...
<Button
className="delete"
onClick={() => {
setSelectedImage(null);
//Make react-hook-form aware of changing photo state
onChange({ target: { name: "photo", value: [] } });
}}
>
...
<input
//name="photo"
{...params}
type="file"
accept="image/*"
id="single"
onChange={(event) => {
setSelectedImage(event.target.files[0]);
onChange(event); // calling onChange returned from register
}}
/>
...
);
}
3- Since your input type is file so, the value of your photo field has length property that you can use it to handle your validation like this:
const schema = yup.object().shape({
photo: yup
.mixed()
.test("required", "photo is required", value => value.length > 0)
.test("fileSize", "File Size is too large", (value) => {
return value.length && value[0].size <= 5242880;
})
.test("fileType", "Unsupported File Format", (value) =>{
return value.length && ["image/jpeg", "image/png", "image/jpg"].includes(value[0].type)
}
)
});
Here is the full edited version of your file.

Resources