Test useFormik hook with React Testing Library - javascript

I did a thorough search before positing this question, most of them were using <Formik/> where they had sent onSubmit handler as a prop so that it can be tested whether it is called or not when the form is submitted.
In my case, I'm using useFormik hook for my form and I can't get to reach the onSubmit code when the form is submitted. The type property of button is submit so the onClick function is undefined when I displayed the element in console log.
Below is excerpt from my code
Component
export const WithMaterialUI = () => {
const formik = useFormik({
initialValues: {
email: "foobar#example.com"
},
validationSchema: validationSchema,
onSubmit: (values) => {
alert(JSON.stringify(values, null, 2));
}
});
return (
<div>
<form onSubmit={formik.handleSubmit} data-testid="form">
<TextField
data-testid="email"
fullWidth
id="email"
name="email"
label="Email"
value={formik.values.email}
onChange={formik.handleChange}
error={formik.touched.email && Boolean(formik.errors.email)}
helperText={formik.touched.email && formik.errors.email}
/>
<Button color="primary" variant="contained" fullWidth type="submit">
Submit
</Button>
</form>
</div>
);
};
Test case
const mockSubmit = jest.fn();
describe("Test for formik onSubmit", () => {
it("should handle form submission", async () => {
const { getByTestId } = render(<WithMaterialUI />);
const email = getByTestId("email");
const form = getByTestId("form");
act(() => {
fireEvent.change(email, { target: { value: "abc#gmail.com" } });
});
act(() => {
fireEvent.submit(form);
});
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalled();
});
});
});
I created a working example using CodeSandbox. Could anyone please help?

Related

How to verify a react-google-recaptcha v2 using jest while testing a React-Typescript App?

So basically I've a Login form with two input fields (password, email) and a react-google-recaptcha. My use case is simple. Test if the submit button is disabled if the input fields are empty and recaptcha is not verified. Enable it only when input fields contain data and recaptcha is verified.
Below is the code that I wrote and I know I did something wrong with recaptcha verification in test file.
I've gone through existing answers in Stack Overflow for example this but facing problem implementing the same.
Login.tsx
import React, { useState } from "react";
import ReCAPTCHA from "react-google-recaptcha";
const Login = () => {
const [creds, setCreds] = useState({
email: "",
pw: "",
});
const [isCaptchaVerified, setIsCaptchaVerified] = useState(false);
const handleCaptchaChange = (value): void => {
if (value !== null) setIsCaptchaVerified(true);
else setIsCaptchaVerified(false);
};
return (
<div>
<input
data-testid="email-testid"
type="email"
name="email"
value={creds.email}
onChange={(e) => {
setCreds({
email: e.target.value,
pw: creds.pw,
});
}}
/>
<input
data-testid="pw-testid"
type="password"
name="password"
value={creds.pw}
onChange={(e) => {
setCreds({
pw: e.target.value,
email: creds.email,
});
}}
/>
<ReCAPTCHA
data-testid="login-recaptcha"
sitekey={siteKey}
onChange={handleCaptchaChange}
/>
<button data-testid="submit-testid" disabled={!isCaptchaVerified || !creds.pw ||
!creds.email}>
Submit
</button>
</div>
);
};
export default Login;
Login.test.tsx
test("test if button is disabled untill captcha is verified",()=> {
const loginRecaptcha = screen.findByTestId('login-recaptcha');
const emailField = screen.findByTestId('email-testid');
const pwField = screen.findByTestId('pw-testid');
const submitButton = screen.findByTestId('submit-testid');
expect(submitButton).toBeDisabled();
fireEvent.change(emailField, { target: { value: "user#test.com" } });
fireEvent.change(pwField, { target: { value: "user#1234" } });
fireEvent.click(loginRecaptcha);
expect(submitButton).not.toBeDisabled();
})

How reset form values using react bootstrap

My goal after clicking the register button is:
Make input fields blank
Do not show error tooltips
Here is the link on CodeSandbox
I've already tried using event.target.reset(); however the tooltips are still appearing on the screen.
export default function App() {
const [showSucessAlert, setshowSucessAlert] = useState(false);
const [validated, setValidated] = useState(false);
const [transitionAlert, setTransitionAlert] = useState(false);
const handleSubmit = (event) => {
const form = event.currentTarget;
event.preventDefault();
if (form.checkValidity() === false) {
event.stopPropagation();
} else {
handleClickTransitionAlert();
setshowSucessAlert(true);
}
setValidated(true);
};
const handleClickTransitionAlert = () => {
setTransitionAlert(true);
setTimeout(() => {
setTransitionAlert(false);
}, 1700);
};
return (
<Form noValidate validated={validated} onSubmit={handleSubmit}>
<Form.Group className="position-relative" controlId="validationPassword">
<Form.Label>Password</Form.Label>
<InputGroup hasValidation id="validationPassword" />
<Form.Control
type="password"
aria-describedby="validationPassword"
required
/>
<Form.Control.Feedback tooltip type="invalid">
Please enter your Password.
</Form.Control.Feedback>
</Form.Group>
<Alert
className={`mt-1 p-1 position-fixed ${
transitionAlert ? "alert-shown" : "alert-hidden"
}`}
show={showSucessAlert}
variant="success"
>
Registered user!
</Alert>
<Button className="mt-5" variant="primary" type="submit">
Register
</Button>
</Form>
);
}
Here is the link on CodeSandbox
Every help is welcome!
I don't commonly use uncontrolled components, but I think you could solve this by adding setValidated(false) and event.target.reset() to the handleClickTransitionAlert, like this:
const handleClickTransitionAlert = (event) => {
setTransitionAlert(true);
setTimeout(() => {
setTransitionAlert(false);
setValidated(false)
event.target.reset()
}, 1700);
};
Try reseting the validated attribute on Bootsrap Form.
it should look something like this (this is pseudo-code):
import React, { useRef, useState } from 'react';
const FormComponent = () => {
const [validated, setValidated] = useState(false);
const formRef = useRef(null);
const handleReset = () => {
formRef.current.reset();
setValidated(false);
};
const handleSubmit = () => {
// Do stuff here
// On success or error:
setValidated(true);
handleReset();
}
return(
<Form ref={formRef} validated={validated} onSubmit={handleSubmit}>
// your form inputs
</Form>
);
export default FormComponent;
}

When to use react useState? [duplicate]

This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Closed last year.
I have a simple registration form with 3 fields. I have stored the state in formValues with value & error associated with each field. Now when i submit the form without filling any or at least one field the form should be invalid but instead it shows validation messages with invalid fields but makes form valid. Even if i have added setTimeout the updated state is not available in the same handleSubmit. If i submit again the process works just fine. I understand that the state updation is async but if we see the logs in console the form's validation message is logged after formValues log in the render and those logs show that the state was updated correctly but the final validation message shows invalid state. If i change it to class component it works. Here's a link to codesandbox.
import React, { useState } from "react";
import { Button, Form, Col } from "react-bootstrap";
const sleep = timeout => new Promise(resolve => setTimeout(resolve, timeout));
const RegistrationForm = () => {
const [formValues, setFormValues] = useState({
name: { value: "", error: null },
email: { value: "", error: null },
password: { value: "", error: null }
});
const handleInputChange = (e, field) => {
const { value } = e.target;
setFormValues(prevValues => ({
...prevValues,
[field]: { value, error: null }
}));
};
const validateForm = () => {
let updatedFormValues = { ...formValues };
Object.keys(formValues).forEach(field => {
if (!formValues[field].value) {
updatedFormValues = {
...updatedFormValues,
[field]: { ...updatedFormValues[field], error: "required" }
};
}
});
setFormValues(updatedFormValues);
};
const isFormValid = () =>
Object.keys(formValues).every(field => formValues[field].error === null);
const handleSubmit = async e => {
e.preventDefault();
validateForm();
await sleep(100);
if (!isFormValid()) {
console.log("form is not valid", formValues);
return;
}
console.log("form is valid", formValues);
// make api call to complete registration
};
console.log({ formValues });
return (
<Form className="registration-form" onSubmit={handleSubmit}>
<Form.Row>
<Col>
<Form.Group controlId="name">
<Form.Label>Name</Form.Label>
<Form.Control
type="text"
placeholder="Enter name"
value={formValues.name.value}
onChange={e => handleInputChange(e, "name")}
/>
<Form.Control.Feedback type="invalid" className="d-block">
{formValues.name.error}
</Form.Control.Feedback>
</Form.Group>
</Col>
<Col>
<Form.Group controlId="email">
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
placeholder="Enter email"
value={formValues.email.value}
onChange={e => handleInputChange(e, "email")}
/>
<Form.Control.Feedback type="invalid" className="d-block">
{formValues.email.error}
</Form.Control.Feedback>
</Form.Group>
</Col>
</Form.Row>
<Form.Row>
<Col>
<Form.Group controlId="password">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
placeholder="Enter password"
value={formValues.password.value}
onChange={e => handleInputChange(e, "password")}
/>
<Form.Control.Feedback type="invalid" className="d-block">
{formValues.password.error}
</Form.Control.Feedback>
</Form.Group>
</Col>
<Col />
</Form.Row>
<Button variant="primary" type="submit">
Submit
</Button>
</Form>
);
};
export default RegistrationForm;
State updates are not just async but are als affected by closures in functional components, so using a sleep or timeout isn't going to leave your with an updated value in the same render cycle
You can read more about it in this post:
useState set method not reflecting change immediately
However, one solution in your case is to maintain a ref and toggle is value to trigger a useEffect in which you will validate the form post handleSubmit handler validates it and sets the formValues
Relevant code:
const validateFormField = useRef(false);
const handleInputChange = (e, field) => {
const { value } = e.target;
setFormValues(prevValues => ({
...prevValues,
[field]: { value, error: null }
}));
};
const validateForm = () => {
let updatedFormValues = { ...formValues };
Object.keys(formValues).forEach(field => {
if (!formValues[field].value) {
updatedFormValues = {
...updatedFormValues,
[field]: { ...updatedFormValues[field], error: "required" }
};
}
});
setFormValues(updatedFormValues);
validateFormField.current = !validateFormField.current;
};
const isFormValid = () =>
Object.keys(formValues).every(field => formValues[field].error === null);
const handleSubmit = async e => {
e.preventDefault();
validateForm();
// make api call to complete registratin
};
useEffect(() => {
if (!isFormValid()) {
console.log("form is not valid", formValues);
} else {
console.log("form is valid", formValues);
}
}, [validateFormField.current]); // This is fine since we know setFormValues will trigger a re-render
Working demo

Dispatch onBlur before send form

I have a form with an email that creates an account on the backend
and the user can change the email in this step. This code do this by putting the function in onBlur, but if I change the email in the input and don't leave the field, onBlur doesn't happen. I can click submit direct, sending my old email for account creation.
This is the code:
const SendForm = ({ submit }) => {
const onLabelSubmit = () => async (event, newValue, name) => {
handleLabelSubmit(newValue, name);
};
const submitForm = () => {
// validations
submit();
};
const handleSubmitAccount = (e) => {
e.preventDefault();
dispatch(submitAccount(field.name, field.email))
.then(() => {
submitForm();
});
};
return (
<form onSubmit={handleSubmitAccount}>
<Field
id="email"
name="email"
label="label"
onBlur={onLabelSubmit(label.email)}
/>
<Button type="submit">
Submit Form
</Button>
</form>
);
};
Is there any way to do what onBlur does, but when I click the submit button?
I need improving the experience and avoiding mistakes.
Thanks!
Add state to the component with useState.
Instead of onBlur use onChange and add value property to your input field, it will fire on every user input so you will always have the latest given email & username.
const SendForm = ({ submit }) => {
const [fields, setFields] = useState({ name: '', email: '' })
const onFieldChange = () => async (newValue, name) => {
setFields({ ...fields, [name]: newValue });
};
const submitForm = () => {
// validations
submit();
};
const handleSubmitAccount = (e) => {
e.preventDefault();
dispatch(submitAccount(field.name, field.email))
.then(() => {
submitForm();
});
};
return (
<form onSubmit={handleSubmitAccount}>
<Field
id="email"
name="email"
label="label"
value={fields.email}
onChange={onFieldChange(label.email)}
/>
<Button type="submit">
Submit Form
</Button>
</form>
);
};

How to reset the form after submit or after clicking on cancel button using formik

const TextForm: React.FunctionComponent<Props> = (props) => {
const formError = yup.object({
name: yup.string().required("Required"),
});
const formValidation = (fieldName) => {
return {
invalid: !!form.errors[fieldName] && form.touched[fieldName],
invalidText: form.errors[fieldName],
onBlur: form.handleBlur,
};
};
const form = useFormik({
initialValues: {
name,
id,
},
formValidation
formError,
validateOnChange: true,
validateOnMount: true,
initialTouched: {},
});
return(
<React.Fragment>
<form>
<TextInput
id="text-input-2"
{...validation("name")}
type="text"
name = "name"
onChange={(event) => {
setName(event.target.value);
form.handleChange(event);
}}
/>
<TextInput
id="text-input-2"
{...validation("id")}
type="text"
name = "id"
onChange={(event) => {
setId(event.target.value);
}}
/>
<Button>Clear</Button>
<Button>Submit</Button>
</form>
</React.Fragment>
)
}
Validations in my form are working fine. But if user does not enter any field, it comes with required warning. I am trying to clear/reset the form on Clear button click ,but could not find any possible solution working. Can anyone help me with this.
A quick search of the Formik docs.
The Formik onSubmit and onReset are passed formikBag as their second argument, of which contains the resetForm action. The resetForm callback can be destructured from this formikBag object and used within your callback.
onSubmit
onReset
const form = useFormik({
initialValues: {
name,
id,
},
formValidation
formError,
validateOnChange: true,
validateOnMount: true,
initialTouched: {},
onSubmit: (values, { resetForm }) => {
// submission logic
resetForm();
},
onReset: (values, { resetForm }) => resetForm(),
});
You also just need to ensure your form buttons have the correct button types so the form takes the correct action when clicked.

Categories

Resources