Update not wrapped in act(...) and the mock function not firing. react-testing-library, jest

Viewed 967

I would love some help on this. I've been working on it for too long to deem not asking a question.

Thank you in advance for your time and attention,

I have scoured the internet and tried many things but I am still having no luck - here's the issue.

All in all - I am following along with https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning

Using react-testing-library in tandem with jest - and running into two issues

  1. Warning: An update to Login inside a test was not wrapped in act(...).
  2. Jest isn’t picking up that the mock is being called

I really think I have everything wired up correctly - unsure what I am missing. Inside the mock I can put a console.log and can confirm that the mock is indeed being called.

/*
  Test
*/
it('should call props.fetchSignIn with the username and password', async () => {
  const promise = Promise.resolve({
    email: 'test@circulate.social',
    firstName: 'Mike',
    lastName: 'A',
  });
  const fetchSignIn = jest.fn(() => promise);
  const { queryByTestId, queryByPlaceholderText } = renderLogin({
    fetchSignIn,
  });
  const emailInput = queryByPlaceholderText('joedoe@gmail.com');
  const passwordInput = queryByPlaceholderText('password');
  const submitButton = queryByTestId('submitButton');

  fireEvent.change(emailInput, {
    target: { value: 'mike@circulate.social' },
  });
  fireEvent.change(passwordInput, { target: { value: 'Password1!' } });

  fireEvent.click(submitButton);

  // I expected this to fail from the parameters being wrong
  expect(fetchSignIn).toHaveBeenCalledWith('asf');

  await act(() => promise);
});
/*
  Functional Component using the `useState(...)` hooks
  onFormFinished is the form `onSubmit` handler
*/
  const handleSignIn = async (
    email: string,
    password: string
  ): Promise<UserContextType['user']> => {
    setIsLoginInFlight(true);
    try {
      const result = await props.fetchSignIn(email, password);
      setIsLoginInFlight(false);
      return result;
    } catch (error) {
      // Other logic removed
  };

  const onFormFinish = async (values: FormValues): Promise<void> => {
    const { email, password } = values;
    const signInResult = await handleSignIn(email, password);

    // Other logic removed
  };

Two things are happening 1 - That warning 2 - Test is failing because fetchSignIn was never called

What is expected The test to fail due to fetchSignIn being called, but the parameters being wrong

Any input or ask for clarification is welcome.

Thank you

3 Answers

Let's address your second problem first.

Jest isn’t picking up that the mock is being called

I find it is tricky as well to get test running with ant design forms. After some digging and experiments, it seems simulating 'click' doesn't trigger the onFinish, but instead, simulating 'submit' does. Imaging the following example using Jest and Enzyme:

export default MyForm = () => {
    <Form onFinish={onFinish}>
       <Form.Item>
         <Button htmlType='submit'>Submit</Button>
       </Form.Item>
    </Form>
}

it('should trigger onFinish if submit', () => {
   const onFinish = jest.fn();
   const wrapper = mount(<MyForm onFinish={onFinish}/>);
   wrapper.find('button').simulate('submit');
   await sleep(200)
   expect(onFinish).toHaveBeenCalled();
})

Myself is also struggling with the first issue:

Warning: An update to Login inside a test was not wrapped in act(...)

Note that this will only happen if there is validation error because there is internal state change in their implementation of the Form.

First of all, this is a warning meaning that you can ignore it if you don't mind the log to be polluted.

Secondly, this is a warning only when it is 'development' but not 'production'.

If you indeed want to make it disappear, this is the "hacky" way (I don't know if there is better way) I am taking:

it('should trigger onFinish if submit', () => {
   const onFinish = jest.fn();
   const wrapper = mount(<MyForm onFinish={onFinish}/>);
   await act(async () => {
     wrapper.find('button').simulate('submit');
   })
   await sleep(200)
   wrapper.update()
   // check errors here
   expect(onFinish).not.toHaveBeenCalled();
})

This is officially documented here: https://github.com/enzymejs/enzyme

I had some trouble too submitting forms with Ant Design 4 + react-testing-library. After some experimentation I ended with always using data-testid to submit forms :

<Button data-testid="submit" htmlType="submit">Submit</>

// Form element has onFinish attribute

// in test
fireEvent.click(rtl.getByTestId("submit"));
expect(...);

This is not what RTL promotes (datat-testid should be avoided, test as a real user would) but I can live with that.

But according to https://github.com/ant-design/ant-design/issues/21272 firing a submit event should work too, I will definitely give a try.

Bordering on comedic, and definitely NOT the proper way to solve the problem. But A way to solve the problem - the only one that worked for me besides avoiding using onFinish entirely and relying on onClick on the button.

So what did the trick is actually wrapping the expect(...) in a setTimeout(...)

      // click || submit - both seemed to work for me
      fireEvent.submit(submitButton);
      setTimeout(() => {
        expect(fetchSignIn).toHaveBeenCalledWith('asf');
      });
    it('should call props.fetchSignIn with the username and password', async () => {
      const promise = Promise.resolve({
        email: 'test@circulate.social',
        firstName: 'Mike',
        lastName: 'A',
      });
      const fetchSignIn = jest.fn(() => promise);
      const { queryByTestId, queryByPlaceholderText } = renderLogin({
        seedEmail: 'aasdf',
        seedPassword: 'asdf',
        fetchSignIn,
      });
      const emailInput = queryByPlaceholderText('joedoe@gmail.com');
      const passwordInput = queryByPlaceholderText('password');
      const submitButton = queryByTestId('submitButton');

      fireEvent.change(emailInput, {
        target: { value: 'mike@circulate.social' },
      });
      fireEvent.change(passwordInput, { target: { value: 'Password1!' } });

      // click || submit - both seemed to work for me
      fireEvent.submit(submitButton);
      setTimeout(() => {
        expect(fetchSignIn).toHaveBeenCalledWith('asf');
      });

      await act(() => promise);
    });

This was brought up to me by someone on the GitHub issue https://github.com/ant-design/ant-design/issues/21272#issuecomment-628607141 so shoutout to @ildarnm on GH

Related