reactjsreact-hooksjestjsreact-testing-library

Jest and React Testing Library not calling functions inside onChange handler


I am trying to test button enabling and disabling in my React component. The problem is that the button is never enabled and it's never enabled because of my calls to validateEmail() and validateForm() inside handleInputChange(). I tried removing those calls and just setting isValid to true and it worked.

Functions inside my Contact functional component:

const [formData, setFormData] = useState({
  firstName: '',
  lastName: '',
  email: '',
  message: '',
})
const [isValid, setIsValid] = useState(false)

function validateForm(obj) {
  return Object.values(obj).every((v) => v.length > 0)
}

const validateEmail = (email) => {
  let re = /\S+@\S+\.\S+/
  return re.test(email)
}

const handleInputChange = (e) => {
  const { name, value } = e.target

  setFormData({
    ...formData,
    [name]: value,
  })

  if (validateEmail(formData.email) && validateForm(formData)) {
    setIsValid(true)
  }
}

And then I have a couple of inputs and a button:

<InputField
  type="email"
  name="email"
  id="email"
  autocomplete="email"
  onChange={(e) => handleInputChange(e)}
  label="Email"
  value={formData.email}
  testId="email"
/>
<Button
  type="submit"
  text="Send message"
  disabled={!isValid}
  testId="submit"
/>

My test:

test('send button is enabled when fields are not empty', async () => {
  render(<Contact />);

  let firstnameVal = "Test First Name";
  let lastnameVal = "Test Last Name";
  let email = "email@email.com";
  let message = "This is a message";

  fireEvent.change(screen.getByTestId("first-name"), {target: {value: firstnameVal}});
  fireEvent.change(screen.getByTestId("last-name"), {target: {value: lastnameVal}});
  fireEvent.change(screen.getByTestId("email"), {target: {value: email}});
  fireEvent.change(screen.getByTestId("message"), {target: {value: message}});

  expect(screen.getByTestId('submit')).toBeEnabled();
});

How can I fix this? Am I missing something?


Solution

  • In React, state updates via the setState (or setFormData in your case) function are asynchronous. This means that when you call setFormData, the formData state does not immediately update to the new value. Instead, the update is scheduled, and the component will re-render with the new state at a later time. If you want to use the actual value immediately, you can use the value from the event instead of the state(formData in your case). Try this approach.

    const handleInputChange = (e) => {
      const { name, value } = e.target;
    
      const newValues = {
        ...formData,
        [name]: value,
      };
    
      setFormData(newValues);
    
      if (validateEmail(newValues.email) && validateForm(newValues)) {
        setIsValid(true);
      }
    };
    

    Further I have a doubt why you using testId instead of data-testid