callstack / react-native-testing-library

🦉 Simple and complete React Native testing utilities that encourage good testing practices.
https://callstack.github.io/react-native-testing-library/
MIT License
3.08k stars 272 forks source link

Asking act on expected update state #821

Closed Cassiano-Esparta closed 2 years ago

Cassiano-Esparta commented 3 years ago

Hi, I am having a problem with act...

I am trying to test a hook-form submit, and when I click my button with fireEvent throws a giant error on console...

My test:

it('Should be able to click on zipcode button', async () => {
    const dismisMock = jest.spyOn(Keyboard, 'dismiss');

    const { getByTestId } = render(
      <Addresses testID="AddressesTest" />,
    );

    await waitFor(async () => {
      expect(getByTestId('ZipcodeContainerTest')).toBeTruthy();

      expect(getByTestId('ZipcodeButton')).toBeTruthy();
      fireEvent.press(getByTestId('ZipcodeButton'));
      expect(dismisMock).not.toHaveBeenCalled();

      expect(getByTestId('InputZipcode')).toBeTruthy();
      fireEvent.changeText(getByTestId('InputZipcode'), {
        target: { value: '78556142' },
      });
      expect(getByTestId('InputZipcode').props.value).toBe('78556-142');

      fireEvent.press(getByTestId('ZipcodeButton'));
      expect(dismisMock).toHaveBeenCalled();
    });
  });

My code:

const formProps = useForm({
  resolver: yupResolver(
    Yup.object().shape({
      zipcode: Yup.string()
        .required('Required')
        .min(9, 'Invalid zipcode'),
    }),
  ),
});

...

const onVerifyZipcode = value => {
  Keyboard.dismiss();
  setIsZipcodeLoading(true);

  if (cart != null) { // cart is coming from redux
      setZipcodeInternal(value.zipcode.replace('-', ''));
  } else {
    setIsZipcodeLoading(false);
    formProps.setError('zipcode', {
      type: 'manual',
      message: 'Invalid zipcode',
    });
  }
};

...

return (
  <View testID="ZipcodeContainerTest">
    <FormProvider {...formProps}>
      <View>
        <View>
          <InputOutlined
            testID="InputZipcode"
            name="zipcode"
            label="CEP"
            maxLength={9}
            keyboardType="numeric"
            mask="zip-code"
            returnKeyType="next"
          />
        </View>
        <View>
          <Button
            testID="ZipcodeButton"
            label="Aplicar"
            onPress={formProps.handleSubmit(onVerifyZipcode)}
            isLoading={isZipcodeLoading}
          />
        </View>
      </View>
    </FormProvider>
  </View>
);

Error:

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

  When testing, code that causes React state updates should be wrapped into act(...):

  act(() => {
    /* fire events that update state */
  });
  /* assert on the output */

  This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
      at Object.<anonymous>.exports.Controller (/Users/esparta/esparta/MM-native-app/node_modules/react-hook-form/src/useFormContext.tsx:21:6)
      at /Users/esparta/esparta/MM-native-app/App/Elements/InputOutlined/index.tsx:22:23
      at View
      at View (/Users/esparta/esparta/MM-native-app/node_modules/react-native/jest/mockComponent.js:19:18)
      at StyledNativeComponent (/Users/esparta/esparta/MM-native-app/node_modules/styled-components/native/dist/styled-components.native.cjs.js:5944:29)
      at View
      at View (/Users/esparta/esparta/MM-native-app/node_modules/react-native/jest/mockComponent.js:19:18)
      at StyledNativeComponent (/Users/esparta/esparta/MM-native-app/node_modules/styled-components/native/dist/styled-components.native.cjs.js:5944:29)
      at Object.<anonymous>.exports.FormProvider (/Users/esparta/esparta/MM-native-app/node_modules/react-hook-form/src/useFormContext.tsx:21:12)
      at View
      at View (/Users/esparta/esparta/MM-native-app/node_modules/react-native/jest/mockComponent.js:19:18)
      at StyledNativeComponent (/Users/esparta/esparta/MM-native-app/node_modules/styled-components/native/dist/styled-components.native.cjs.js:5944:29)
      at View
      at View (/Users/esparta/esparta/MM-native-app/node_modules/react-native/jest/mockComponent.js:19:18)
      at RCTScrollView
      at RCTScrollView (/Users/esparta/esparta/MM-native-app/node_modules/react-native/jest/setup.js:324:22)
      at ScrollView (/Users/esparta/esparta/MM-native-app/node_modules/react-native/jest/mockScrollView.js:21:52)
      at StyledNativeComponent (/Users/esparta/esparta/MM-native-app/node_modules/styled-components/native/dist/styled-components.native.cjs.js:5944:29)
      at View
      at View (/Users/esparta/esparta/MM-native-app/node_modules/react-native/jest/mockComponent.js:19:18)
      at StyledNativeComponent (/Users/esparta/esparta/MM-native-app/node_modules/styled-components/native/dist/styled-components.native.cjs.js:5944:29)
      at Addresses (/Users/esparta/esparta/MM-native-app/App/Containers/NewCartScreen/screens/AddressesLogged/index.tsx:44:48)
      at ThemeProvider (/Users/esparta/esparta/MM-native-app/node_modules/styled-components/native/dist/styled-components.native.cjs.js:5913:26)
      at CustomThemeProvider (/Users/esparta/esparta/MM-native-app/App/Hooks/Theme.tsx:25:49)

  at printWarning (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:68:30)
  at error (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:44:5)
  at warnIfNotCurrentlyActingUpdatesInDEV (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:15034:9)
  at readFormState (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:7129:9)
  at Object.observer [as next] (node_modules/react-hook-form/src/useFormState.ts:56:5)
  at Ae.[object Object] [as next] (node_modules/react-hook-form/src/utils/Subject.ts:52:7)
  at ke.submitCount [as next] (node_modules/react-hook-form/src/utils/Subject.ts:64:3)
  at node_modules/react-hook-form/src/useForm.ts:1081:11

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

  When testing, code that causes React state updates should be wrapped into act(...):

  act(() => {
    /* fire events that update state */
  });
  /* assert on the output */

  This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
      at Addresses (/Users/esparta/esparta/MM-native-app/App/Containers/NewCartScreen/screens/AddressesLogged/index.tsx:44:48)
      at ThemeProvider (/Users/esparta/esparta/MM-native-app/node_modules/styled-components/native/dist/styled-components.native.cjs.js:5913:26)
      at CustomThemeProvider (/Users/esparta/esparta/MM-native-app/App/Hooks/Theme.tsx:25:49)

  at printWarning (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:68:30)
  at error (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:44:5)
  at warnIfNotCurrentlyActingUpdatesInDEV (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:15034:9)
  at isValid (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:7129:9)
  at Object.observer [as next] (node_modules/react-hook-form/src/useForm.ts:1208:68)
  at Ae.[object Object] [as next] (node_modules/react-hook-form/src/utils/Subject.ts:52:7)
  at ke.submitCount [as next] (node_modules/react-hook-form/src/utils/Subject.ts:64:3)
  at node_modules/react-hook-form/src/useForm.ts:1081:11

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

  When testing, code that causes React state updates should be wrapped into act(...):

  act(() => {
    /* fire events that update state */
  });
  /* assert on the output */

  This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
      at Object.<anonymous>.exports.Controller (/Users/esparta/esparta/MM-native-app/node_modules/react-hook-form/src/useFormContext.tsx:21:6)
      at /Users/esparta/esparta/MM-native-app/App/Elements/InputOutlined/index.tsx:22:23
      at View
      at View (/Users/esparta/esparta/MM-native-app/node_modules/react-native/jest/mockComponent.js:19:18)
      at StyledNativeComponent (/Users/esparta/esparta/MM-native-app/node_modules/styled-components/native/dist/styled-components.native.cjs.js:5944:29)
      at View
      at View (/Users/esparta/esparta/MM-native-app/node_modules/react-native/jest/mockComponent.js:19:18)
      at StyledNativeComponent (/Users/esparta/esparta/MM-native-app/node_modules/styled-components/native/dist/styled-components.native.cjs.js:5944:29)
      at Object.<anonymous>.exports.FormProvider (/Users/esparta/esparta/MM-native-app/node_modules/react-hook-form/src/useFormContext.tsx:21:12)
      at View
      at View (/Users/esparta/esparta/MM-native-app/node_modules/react-native/jest/mockComponent.js:19:18)
      at StyledNativeComponent (/Users/esparta/esparta/MM-native-app/node_modules/styled-components/native/dist/styled-components.native.cjs.js:5944:29)
      at View
      at View (/Users/esparta/esparta/MM-native-app/node_modules/react-native/jest/mockComponent.js:19:18)
      at RCTScrollView
      at RCTScrollView (/Users/esparta/esparta/MM-native-app/node_modules/react-native/jest/setup.js:324:22)
      at ScrollView (/Users/esparta/esparta/MM-native-app/node_modules/react-native/jest/mockScrollView.js:21:52)
      at StyledNativeComponent (/Users/esparta/esparta/MM-native-app/node_modules/styled-components/native/dist/styled-components.native.cjs.js:5944:29)
      at View
      at View (/Users/esparta/esparta/MM-native-app/node_modules/react-native/jest/mockComponent.js:19:18)
      at StyledNativeComponent (/Users/esparta/esparta/MM-native-app/node_modules/styled-components/native/dist/styled-components.native.cjs.js:5944:29)
      at Addresses (/Users/esparta/esparta/MM-native-app/App/Containers/NewCartScreen/screens/AddressesLogged/index.tsx:44:48)
      at ThemeProvider (/Users/esparta/esparta/MM-native-app/node_modules/styled-components/native/dist/styled-components.native.cjs.js:5913:26)
      at CustomThemeProvider (/Users/esparta/esparta/MM-native-app/App/Hooks/Theme.tsx:25:49)

  at printWarning (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:68:30)
  at error (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:44:5)
  at warnIfNotCurrentlyActingUpdatesInDEV (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:15034:9)
  at readFormState (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:7129:9)
  at Object.observer [as next] (node_modules/react-hook-form/src/useFormState.ts:56:5)
  at Ae.[object Object] [as next] (node_modules/react-hook-form/src/utils/Subject.ts:52:7)
  at ke.submitCount [as next] (node_modules/react-hook-form/src/utils/Subject.ts:64:3)
  at node_modules/react-hook-form/src/useForm.ts:1081:11

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

  When testing, code that causes React state updates should be wrapped into act(...):

  act(() => {
    /* fire events that update state */
  });
  /* assert on the output */

  This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
      at Addresses (/Users/esparta/esparta/MM-native-app/App/Containers/NewCartScreen/screens/AddressesLogged/index.tsx:44:48)
      at ThemeProvider (/Users/esparta/esparta/MM-native-app/node_modules/styled-components/native/dist/styled-components.native.cjs.js:5913:26)
      at CustomThemeProvider (/Users/esparta/esparta/MM-native-app/App/Hooks/Theme.tsx:25:49)

  at printWarning (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:68:30)
  at error (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:44:5)
  at warnIfNotCurrentlyActingUpdatesInDEV (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:15034:9)
  at isValid (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:7129:9)
  at Object.observer [as next] (node_modules/react-hook-form/src/useForm.ts:1208:68)
  at Ae.[object Object] [as next] (node_modules/react-hook-form/src/utils/Subject.ts:52:7)
  at ke.submitCount [as next] (node_modules/react-hook-form/src/utils/Subject.ts:64:3)
  at node_modules/react-hook-form/src/useForm.ts:1081:11

● Addresses component › Should be able to click on zipcode button

expect(jest.fn()).toHaveBeenCalled()

Expected number of calls: >= 1
Received number of calls:    0

  165 |
  166 |       fireEvent.press(getByTestId('ZipcodeButton'));
> 167 |       expect(dismisMock).toHaveBeenCalled();
      |                          ^
  168 |     });
  169 |   });
  170 | });

  at _callee6$ (App/__tests__/containers/new-cart-screens/Addresses.spec.tsx:167:26)
  at tryCatch (node_modules/regenerator-runtime/runtime.js:63:40)
  at Generator.invoke [as _invoke] (node_modules/regenerator-runtime/runtime.js:294:22)
  at Generator.next (node_modules/regenerator-runtime/runtime.js:119:21)
  at tryCatch (node_modules/regenerator-runtime/runtime.js:63:40)
  at invoke (node_modules/regenerator-runtime/runtime.js:155:20)
  at node_modules/regenerator-runtime/runtime.js:190:11
  at tryCallTwo (node_modules/react-native/node_modules/promise/lib/core.js:45:5)
  at doResolve (node_modules/react-native/node_modules/promise/lib/core.js:200:13)
  at new Promise (node_modules/react-native/node_modules/promise/lib/core.js:66:3)

Versions: "@testing-library/jest-native": "^4.0.1", "@testing-library/react-native": "^7.2.0", "jest": "^26.6.3", "jest-expo": "^42.1.0", "react-test-renderer": "17.0.2", "react-hook-form": "^7.9.0", "react-native": "0.63.4",

I think those are the necessary ones, if i let one out just ask :D

jest.config.js (if needed)

module.exports = {
  preset: 'jest-expo',
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
  setupFiles: ["./node_modules/react-native-gesture-handler/jestSetup.js",'./App/__tests__/jestSetupFile.ts'],
  setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'],
  modulePathIgnorePatterns: ['App/__tests__/jestSetupFile.ts'],
  testPathIgnorePatterns: [
    './node_modules/',
    './App/Config/',
    './App/Navigation/',
  ],
  collectCoverageFrom: [
    'App/Components/Madeira/**/*.tsx',
    'App/Elements/**/*.tsx',
    'App/Containers/**/*.tsx',
  ],
  transformIgnorePatterns: [
    'node_modules/(?!(jest-)?react-native|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|@sentry/.*|@ptomasroos/react-native-multi-slider|react-native-gesture-handler/.*|@reduxjs/toolkit|@react-native-picker)',
  ],
};

I've tried to use the act function, but not seems to work in my case... I'm forgetting something? Or doing something wrong?

I've tried to do as this issue but nothing worked...

guvenkaranfil commented 3 years ago

hello, @Cassiano-Esparta I had experienced this warning before. But I can not remember what was the situation though my guess is about something async behavior. As I can see yupResolver async function is defined https://github.com/react-hook-form/resolvers/blob/72aa95c25b0a14f78822cee32517120d6ede4cde/yup/src/yup.ts#L38.

So Can you please provide me with a sample project? I want to dive deep into this warning. Thanks

pierrezimmermannbam commented 2 years ago

Yes I think @guvenkaranfil is right, the issue here seems to be that you trigger your handleSubmit function which is asynchronous and it probably updates some states after some promises are resolved, the issue being here that you don't wait in your test for this state update because you're only testing the fact that keyboard dismiss has been called.

First, I'd avise against wrapping your whole test in waitFor because we don't know anymore when we are waiting or not and I'm not sure having events fired within a waitFor is a good pattern because you're not actually waiting for them and do not want them to repeat should your assertions fail. I'd recommend putting only one assertions in each waitfor and keeping events out of it.

To fix the warning, you can either mock the function that is called on your form submit so that it isn't asynchronous but it's not the best here i think, otherwise you should await for something that will be true only once your form will be submitted, for instance that your loader is not visible anymore. The warning says that

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

So I guess Addresses is one of your component and it has a state which is updated after promises are resolved, so if there is a visual way to see it that's what you should expect in your test, like

fireEvent.press(getByTestId('ZipcodeButton'));

await waitFor(() => { 
    // expect something on adresses, for instance query a text
}

expect(dismisMock).toHaveBeenCalled();

A way to remember how to fix this warning is when you trigger an event, you can ask yourself how would the user tell if the action is complete (in our case the form submit) and assert on that when possible

Hope it helps !

AugustinLF commented 2 years ago

You can also try

await act(async () => {
   fireEvent.changeText(getByTestId('InputZipcode'), {
        target: { value: '78556142' },
      });
})

@pierrezimmermannbam explained well the overall problem, and I do not believe the problem results from an issue with the library. If you still can't fix it, please post a minimal repro so we can try to figure out what's happening, because I don't think we can do more to help you now.

Meanwhile, I'll close the issue since I believe it was answered. I'll reopen if needed.