Formik Material UI and react testing library - reactjs

I am finding it hard to work with react testing library and understand the queries I need to use in order to select the components I need to test. The queries are too simplistic when the DOM gets more and more verbose with frameworks like material ui and formik.
I have created a code sandbox to illustrate the problem. You can check the failing test there.
https://codesandbox.io/embed/px277lj1x
The issue I am getting is, the query getLabelTextBy() does not return the component. It looks like the aria label by or the for attribute is not being rendered by material ui. Not sure how to fix this error.
The code is below here for reference too
//Subject under test
import React from "react";
import { Button } from "#material-ui/core";
import { TextField } from "formik-material-ui";
import { Field, Form, Formik } from "formik";
import * as yup from "yup";
const validationSchema = yup.object().shape({
name: yup.string().required("Office name is required"),
address: yup.string().required("Address is required")
});
export default () => (
<Formik
initialValues={{
name: "",
address: ""
}}
validationSchema={validationSchema}
onSubmit={(values, { setSubmitting }) => {
setSubmitting(false);
console.log("form is submitted with", values);
}}
render={({ submitForm, isSubmitting, isValid }) => (
<Form>
<Field
label="Office Name"
name="name"
required
type="text"
component={TextField}
/>
<Field
label="Address Line 1"
name="addressLine1"
type="text"
component={TextField}
/>
<Button
variant="contained"
color="primary"
fullWidth={false}
size="medium"
disabled={isSubmitting || !isValid}
onClick={submitForm}
data-testid="submitButton"
>
Submit
</Button>
</Form>
)}
/>
);
// Test
import React from "react";
import { render, fireEvent } from "react-testing-library";
import App from "./App";
describe("Create Office Form tests", () => {
it("button should not be disabled when all required fields are filled up", () => {
const { getByLabelText, getByTestId, debug } = render(<App />);
const values = {
"office name": "office",
address: "address 1"
};
for (const key in values) {
const input = getByLabelText(key, { exact: false, selector: "input" });
fireEvent.change(input, { target: { value: values[key] } });
}
const button = getByTestId("submitButton");
expect(button.disabled).not.toBeDefined();
});
});

You must add an id to your Field because the label's for attribute expect the ID of the input element it refers to:
<Field
id="myName"
label="Office Name"
name="name"
required
type="text"
component={TextField}
/>
A working example:

Few things. The use of { getByLabelText, getByTestId, debug } is not advised by the creator of the library. You should use screen.getByLabelText().
If there is a change, it's possible that it doesn't wait for the new render, so it would be better to await it or wrap the expect in a waitFor method.
Also, expect(button.disabled).not.toBeDefined() should not be correct. I think you're just checking if the button is disabled or not, right?
So you can use expect(button).toBeDisabled() or not.toBeDisabled()
At least to test it I think you should take off the for loop and check it normally. You can use screen.debug(component) to have the HTML shown after some action.

Related

Cannot read properties of undefined (reading 'getFieldProps') with the FastField in formik

I have a form with the Formik and I am going to decrease re-renders so I used the FastField.
I got the error Cannot read properties of undefined (reading 'getFieldProps') and I don't have an idea how I can solve it.
there is full code here
I found about it here and here
import React from "react";
import ReactDOM from "react-dom";
import { useFormik, FastField } from "formik";
import * as yup from "yup";
import Button from "#material-ui/core/Button";
const validationSchema = yup.object({});
const WithMaterialUI = () => {
const formik = useFormik({
initialValues: {
firstName:''
},
validationSchema: validationSchema,
onSubmit: (values) => {
alert(JSON.stringify(values, null, 2));
}
});
return (
<div>
<form onSubmit={formik.handleSubmit}>
<label htmlFor="firstName">First Name</label>
<FastField name="firstName" placeholder="Weezy" />
<Button color="primary" variant="contained" fullWidth type="submit">
Submit
</Button>
</form>
</div>
);
};
ReactDOM.render(<WithMaterialUI />, document.getElementById("root"));
In the documentation you'll find
** Be aware that , , , connect(), and will NOT work with useFormik() as they all require React Context.
So you can't keep using the hook if you want to use <FastField/>, so I guess you have to fallback to the old style of wrapping your form with <Formik> component

formik axios post form submission on reactjs

Newbie in reactjs ,trying to build a simple form with formik and validating with Yup library .just got a problem ,how to submit axios post request in formik with validations shown in below code.i have confusion on handle submit function onSubmit
CODE
import React,{useState} from 'react'
import axios from 'axios'
import { toast } from "react-toastify";
import { useHistory } from "react-router-dom";
import config from '../../utils/config';
import { useFormik } from 'formik';
import * as Yup from 'yup';
export default function AddCompanyPage = () => {
let history = useHistory();
const formik = useFormik({
initialValues: {name:""},
validationSchema : Yup.object().shape({
name: Yup.string()
.min(2, "*Names must have at least 2 characters")
.max(20, "*Names can't be longer than 100 characters")
.required("*Name is required"),
}),
onSubmit:values=>
{
const AddCompany=async e()=> {
e.preventDefault();
axios.post(`${config.APP_CONFIG}/Companies/Company`,values)
.then(res => {
if(res.data.status_code === 200){
//setUser(res.data.msg)
history.push("/settings/companies");
}
})
.catch(err => {
toast.error("Couldnot lpost data")
})
}
},
})
return (
<h2 className="text-center mb-4">Add a Company</h2>
<form onSubmit={formik.handleSubmit}>
<div className="form-group">
<label htmlFor="Company">Company Name</label>
<input
id="name"
name="name"
type="text"
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={formik.values.name}
/>
{formik.touched.name && formik.errors.name ? (
<div>{formik.errors.name}</div>
) : null}
</div>
<button type="submit">Submit</button>
</form>
)
}
The approach seems fine, you can try correcting the syntax of the return statement. In the return statement you should return a single element, right now you are returning 2 elements, h2 and form.
You can try wrapping it in a wrapper class or a div (not the best approach).
Also remove the word function where you are declaring/exporting the component

react-hook-form when editing doesn't update

I can't find a way to provide the existing information from the DB to the react-hook-form library to revalidate when clicking submits and to update it if the data was changed.
The problem is when clicking on submit button, this object will shown without any value for firstName and lastname:
{firstName: undefined, lastname: undefined}
Simplified version of my component:
import React from "react";
import { useForm } from "react-hook-form";
import { TextField } from "#material-ui/core";
const App = (props) => {
const { register, handleSubmit} = useForm();
const onSubmit = (data) => {
console.log(data); // ---> here
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<h2>Step 1</h2>
<label>
First Name:
<TextField {...register("firstName")} defaultValue="firstname" />
</label>
<label>
Last Name:
<TextField {...register("lastname")} defaultValue="lastname" />
</label>
<input type="submit" />
</form>
);
};
export default App
Any idea how to create a form that allows you to edit the fields?
Check out on codesandbox
The problem is with the TextFiled component. the register is not set correctly.
According to the MUI documentation:
props:inputProps type:object description:Attributes applied to the input element.
So you need to pass the register throw the inputProps:
<TextField inputProps={register("firstName")} defaultValue="firstname" />
<TextField inputProps={register("lastname")} defaultValue="lastname" />

React Hook Form using Controller, yup and Material UI - validation issue

I have a simple form with a single Material UI TextField inside React Hook Form. I use Yup schema validation (via yupResolver). I try to validate form and display errors visually (boolean prop 'error' in TextField). I use Controller with 'render' prop, however it fails to update error on TextField change. Does anyone know what I am doing wrong here?
Link to codesandbox
import React from "react";
import ReactDOM from "react-dom";
import { TextField } from "#material-ui/core";
import { Controller, useForm } from "react-hook-form";
import * as yup from "yup";
import { yupResolver } from "#hookform/resolvers/yup";
import "./styles.css";
const schema = yup.object().shape({
title: yup.string().required("Required")
});
function App() {
const {
handleSubmit,
formState: { errors },
control
} = useForm({
resolver: yupResolver(schema)
});
const onSubmit = (data) => console.log("onSubmit", data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
render={({ formState, fieldState }) => {
return <TextField label="Title" error={!!formState.errors?.title} />;
}}
name="title"
control={control}
/>
<input type="submit" />
</form>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Additonally 'fieldState' in Controller seems to be an empty object all the time. Shouldn't it show properties as listed in
Link
I found the answer following #damjtoh suggestion. I've noticed there is a 'field' parameter in the render function in RHF code examples. Adding it to TextField connected component with the form. Just remember to add 'defaultValue' to avoid 'changing uncontrolled input' error. Here's how it should look like:
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
render={({ field, formState }) => (
<TextField
{...field}
label="Title"
error={!!formState.errors?.title}
/>
)}
name="title"
control={control}
defaultValue=""
/>
<input type="submit" />
</form>
You aren't connecting the material-ui component to the react-hook-form, you should check the Integrating Controlled Inputs section of react-hook-form documentation.

Using Material-UI's Autocomplete component with Formik

Currently trying to use Material UI's Autocomplete component with Formik. So far things like text fields and traditional selects from Material-UI play very nice with Formik. Implementing Autocomplete is not the case. Formik's onChange handler doesn't seem to update the value for my city_id. I know Autocomplete is still not apart of Material-UI's core library but was still seeing if something like this was a possibility at the moment.
import React from "react";
import ReactDOM from "react-dom";
import { Formik, Form } from 'formik';
import TextField from '#material-ui/core/TextField';
import Autocomplete from '#material-ui/lab/Autocomplete';
import Button from '#material-ui/core/Button';
import { cities } from '../data/cities';
import "./styles.css";
const initialValues = {
city_id: '',
};
const submit = params => {
alert(`Value for city_id is: ${params.city_id}`);
};
function App() {
return (
<Formik
initialValues={ initialValues }
onSubmit={ submit }
>
{({
handleChange,
values,
}) => (
<Form>
<Autocomplete
id="city_id"
name="city_id"
options={ cities }
groupBy={ option => option.state }
getOptionLabel={ option => option.name }
style={{ width: 300 }}
renderInput={params => (
<TextField
{ ...params }
onChange={ handleChange }
margin="normal"
label="Cities"
fullWidth
value={ values.city_id }
/>
)}
/>
<Button
variant="contained"
color="primary"
type="submit"
>
Submit
</Button>
</Form>
)}
</Formik>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Your problem is that handleChange won't work the way you are doing.
If you take a look at the handleChange docs:
General input change event handler. This will update the values[key] where key is the event-emitting input's name attribute. If the name attribute is not present, handleChange will look for an input's id attribute. Note: "input" here means all HTML inputs.
Which should work fine, but the problem is that the TextField inside Autocomplete will only trigger handleChange when you type something on it, and the value will be the text, not the id or other property you want, so you need to move handleChange to the Autocomplete.
And there is another problem, you can't use handleChange in the Autocomplete because it doesn't references the input you want and it also have different parameters from the normal onChange of the input, as you can see in the docs.
onChange
func
Callback fired when the value changes.
Signature:
function(event: object, value: any) => void
event: The event source of the callback
value: null
So what you need to do is use setFieldValue and pass it to Autocomplete like
onChange={(e, value) => setFieldValue("city_id", value)}
You need to pass the name of your field and what value you want to get.
Here is a working example
#vencovsky has provided the correct answer that is still working for me with Material UI 14.10.1.
I'm adding a bit more to it as I have my field set to required in using Yup validation.
To get this to work correctly I have the following:
Yup config:
validationSchema = {
Yup.object().shape({
contact: Yup.string().max(255).required('Contact is required'),
})
}
react:
<Autocomplete
id="contact-autocomplete"
options={contacts}
getOptionLabel={(contact) => `${contact?.firstName} ${contact?.lastName}`}
onChange={(e, value) => setFieldValue("contact", value?.id || "")}
onOpen={handleBlur}
includeInputInList
renderInput={(params) => (
<TextField
{...params}
error={Boolean(touched.contact && errors.contact)}
fullWidth
helperText={touched.contact && errors.contact}
label="Contact Person"
name="contact"
variant="outlined"
/>
)}
/>
When the user click on the Autocomplete element, it fires the onOpen which runs the Formik onBlur and marks the field as touched. If an item is then not picked, Formik flags the field and displays the Contact is required validation message.
You have to add onChange = {(event, value) => handleChange(value)} in Autocomplete tag as
import React from "react";
import ReactDOM from "react-dom";
import { Formik, Form } from 'formik';
import TextField from '#material-ui/core/TextField';
import Autocomplete from '#material-ui/lab/Autocomplete';
import Button from '#material-ui/core/Button';
import { cities } from '../data/cities';
import "./styles.css";
const [cityId,setCityId]=React.useState({city_id:''});
const handleChange=(value)=>{
// Here is the value is a selected option label or the new typed value
setCityId({city_id:value});
}
function App() {
return (
<Formik
initialValues={ cityId }
onSubmit={() => {
alert(`Value for city_id is: ${cityId.city_id}`);
}}
>
{({
handleChange,
values,
}) => (
<Form>
<Autocomplete
id="city_id"
name="city_id"
options={ cities }
groupBy={ option => option.state }
getOptionLabel={ option => option.name }
style={{ width: 300 }}
onChange = {(event, value) => handleChange(value)}
renderInput={params => (
<TextField
{ ...params }
onChange={ handleChange }
margin="normal"
label="Cities"
fullWidth
value={ values.city_id }
/>
)}
/>
<Button
variant="contained"
color="primary"
type="submit"
>
Submit
</Button>
</Form>
)}
</Formik>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
If onChange don't work you can use onInputChange as well.
I had the same issue recently and solved. Sharing my experience
Updating formik values directly on OnChange method solved the issue
onChange={(event, value) => (formik.values.country = value!)}
Here is the full code
Formik settings
const formik = useFormik({
initialValues: {
id: user.id || "",
name: user.name || "",
country: user.country,
email: user.email || "",
submit: null,
},
validationSchema: Yup.object({
email: Yup.string()
.email("Must be a valid email")
.max(255)
.required("Email is required"),
name: Yup.string().max(255).required("Name is required"),
}),
onSubmit: async (values, helpers): Promise<void> => {
console.log("Updating user...");
try {
let userData: UserDetails = {
id: values.id,
email: values.email,
name: values.name,
country: values.country,
};
await userApi.registerUser(userData);
helpers.setStatus({ success: true });
helpers.setSubmitting(false);
toast.success("User updated!");
} catch (err) {
console.error(err);
toast.error("Something went wrong!");
helpers.setStatus({ success: false });
helpers.setErrors({ submit: err.message });
helpers.setSubmitting(false);
}
},
});
Autocomplete
<Autocomplete
getOptionLabel={(option): string => option.text}
options={countries}
value={formik.values.country}
defaultValue={formik.values.country}
onChange={(event, value) => (formik.values.country = value!)}
renderInput={(params): JSX.Element => (
<TextField
{...params}
fullWidth
label="Country"
name="country"
error={Boolean(
formik.touched.country && formik.errors.country
)}
helperText={formik.touched.country && formik.errors.country}
onBlur={formik.handleBlur}
/>
)}
/>

Resources