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.05k stars 271 forks source link

Error thrown - Warning: You called act(async () => ...) without await. #379

Closed jpmonette closed 1 year ago

jpmonette commented 4 years ago

Ask your Question

I have a simple test that seems to generate the right snapshot at the end of execution, but throws me a console.error during the execution.

Here are the steps to get to the expected component state:

  1. Load the component
  2. Wait for the useEffect asynchronous logic to be executed so that the SegmentedControlIOS is exposed (testID = recordTypePicker)
  3. Set the selectedSegmentIndex to "1"
  4. Wait for the component to re-render and all the async logic to be executed
  5. Assert that the rendered component includes newSObjectLayout testID

The test

import * as React from 'react';
import { fireEvent, render, waitFor } from 'react-native-testing-library';
import { NewSObjectContainer } from '../NewSObject';

describe('NewSObjectContainer', () => {
  const setup = () => {
    const route = { params: { sobject: 'Account' } };
    const navigation = { setOptions: jest.fn() };

    const container = render(<NewSObjectContainer route={route} navigation={navigation} />);
    return { container };
  };

  it('should render a NewSObjectContainer - with page layout exposed', async () => {
    const { container } = setup();

    await waitFor(() => expect(container.getByTestId('recordTypePicker')).toBeTruthy());

    const input = container.getByTestId('recordTypePicker');
    fireEvent(input, 'onChange', { nativeEvent: { selectedSegmentIndex: 1 } });

    await waitFor(() => expect(container.getByTestId('newSObjectLayout')).toBeTruthy());

    expect(container.toJSON()).toMatchSnapshot();
  });
});

The console log

./node_modules/.bin/jest src/containers/__tests__/NewSObject.spec.tsx --u
  console.error
    Warning: You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);

      at CustomConsole.console.error (node_modules/react-native/Libraries/YellowBox/YellowBox.js:63:9)
      at printWarning (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:120:30)
      at error (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:92:5)
      at node_modules/react-test-renderer/cjs/react-test-renderer.development.js:14953:13
      at tryCallOne (node_modules/promise/lib/core.js:37:12)
      at node_modules/promise/lib/core.js:123:15
      at flush (node_modules/asap/raw.js:50:29)

 PASS  src/containers/__tests__/NewSObject.spec.tsx
  NewSObjectContainer
    ✓ should render a NewSObjectContainer - with page layout exposed (499ms)

 › 1 snapshot written.
Snapshot Summary
 › 1 snapshot written from 1 test suite.

Test Suites: 1 passed, 1 total
Tests:       q passed, q total
Snapshots:   1 written, 1 passed, q total
Time:        4.865s, estimated 8s
Ran all test suites matching /src\/containers\/__tests__\/NewSObject.spec.tsx/i.
JanithaR commented 3 years ago

This leaves your code with following warning logged to console

Warning: You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);

I’ve noticed that this error/warning happens when I use more than 1 await within a it statement

This 🤨. I just tested out the theory on some of my tests and spot on.

piotrponikowski commented 3 years ago

Same issue here, with multiple await. Workaround from this comment works for me: https://github.com/callstack/react-native-testing-library/issues/379#issuecomment-714341282

JanithaR commented 3 years ago

Same issue here, with multiple await. Workaround from this comment works for me: #379 (comment)

DUDE! 😘

normanzb commented 3 years ago

I don't thing we support putting jest's expect into waitFor.

how came, expect is even used in the sample in the doc: https://testing-library.com/docs/dom-testing-library/api-async/#waitfor

samir-kutty commented 3 years ago

A clear winner without the global promise hacks. #379 (comment)

Thank you kind sir! @babylone-star

getsaf commented 2 years ago

For us, the issue seems to be due to the detection that calls to act are properly "awaited".

I patched that package and modified the detection to use finally instead of the double-then method. The end result was that most tests now pass without warnings now.

Patch for react-test-renderer@17.0.2

before

node_modules/react-test-renderer/cjs/react-test-renderer.development.js

Promise.resolve().then(function () {}).then(function () {
if (called === false) {
error('You called act(async () => ...) without await. ' + 'This could lead to unexpected testing behaviour, interleaving multiple act ' + 'calls and mixing their scopes. You should - await act(async () => ...);');
}
});

after

        result.finally(function () {
          if (called === false) {
            error('You called act(async () => ...) without await. ' + 'This could lead to unexpected testing behaviour, interleaving multiple act ' + 'calls and mixing their scopes. You should - await act(async () => ...);');
          }
        });

Full patch for patch-package:

diff --git a/node_modules/react-test-renderer/cjs/react-test-renderer.development.js b/node_modules/react-test-renderer/cjs/react-test-renderer.development.js
index f401dd2..7b84f7d 100644
--- a/node_modules/react-test-renderer/cjs/react-test-renderer.development.js
+++ b/node_modules/react-test-renderer/cjs/react-test-renderer.development.js
@@ -15292,7 +15292,9 @@ function act(callback) {
     {
       if (typeof Promise !== 'undefined') {
         //eslint-disable-next-line no-undef
-        Promise.resolve().then(function () {}).then(function () {
+        // This patch makes React's act play nicer with React Native
+        // https://github.com/facebook/react/issues/22634
+        result.finally(function () {}).then(function () {
           if (called === false) {
             error('You called act(async () => ...) without await. ' + 'This could lead to unexpected testing behaviour, interleaving multiple act ' + 'calls and mixing their scopes. You should - await act(async () => ...);');
           }

Edits:

thymikee commented 2 years ago

@getsaf have you posted this to the React core team maybe? I wonder what's their stand on it

getsaf commented 2 years ago

@thymikee Not yet, I posted here because this is the most informative thread I found. I'll need some time to run through the issue submission process and TBH, I have an assumption about why this fails, but would like to verify my assumption.

Edit: I have effectively disproven that node-versions are the issue. Same error occurs in earlier version of node (v10) as well as newer (v14).

I have no real explanation for how this fixes the issue except that it affects the promise chain timing in such a way that then is guaranteed to be called before finally

thenriquedb commented 2 years ago

I was having the same problem when I tried to use three findBy in a row. The error disappeared when replacing the last two calls with a getBy*.

Before

  it('should ', async () => {
    // ...

    const { getByTestId, getByText, findByText } = setup();

    const street = await findByText(data.street);
    const neighborhood = await findByText(data.neighborhood);
    const city = await findByText(`${data.city}, ${data.state}`);

    expect(street).toBeTruthy();
    expect(neighborhood).toBeTruthy();
    expect(city).toBeTruthy();
  });

After

  it('should ', async () => {
    // ...

    const { getByTestId, getByText, findByText } = setup();

    const street = await findByText(data.street);
    const neighborhood = getByText(data.neighborhood);
    const city = getByText(`${data.city}, ${data.state}`);

    expect(street).toBeTruthy();
    expect(neighborhood).toBeTruthy();
    expect(city).toBeTruthy();
  });
power-f-GOD commented 2 years ago

This fixed the error for me (the last test block):


import {
  screen,
  render,
  fireEvent,
  act,
  waitFor,
} from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";

import VerifyAccount from "./index";
import { BrowserRouter } from "react-router-dom";

jest.mock("src/services/users");

let gotoLoginButton;
let resendLinkButton;

beforeEach(() => {
  act(() => {
    render(
      <VerifyAccount match={{ params: { email: "johndoe@example.com" } }} />,
      { wrapper: BrowserRouter },
    );
  });
  gotoLoginButton = screen.getByText("Login to your account");
  resendLinkButton = screen.getByText("Resend verification link");
});

test("renders verify-account page buttons", () => {
  expect(gotoLoginButton).toBeInTheDocument();
  expect(gotoLoginButton).toHaveAttribute("href", "/login");
  expect(resendLinkButton).toBeInTheDocument();
});

test("verify-account page state changes after button click", async () => {
  await act(async () => {
    fireEvent.click(resendLinkButton);
  });
  waitFor(() => {
    expect(resendLinkButton.textContent).toMatch("Resending link...");
  });
});
alterx commented 2 years ago

Anyone else getting this warning with the basic React Native example?

import React from 'react'
import {Button, Text, TextInput, View} from 'react-native'
import {fireEvent, render, waitFor} from '@testing-library/react-native'

function Example() {
  const [name, setUser] = React.useState('')
  const [show, setShow] = React.useState(false)

  return (
    <View>
      <TextInput value={name} onChangeText={setUser} testID="input" />
      <Button
        title="Print Username"
        onPress={() => {
          // let's pretend this is making a server request, so it's async
          // (you'd want to mock this imaginary request in your unit tests)...
          setTimeout(() => {
            setShow(!show)
          }, Math.floor(Math.random() * 200))
        }}
      />
      {show && <Text testID="printed-username">{name}</Text>}
    </View>
  )
}

test('examples of some things', async () => {
  const {getByTestId, getByText, queryByTestId, toJSON} = render(<Example />)
  const famousProgrammerInHistory = 'Ada Lovelace'

  const input = getByTestId('input')
  fireEvent.changeText(input, famousProgrammerInHistory)

  const button = getByText('Print Username')
  fireEvent.press(button)

  await waitFor(() => expect(queryByTestId('printed-username')).toBeTruthy())

  expect(getByTestId('printed-username').props.children).toBe(
    famousProgrammerInHistory,
  )
  expect(toJSON()).toMatchSnapshot()
})
AugustinLF commented 2 years ago

@alterx What version are you using? Didn't warn for me on 7.1 or on 9.0.

alterx commented 2 years ago

@AugustinLF I'm on 9.0.0, literally just copy and pasting that example throws the warning (the test passes, tho)

sasweb commented 2 years ago

I cannot confirm that this is happening for the React Native example @alterx However, I always run into this issue as soon as multiple waitFor are chained. For example, when we extend the React Native example by another waitFor I get the warning.

test('examples of some things', async () => {
  const { getByTestId, getByText, queryByTestId, toJSON } = render(<Example />)
  const famousProgrammerInHistory = 'Ada Lovelace'

  const input = getByTestId('input')
  fireEvent.changeText(input, famousProgrammerInHistory)

  const button = getByText('Print Username')
  fireEvent.press(button)

  await waitFor(() => expect(queryByTestId('printed-username')).toBeTruthy())

  expect(getByTestId('printed-username').props.children).toBe(famousProgrammerInHistory)

  // Just repeating here but could be any other waitFor
  // As soon as there is more than 1 waitFor the error message is thrown
  await waitFor(() => expect(queryByTestId('printed-username')).toBeTruthy())

  expect(getByTestId('printed-username').props.children).toBe(famousProgrammerInHistory)

  expect(toJSON()).toMatchSnapshot()
})
alterx commented 2 years ago

Thanks @pawelsas , but in this specific situation that's not the case. I'm literally copy and pasting the example code from the project's page. I'm clueless tbh

gsieczkowski10clouds commented 2 years ago

Maybe this will be helpful for someone. In my team we have a set of rules for writing tests with jest and testing library:

Terceramayor commented 2 years ago

Hi all, just wanted to mention the solution I found to avoid the warning for when there are multiple awaits within the it callback. I manage to get rid off the warning by wrapping all the await assignments within an async IIFE. Something Like:


it("My test", async () => {
      (async () => {
        const { getByTestId, debug } = await myAsyncRenderFunction()
        const button = await waitFor(() =>
          getByTestId(`element`),
        )
        fireEvent.press(button)
        expect(element).toBeDefined()
      })()
    })
milesj commented 2 years ago

@Terceramayor The IIFE itself doesn't seem to be awaited?

Terceramayor commented 2 years ago

@milesj Indeed you're right. I just tried by removing the async keyword on the it callback and the result is the same; the test passes with no warnings. So this seems to be a way to wrap all the async processes within an async IIFE where the it() only "sees" the invocation of one async function. Still it's not fully clear to me why the IIFE doesn't seem to require to be awaited.

jfpalacios commented 2 years ago

@Terceramayor I don't think the tests are correct if you do that.. any change I make still results in a pass

rickyalmeidadev commented 2 years ago

I was having the same problem when I tried to use three findBy in a row. The error disappeared when replacing the last two calls with a getBy*.

Before

  it('should ', async () => {
    // ...

    const { getByTestId, getByText, findByText } = setup();

    const street = await findByText(data.street);
    const neighborhood = await findByText(data.neighborhood);
    const city = await findByText(`${data.city}, ${data.state}`);

    expect(street).toBeTruthy();
    expect(neighborhood).toBeTruthy();
    expect(city).toBeTruthy();
  });

After

  it('should ', async () => {
    // ...

    const { getByTestId, getByText, findByText } = setup();

    const street = await findByText(data.street);
    const neighborhood = getByText(data.neighborhood);
    const city = getByText(`${data.city}, ${data.state}`);

    expect(street).toBeTruthy();
    expect(neighborhood).toBeTruthy();
    expect(city).toBeTruthy();
  });

Your approach solved the problem in my case. Thanks for sharing.

cgorski28 commented 2 years ago

This leaves your code with following warning logged to console

Warning: You called act(async () => ...) without await. This could lead to unexpected testing behaviour, interleaving multiple act calls and mixing their scopes. You should - await act(async () => ...);

I’ve noticed that this error/warning happens when I use more than 1 await within a it statement

Same issue here. I tried all the things I mentioned earlier, but none worked here.

Be sure to only use await findBy* once and replace the rest of your findBy* 's with getBy* 's. This solved the issue for me.

janzenz commented 2 years ago

@cgorski28 it solved some of my tests but what if you do need multiple findBy* in your test? Has anyone encountered this?

rijk commented 2 years ago

@janzenz you can do this using a non-async test and then:

it('supports multiple findBys', (done) => {
  const { findByText } = render(<Item />);

  findByText('one').then(() => {
    // ...

    findByText('two').then(done);
  });
});

However, in most cases one findBy (or waitFor) is enough; once the first element is found the rest is usually there as well.

seyaobey-dev commented 2 years ago

@rijk I have tried your solution for multiple findBy* by I'm getting this (rather nasty) error, not even sure what it means:

Failed: Array [
      ReactTestInstance {
        "_fiber": FiberNode {
          "_debugHookTypes": null,
          "_debugID": 614,
          "_debugNeedsRemount": false,
          "_debugOwner": [FiberNode],
          "_debugSource": null,
          "actualDuration": 0,
          "actualStartTime": -1,
          "alternate": null,
          "child": [FiberNode],
          "childLanes": 0,
          "dependencies": null,
          "elementType": "View",
          "firstEffect": [FiberNode],
          "flags": 0,
          "index": 0,
          "key": null,
          "lanes": 0,
          "lastEffect": [FiberNode],
          "memoizedProps": [Object],
          "memoizedState": null,
          "mode": 0,
          "nextEffect": null,
          "pendingProps": [Object],
          "ref": null,
          "return": [FiberNode],
          "selfBaseDuration": 0,
          "sibling": null,
          "stateNode": [Object],
          "tag": 5,
          "treeBaseDuration": 0,
          "type": "View",
          "updateQueue": null,
        },
      },
    ]

      at Env.fail (node_modules/jest-jasmine2/build/jasmine/Env.js:722:61)
rijk commented 2 years ago

It was example code I typed, not to be taken literally. I guess the error is because when you do findBy().then(done) it passes the result of the findBy to done which you are not supposed to be doing.

An actual code example using waitFor, but similar concept:

it('should hide bottom sheet when clicking on Later button and NOT show it again', (done) => {
  render(inbox);

  // [code that Clicks Later button]

  waitFor(() => expect(BottomSheet.hide).toBeCalled()).then(() => {
    (BottomSheet.show as jest.Mock).mockClear();

    render(inbox);

    waitFor(() => expect(BottomSheet.show).not.toBeCalled()).then(done);
  });
});
tcank commented 2 years ago

For the case of multiples await waitFor in the same test case, arriving in "You called act(async () => ...) without await" warning, we found a solution in this stack overflow post https://stackoverflow.com/questions/64952449/react-native-testing-act-without-await/69201830#69201830 There is more info about our case on a similar issue opened to react-hooks-testing-library: https://github.com/testing-library/react-hooks-testing-library/issues/825#issuecomment-1119588405

mdjastrzebski commented 2 years ago

@tcank that's a very interesting trail. Great thanks for posting it here. I will try to dig deeper in the coming days/weeks.

thymikee commented 2 years ago

FYI, this is something that we're already doing in our own preset, but for the sake of modern timers: https://github.com/callstack/react-native-testing-library/blob/main/jest-preset/index.js

kuciax commented 2 years ago

@mdjastrzebski Do we have any news in this area?

mmomtchev commented 2 years ago

Having nothing better to do, I just wasted 3 hours on this shit, I know what is going on, but there is no easy solution.

This warning depends on a piece of remarkably kludgy (and fragile) code: https://github.com/facebook/react/blob/12adaffef7105e2714f82651ea51936c563fe15c/packages/react-dom/src/test-utils/ReactTestUtilsPublicAct.js#L129

(in React 18 this has been reshuffled around to another file but the problem remains)

Look at it:

Promise.resolve()
  .then(() => {})
  .then(() => {
    if (called === false) {
      console.error(
        'You called act(async () => ...) without await. ' +
          'This could lead to unexpected testing behaviour, interleaving multiple act ' +
          'calls and mixing their scopes. You should - await act(async () => ...);',
      );
    }
  });

in the function body and the return value is:

    return {
      then(resolve, reject) {
        called = true;
        result.then(

Normally the second useless .then() will be called after the .then() of the return value if the function is awaited. However the return value is evaluated in the jest context - where a Promise is Node.js' internal promise. The then() here is evaluated by the shim in react-native/lib - here Promise is a JS constructor.

Now enter asap - which is used by the Promise shim. It does what it says - it finds a way to flush all Promises before anyone else has a chance to do something else. Which defeats that clever hack.

mmomtchev commented 2 years ago

In fact a possible solution will be exactly the opposite of the previous proposal: try to make jest to use the fake Promises

Norfeldt commented 2 years ago

@mmomtchev do you have a hack to use asap in jest?

Norfeldt commented 2 years ago

This is a very big issue without any solution. Right now I'm "patching" it to go away 🙈, but have to use patch-package if this continues to be unsolved 🙊.

Screenshot 2022-05-20 at 14 42 34
if (argsWithFormat[0].includes("not wrapped in act(")) return
mmomtchev commented 2 years ago

@Norfeldt I haven't tried too hard but maybe it is possible - the problem is that you will have to replace the await in jest with the internal Promise implementation from react-native - it can probably be achieved with babel but I think that this should be solved in a cleaner manner, jest + TypeScript + react-native is already a nightmare, the last thing we want is another layer of complexity over it because of two lines of fragile code

React are obviously difficult to move And in React Native this doesn't come down to 2 lines of code - React Native will be very seriously impacted

mmomtchev commented 2 years ago

@Norfeldt Here is your quick hack:

Create test/asap.js with

const nodePromise = Promise;

module.exports = (r) => nodePromise.resolve().then(r);

Then add to jest.config.js:

    moduleNameMapper: {
        '^asap$': '<rootDir>/test/asap.js',
        '^asap/raw$': '<rootDir>/test/asap.js'
    },
Norfeldt commented 2 years ago

thanks a lot @mmomtchev you just improved my testing experience ❤️ Seemed like I did not have to install the asap npm package?

mdjastrzebski commented 2 years ago

It seems that in the past the async act() warning was appearing when there were two awaits in the test. One await e.g. with findBy was working fine, but adding a second one caused the async act() warning to appear.

I've noticed that this is no longer the case as our examples/basic has one test ('User can sign in after incorrect attempt') where there is a double await and error is no longer presented. This might be due to update of React, React Native & Jest deps that I've made recently.

Could anyone of interested people here, verify/reject that on your own code/tests, without any additional workarounds? @Norfeldt, @mmomtchev, @tcank, @jpmonette

Norfeldt commented 2 years ago

@mdjastrzebski I removed my patch, upgraded RNTL 9 - did some fixes for some test and it works without the act warnings (it does however seems a little flaky, since sometimes I did see some, but when I ran the test again they were gone 😳). Upgraded to 11 and it also seems to work.

mdjastrzebski commented 2 years ago

@Norfeldt awesome, thanks for feedback!

Norfeldt commented 2 years ago

@mdjastrzebski a few hours after my reply I can conclude that the it has changed from always happing to sometimes happing (flaky) 😢

mdjastrzebski commented 2 years ago

@Norfeldt can you explain what exactly do you mean by flacky here?

Norfeldt commented 2 years ago

Meaning that when I run all my tests the warning appears in some of them. But then I run them again and they then disappear or occur in some other tests.

mdjastrzebski commented 2 years ago

@Norfeldt Hmmm, so it get's non-deterministic. Not sure if that better or worse. Does your previous patch resolve the flakyness?

Norfeldt commented 2 years ago

I have re-enabled

patches/react-test-renderer+17.0.1.patch

diff --git a/node_modules/react-test-renderer/cjs/react-test-renderer.development.js b/node_modules/react-test-renderer/cjs/react-test-renderer.development.js
index 23e0613..a41f8cc 100644
--- a/node_modules/react-test-renderer/cjs/react-test-renderer.development.js
+++ b/node_modules/react-test-renderer/cjs/react-test-renderer.development.js
@@ -65,6 +65,8 @@ function printWarning(level, format, args) {
     // breaks IE9: https://github.com/facebook/react/issues/13610
     // eslint-disable-next-line react-internal/no-production-logging

+    if (argsWithFormat[0].includes("not wrapped in act(")) return
+
     Function.prototype.apply.call(console[level], console, argsWithFormat);
   }
 }

package.json

...
"scripts": {
    ...
    "test": "jest --config='./test/jest/config.js' --colors --coverage=false --forceExit --runInBand",
    "postinstall": "patch-package"
  },

the warnings are gone.. 🙈

mdjastrzebski commented 2 years ago

@Norfeldt: if (argsWithFormat[0].includes("not wrapped in act(")) return, no wonder there are no warnings ;-)

Norfeldt commented 2 years ago

@Norfeldt: if (argsWithFormat[0].includes("not wrapped in act(")) return, no wonder there are no warnings ;-)

Yes, it works like magic 🪄

mdjastrzebski commented 2 years ago

The longest running RNTL issue :-)

@Norfeldt @mmomtchev @kuciax @tcank @rijk @ccfz @ryanhobsonsmith @milesj @seyaobey-dev did anyone here actually observed incorrect testing behaviour occurring as a result of this warning or is it just annoying because we like not to have warnings in our test runs?

Norfeldt commented 2 years ago

The longest running RNTL issue :-)

@Norfeldt @mmomtchev @kuciax @tcank @rijk @ccfz @ryanhobsonsmith @milesj @seyaobey-dev did anyone here actually observed incorrect testing behaviour occurring as a result of this warning or is it just annoying because we like not to have warnings in our test runs?

I'm experiencing flaky CI tests (when I rerun them they pass, they always pass on my local machine). It could be a server issue, but it always lures in the back of my mind if it's related to this issue.

The annoyance is a small thing. When running over 100 tests or debugging one, it makes it hard to work in the terminal.

https://youtu.be/xCcZJ7bQFQA look at 59:04 to see an example of what I mean.

milesj commented 2 years ago

Yes it does actually cause problems, with the biggest issue being that it masks failures and tests pass with a green status. This is primarily caused by RN overriding the global Promise.

Once we patched that, it uncovered a massive amount of these problems. Invalid mocks, accessing on undefined errors, so on and so forth. I really have no idea why this happens, but it does.