react-native-testing-library: Error thrown - Warning: You called act(async () => ...) without await.

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.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 49
  • Comments: 112 (19 by maintainers)

Commits related to this issue

Most upvoted comments

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

I have had this problem come up in a variety of places and 99% of the time the problem was that I was waiting for synchronous code. That is why for some people the error went away, after they removed an await, it is a strong sign that the test only required one await or that the await is in the wrong place. Something that works quite nicely in these situations is going through the test and remove all awaits and then look at the code that is being tested to see where the async code is actually happening and to use await only once the async code is actually triggered. If the assertion still fails because the render did not finish have a look at the bottom examples, maybe they can help because they wait until something shows up or is finished running.

The following three examples are the ways I tend to use await the most, maybe it helps clean up tests:

  1. Waiting until a assertion is finished, this is great for circumstances where I only care that the final render is correct:
it('renders the send button', async () => {
    const utils = renderConversation();

    await waitFor(() => {
        expect(utils.getByText('Send')).toBeTruthy();
    });
});
  1. Wait for an element to render, this I only really need if I want to call fireEvent on the element. If you wait for an element to render with the below method and await the assertion as well, the error will probably come up. This is a classic case of unnecessary waiting that I describe at the top. Therefore, don’t wait for synchronous code.
it('hides the reply', async () => {
    const utils = renderConversationScreen();
    const replyButton = await utils.findByTestId('hideReplyButton'); // findBy... uses waitFor internally i.e. await waitFor(() => utils.getByTestId('hideReplyButton')); workes just as well
    fireEvent(reply, 'Press');

    expect(utils.queryByTestId('hideReplyButton')).toBeNull();
});
  1. FireEvent triggers async code, which should finish before asserting anything
it('queues the conversation messages', async () => {
    const utils = renderConversationScreen();
    const conversation = utils.getByTestId('Conversation');

    const reply = await utils.findByTestId('replyView'); // I wait for the reply to render
    const replyNativeEvent = { layout: { height: 50 } };
    await act(async () => {
        fireEvent(reply, 'Layout', { nativeEvent: replyNativeEvent }); // I wait for all animations to complete
    });

    expect(conversation.props.queueMessages).toBe(true);
});

@KieranO547 though the code is quite simple two follow up question:

  1. When you get that error is the code you posted the only code that is running? To make sure that is the case I would remove all other renderings and tests in the file and only run that one file. The weird thing about the error showing up for you is that the spec you posted seems to contain nothing that could trigger the error.

  2. What does the code look like, which you are testing. Are there timeouts, state changes, animations etc. just the spec code is only one side of the medal and makes it very hard to debug.

@brunnohofmann @rmathias86 In my case, I found that the problem was caused by the following line:

expect(findByText('An error message')).toBeTruthy();

Since version 6, findBy internally uses async/await and polls until it finds the element in question. I found that wrapping this in an expect (where this is the last line in my test), it creates a race condition between the findBy polling and the end of the test. Since findBy has a 4.5 second timer before it errors out, the “unmounted” error would appear 4.5 seconds after it is triggered and appear to be tied to a random test, even though it wasn’t actually caused by a random test.

Replacing the offending line with this:

expect(queryByText('An error message')).not.toBeNull();

Fixed my problem.

I’ve found a way of hijacking the react-native jest preset, to avoid replacing the global Promise object with PromiseJs. Here is how it’s implemented

@mdjastrzebski now this could be a potential solution until https://github.com/facebook/react-native/issues/29303 is dealt with by the RN team. @testing-library/react-native could provide it’s own jest preset as a wrapper of the original react-native preset in a similar way than my example before. This preset can be optional as this issue does not affect everyone.

We used to have a preset and it was removed, so I’m sure there are some arguments against doing it again. However, this solves this issue and issues in #568 as well.

Any updates here? This is happening when i use more than 1 await inside a it

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.

I tried to do the suggested steps from @sbalay in this comment: https://github.com/callstack/react-native-testing-library/issues/379#issuecomment-714341282, but I was not able to make it run as expected in my setup.

What I did is something similar, I installed a Promise pollyfil with npm install --save-dev promise-polyfill. After that I added this into my jest.setup.ts file (I guess it will work for .js files too):

import Promise from 'promise-polyfill';
global.Promise = Promise;

Before the workaround: image

After the workaround: image

Looks like a good workaround for now.

@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'
    },

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

I guess this is the issue, too. In my case, those messages disappeared when I replaced findBy queries with getBy and left only one findBy query.

Found it! Same issue as https://github.com/facebook/react-native/issues/29303 or https://github.com/facebook/jest/issues/10221 (and also interfering in https://github.com/callstack/react-native-testing-library/pull/568)

Not sure how and not sure why, but the fact that the react-native preset for jest overrides the Promise object is part of the cause behind this warning.

When I remove that line from the preset (in my node_modules folder) the warning disappears.


@milesj thanks for the clarification! Not happening in this case.

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

  • using multiple waitFor in one it causes problems with jest promises - you can fix this by using Promise polyfill
  • using await on synchronous functions causes race conditions problems in testing-library. They occur randomly directly inside it. When used inside act or waitFor problem occurs more often - the only known solution for now is to avoid spamming await on every function invoked in tests
  • using findBy* inside expect leads to race conditions problems - instead of direct use, define variable before expect:
const element = await findBytestId('elementTestId')
expect(() => element.toBeDefined())
  • using fireEvent inside act may lead to problems with Promises and race conditions - use async act: await act(async () => fireEvent...)
  • using findBy* and waitFor in the same it can lead to react error Can't perform a React state update on an unmounted component. - for now the best known solution is to avoid this situations and instead of findBy use waitFor together with queryBy*
  • For this warning: Warning: You called act(async () => ...) without await. try to replace queryBy* and findBy* with getBy*. Check if all act are used properly. Check if you don’t have any async methods without await.
  • sometimes test can spam with message An update to [Component] inside a test was not wrapped in act(...) even if all fireEvents and other actions that need it are wrapped by act. This can be caused by many reasons. I.e. in our project Apollos MockedProvider was not resolving promises on time (nor waiting for them). In rare cases you can try to use combination of async act and waitFor:
await act(async () => {
  await waitFor(() => {
    fireEvent...
  })
})

They are not a 100% solution but we noticed that checking this list resolves most of the problems. In rest of the cases we just use trial and error method to find out what should be corrected 😦

i am having this problem with @testing-library/react-native": "^11.5.0

Closing this issue, as it became non-actionable. Recent changes seem to have fixed the warnings for some users, while this issue contains various instances of act warnings, both sync and async, triggered by different conditions that probably deserve separate treatment.

If you will find act warnings still occurring in the latest RNTL version, then please submit a GH issue with minimal repro repository based on ours examples/basic so that we have an actionable PR with clear repro case to fix.

@jpmonette, @Norfeldt, @mmomtchev, @tcank, @rijk ^

This particular line looks a bit odd to me:

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

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

Could you try something like:

const element = await findByTestId('record...') // This is essentially waitFor & getByTestId
expect(element).toBeTruthy()

Let me know if you still got the same error.

In my case, it happens when using await findByX at least two times in the same test, so when callbacks do something asynchronously and I need to wait for an element to appear:

const component = render(<MyComponent />)

const button = await component.findByText("First button")
fireEvent.press(button)

const submit = await component.findByText("Submit")
fireEvent.press(submit)

expect(someCallback).toBeCalled()

I still have no idea why does it complain in such case, though…

I patched react-test-renderer (as a temporary fix) since this error was failing my CI tests.

if(!argsWithFormat[0].includes(/You called act\(async \(\) => ...\) without await/)) {
  Function.prototype.apply.call(console.error, console, argsWithFormat);
}

@mdjastrzebski , I ran reactnavigation example in this repo and still getting the above error. But when i downgrade to v6.0.0, it goes away. Can you please help me what’s going wrong ?

I also ran into same problem after upgrading to v7.0.1. Also, if you just run the react-navigation example in the repo itself you will see this problem.

I’m also getting this error when using await multiple times.

Something I noticed though is that if I change the test from async/await to normal then with done then the warning goes away.

Test that gets a warning:

describe(ShoppingHeader, () => {
  let fixture: RenderAPI;

  beforeEach(() => {
    const Stack = createStackNavigator();
    const TestRoot = () => <Text>Hello</Text>;
    const TestCart = () => <Text>The Cart Screen</Text>;

    fixture = render(
      <NavigationContainer>
        <Stack.Navigator screenOptions={{ header: ShoppingHeader }}>
          <Stack.Screen name="Test Root" component={TestRoot} />
          <Stack.Screen name="Cart" component={TestCart} />
        </Stack.Navigator>
      </NavigationContainer>,
    );
  });

  afterEach(cleanup);

  describe('back button', () => {
    beforeEach(async () => {
      const cartButton = await fixture.findByA11yLabel('Open Cart');
      fireEvent.press(cartButton);
    });

    it('navigates back when tapped', async () => {
      const backButton = await fixture.findByA11yLabel('Back');
      fireEvent.press(backButton);

      await waitForElementToBeRemoved(() => fixture.getByText('The Cart Screen'));
      expect(fixture.getByText('Test Root')).toBeTruthy();
      expect(() => fixture.getByA11yLabel('Back')).toThrow();
    });
  });
});

Without using async/await:

it('navigates back when tapped', (done) => {
  fixture.findByA11yLabel('Back').then((backButton) => {
    fireEvent.press(backButton);
    return waitForElementToBeRemoved(() => fixture.getByText('The Cart Screen'));
  }).then(() => {
    expect(fixture.getByText('Test Root')).toBeTruthy();
    expect(() => fixture.getByA11yLabel('Back')).toThrow();
    done();
  });
});
The component if it's useful
const ShoppingHeader = (props: StackHeaderProps) => {
  const { scene, previous, navigation } = props;
  const { options } = scene.descriptor;
  let title: any;

  if (options.headerTitle) {
    title = options.headerTitle;
  } else if (options.title) {
    title = options.title;
  } else {
    title = scene.route.name;
  }

  return (
    <Appbar.Header>
      {previous && <Appbar.BackAction onPress={navigation.goBack} />}
      <Appbar.Content title={title} />
      <Appbar.Action
        accessibilityLabel="Open Cart"
        icon="cart"
        onPress={() => navigation.navigate('Cart')}
      />
    </Appbar.Header>
  );
};

@jsamr You should wrap the reload() call in act like this:

    act(() => {
      result.instance.reload();
    });

As each state mutation should be wrapped in act. In case of fireEvent this is done for you.

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 () => ...);

which is simingly caused by await findByText('load-cycle-1');

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.

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:

  • Updated patch to something more robust
  • Updated to remove info about test failures (turns no tests were broken with this method).

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

Thank you kind sir! @babylone-star

@Norfeldt @milesj @mmomtchev do you use RNTL Jest preset: preset: '@testing-library/react-native',? Does using it make any difference regarding test runs?

@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.

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.

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

Found it! Same issue as facebook/react-native#29303 or facebook/jest#10221 (and also interfering in #568)

Not sure how and not sure why, but the fact that the react-native preset for jest overrides the Promise object is part of the cause behind this warning.

When I remove that line from the preset (in my node_modules folder) the warning disappears.

@milesj thanks for the clarification! Not happening in this case.

I can confirm that removing line 24 from node-modules/react-native/jest/setup.js clears the warning.

Not sure if there’s anything we can do besides patching it. I tried all proposed solutions and this was the only one that worked.

Having the same issue as @rmathias86

so what is the solution eventually?

After upgrading from 5.x to 7.x, I started to get randomly this following error. Sometimes all tests pass, sometimes not. This is really random.

Unable to find node on an unmounted component.

  at node_modules/@testing-library/react-native/build/helpers/findByAPI.js:16:115

I suspect this is related to the same problem.

@thymikee here is a repo with the example from @ryanhobsonsmith. It would be great if someone had a look.

Ok, Miles’ solution was too far off actually. In case anyone else finds this and wants a quick workaround inspired by: https://github.com/facebook/react/issues/15416, then the best hack I’ve got work around things is to just build in some manual wait time inside of an act() call like so:

function wait(ms) {
  return act(() => new Promise((r) => setTimeout(r, ms)));
}

function fireAsyncPress(element, ms = 1) {
  fireEvent.press(element);
  return wait(ms);
}

test("it works", async () => {
  const { getByRole, getByText } = render(<Example />);
  getByText(/Value is 0/i);
  const button = getByRole("button");

  await fireAsyncPress(button);
  getByText(/Value is 1/i);

  await fireAsyncPress(button);
  getByText(/Value is 2/i);
});

Would be happy to learn if anyone has worked out a better practice for testing these kinds of async event handlers / state-changes though.

@mdjastrzebski I ran into a similar issue (although, don’t know if it is exactly the same problem) and made a mcve here: https://github.com/jsamr/react-native-testing-library-379

we’ve released RNTL v11.2.0 that might have improved this issue. Could you verify if it improves things for you.

This did fix this error for me. I am in a new codebase that I set up last week. I was on React Native Testing Library 11.1.0, and upgrading to 11.2.0 made it so the warning I was receiving went away. Here was the test where I had been getting the warning, though the test itself was passing. I presume I was getting the warning because I had multiple awaits in it:

const mocks = [signInMutationMock(...)]
renderApplication({ mocks })

expect(await screen.findByTestId('SignInScreen')).toBeDefined()

fireEvent.changeText(
  screen.getByLabelText('Email address'),
  'jan@example.com',
)
fireEvent.changeText(screen.getByLabelText('Password'), 'Password')
fireEvent.press(screen.getByRole('button'))

// this was the line that was triggering the console error
expect(await screen.findByTestId('HomeScreen')).toBeDefined()

fireEvent.press(screen.getByText('Sign Out'))
await pressAlertButton('Yes, sign out')

expect(screen.queryByTestId('HomeScreen')).toBeNull()
expect(screen.getByTestId('SignInScreen')).toBeDefined()

React: 18.1.0 React Native: 0.70.1 RN Testing Library: 11.2.0 React Test Renderer: 18.1.0 (I don’t have this added explicitly in my project, included by RN Testing Library)

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

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.

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

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

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();
  });

After upgrading from 5.x to 7.x, I started to get randomly this following error. Sometimes all tests pass, sometimes not. This is really random.

Unable to find node on an unmounted component.

  at node_modules/@testing-library/react-native/build/helpers/findByAPI.js:16:115

I suspect this is related to the same problem.

@rmathias86 I’m getting this error too, tests failing intermittently on CI. For us, the tests that fail are pretty random but none of them are using async/await or the findBy/waitFor API. What makes you think it’s related? Did you ever find a fix?

Btw, here’s the full stack trace, it’s worryingly small like most of it is missing

Unable to find node on an unmounted component.

  at node_modules/@testing-library/react-native/build/helpers/findByAPI.js:16:115
  at Timeout.runExpectation [as _onTimeout] (node_modules/@testing-library/react-native/build/waitFor.js:48:24)

@brunnohofmann @rmathias86 In my case, I found that the problem was caused by the following line:

expect(findByText('An error message')).toBeTruthy();

Since version 6, findBy internally uses async/await and polls until it finds the element in question. I found that wrapping this in an expect (where this is the last line in my test), it creates a race condition between the findBy polling and the end of the test. Since findBy has a 4.5 second timer before it errors out, the “unmounted” error would appear 4.5 seconds after it is triggered and appear to be tied to a random test, even though it wasn’t actually caused by a random test.

Replacing the offending line with this:

expect(queryByText('An error message')).not.toBeNull();

Fixed my problem.

Great find!

There were 3 tests doing expect(findBy****). I extracted the findBy out of expect function, and put await before that. Solved my problem.

const element = await findBy****
expect(element).****

I tried replacing promise with the native Promise and it completely broke half of our tests. We even tried using native async/await instead of regenerator-runtime and it had the same problem. I’d wager a lot of RN APIs expect the timings provided by promise to work correctly, which is rather unfortunate.

Nice find!

    "expo": "^46.0.10",
     ...
    "@testing-library/jest-native": "^4.0.12",
    "@testing-library/react-native": "^11.2.0",
    ...    
    "jest": "^29.0.3",
    "jest-expo": "^46.0.1",
    "msw": "^0.44.2",
    "patch-package": "^6.4.7",
    "prettier": "^2.5.1",
    "react-test-renderer": "^18.2.0",
    ...
    "ts-jest": "^29.0.1",
    "ts-node": "^10.9.1",
console.error
      Warning: An update to _default 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 _default
          at CurrentPortfolioContextProvider
          at QueryClientProvider
          at AppProviders

As mentioned, I think it’s because I do some fetching and react-query does some react state management. If I just had a way to render it and tell it when RQ is done with the state management, then that might solve it.

@Norfeldt that’s weird our recent basic example uses Expo 46 & Jest 28: https://github.com/callstack/react-native-testing-library/blob/main/examples/basic/package.json

This basic example does not have reanimated.

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.

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

Yes, it works like magic 🪄

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

@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)

Hello all!,

I followed @MarceloPrado’s suggestion which is to comment out this line in react-native package: https://github.com/facebook/react-native/blob/master/jest/setup.js#L24. It does clear out the warnings however it breaks a few tests in our codebase and I couldn’t figure out on how to fix the tests.

I tried a different approach which is to update the waitFor function in the compiled js file here: https://github.com/callstack/react-native-testing-library/blob/master/src/waitFor.js#L57-L74 to be:

async function waitFor(expectation, options) {
  if (!checkReactVersionAtLeast(16, 9)) {
    return waitForInternal(expectation, options);
  }

  let result;

  //$FlowFixMe: `act` has incorrect flow typing
  const actResult = (0, _act.default)(async () => {
    result = await waitForInternal(expectation, options);
  });

  await new Promise(actResult.then)

  //$FlowFixMe: either we have result or `waitFor` threw error
  return result
}

It seems to remove the warnings in our codebase (although we need to fix a couple of tests due to not handling the async operation properly). The thing that I’m unsure about is whether this will create another problems that I’m not aware of.

Having the same problem here!!!

After upgrading from 5.x to 7.x, I started to get randomly this following error. Sometimes all tests pass, sometimes not. This is really random.

Unable to find node on an unmounted component.

  at node_modules/@testing-library/react-native/build/helpers/findByAPI.js:16:115

I suspect this is related to the same problem.

@sbalay It’s possible to have duplicate transitive dependencies. Like in Yarn for example, you may have a lock file that has an entry like the following (Yarn sometimes doesn’t dedupe correctly).

react-test-renderer@^16.10.1:
  version "16.13.1"
  resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1"
  integrity sha512-Sn2VRyOK2YJJldOqoh8Tn/lWQ+ZiKhyZTPtaO0Q6yNj+QDbmRkVFap6pZPy3YQk8DScRDfyqm/KxKYP9gCMRiQ==
  dependencies:
    object-assign "^4.1.1"
    prop-types "^15.6.2"
    react-is "^16.8.6"
    scheduler "^0.19.1"
    
react-test-renderer@^16.13.1:
  version "16.13.1"
  resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.13.1.tgz#de25ea358d9012606de51e012d9742e7f0deabc1"
  integrity sha512-Sn2VRyOK2YJJldOqoh8Tn/lWQ+ZiKhyZTPtaO0Q6yNj+QDbmRkVFap6pZPy3YQk8DScRDfyqm/KxKYP9gCMRiQ==
  dependencies:
    object-assign "^4.1.1"
    prop-types "^15.6.2"
    react-is "^16.8.6"
    scheduler "^0.19.1"

I’m also getting this console error logged when I call findByText more than once. I’m trying to understand if this is an actual bug as @renanmav and @milesj seem to suggest or if I’m just not understanding something about how to use react testing library properly.

A simple (albeit contrived) example I’ve come up with to reproduce the bug is:

function Example() {
  const [value, setValue] = useState(0);
  const asyncIncrement = useCallback(() => {
    setTimeout(() => setValue((v) => v + 1), 0);
  }, []);

  return (
    <View>
      <Text>Value is {value}</Text>
      <Button title="Increment" onPress={asyncIncrement} />
    </View>
  );
};

test("it works", async () => {
  const { findByText, getByRole, getByText } = render(<Example />);
  getByText(/Value is 0/i);
  const button = getByRole("button");

  fireEvent.press(button);
  await findByText(/Value is 1/i);

  //If I comment out these lines and no error message will be logged
  fireEvent.press(button);
  await findByText(/Value is 2/i);

  // If I uncomment these lines and two error messages will be logged
  // fireEvent.press(button);
  // await findByText(/Value is 3/i);
});

The test seems to work fine (passes as written, fails if I change expected values), but I still see the console.error message warning complaining that “You called act(async () => …) without await”

I get this console error once per additional time (beyond the first) that I make calls to fireEvent.press(button) and findByText in the same test. I’ve tried suggestions by @ccfz to wrap the fireEvent() calls with await act(async ...), but that didn’t help (not sure I understand how that would have helped either? Doesn’t fireEvent already wrap things in act()?)

Could someone help me understand what is going wrong here?

@ccfz Thanks for all the info. So to answer the 2 follow up Q’s,

  1. Yeah theres no other code running, these tests are just aiming to assert that my validation is disabling the login button based on the validation, I’ve implemented the validation via react-hook-forms + yup schema validation, it works just fine from a user perspective and its just a login form so there is definitely no other code running.
  2. Zero timeouts, react-hook-forms handles the form state, no animations. I do get that it wasn’t enough info, i wasn’t trying to hijack the post with a million lines of code. As an example, I just have a basic “form” with two input elements for username/password, the text-input comes from react-native-elements, which look like this:
      <Controller
        control={control}
        name="password"
        defaultValue=""
        render={(props) => (
          <Input
            {...props}
            onTouchStart={() => trigger('password')}
            onFocus={() => setTouched(touched + 1)}
            style={input}
            placeholder="********"
            leftIcon={
              <Icon name="md-key" size={20} color="black" style={inputIcon} />
            }
            errorStyle={inputError}
            errorMessage={errors.password?.message}
            label="Password"
            secureTextEntry={true}
            onChangeText={(text) => {
              props.onChange(text);
              trigger('password');
            }}
          />
        )}
      />

The touched state is only used for the disabled state of the login button which is exactly what I’m testing for.

Edit: I just refactored the test to be:

test.only('should disable button when password length is short', async () => {
  const { getByPlaceholderText, getByText } = render(<EmailPassword />);
  const emailInput = getByPlaceholderText(/email@example.com/i);
  const passwordInput = getByPlaceholderText(/\*\*\*\*\*\*\*\*/i);
  const loginButton = getByText(/Login/i);

  await act(async () => {
    fireEvent.changeText(emailInput, 'test@gmail.com'); // valid email
  });
  expect(loginButton).toBeDisabled(); // short password so disabled

  await act(async () => {
    fireEvent.changeText(passwordInput, '12345');
  });
  expect(loginButton).toBeDisabled(); // short password so disabled

  await act(async () => {
    fireEvent.changeText(passwordInput, '123456');
  });
  expect(loginButton).toBeEnabled();
});

Matching your third suggestion and thats got rid of the messages for me so i suppose thats what i was missing, the error message handler is part of the component library - still getting used to testing lol - And thank you again!

I’ve dug into this a bit and I believe the issue is race conditions.

Here’s the act code and the block where the console log happens: https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L3680 You’ll notice this just creates a resolved promise and logs in a then block (2 next ticks).

To ignore the error, the then of the initial render needs to fire here: https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberWorkLoop.old.js#L3701 However, in my testing, this is actually firing after the resolved promise above, resulting in the warnings. Why? Because RN/React rendering is slow? Not really sure, but there’s definitely not an easy way to fix it, or no way in general.

I suggest use wait-for-expect directly.

I’m facing the same error but my test is different. Before moving from version 2 to 7 my test was the same and I have no errors, but after updating to v7 i’ve been getting the same error.

I have something really simple like this:

 const { getByText, update } = render(<Component />);

 fireEvent(getByText('idx-1'), 'onPress');

 await act(async () => {
    update(<Component />);
 });

I use act because when I fire the event I have some async logic in my component that will re-render it. If i don’t use the last code I’ve got the next error:

 Warning: An update to ForwardRef(NavigationContainer) 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 */

UPDATE: 11-08-2020 I changed (cleaned) my tests. I don’t have to use anymore this awful thing I was doing adding await to all the act functions I was using and It worked fine. BUT, this is not always for all the cases, so you must be careful how is written your async code and then how is written your test… testing this async code. Also, you must be careful if you are using async callings inside the useEffect hook. I was concerned about this but I was doing it wrong in some parts. I’ll share with you what I’m doing with my code now, which thanks to the tests and the library, I think is more solid:

I’m adding a variable to know if you component is mounted/unmounted. With this variable validate wether update or not the state of the componente, which cause to re-render the component.

// I prefer to use isMounted. This way I don't have to use a negation to validate if something is  `true` when is `false` :/
useEffect(() => {
  let isMounted = true;

 (async ( ) => {
   const result = await someAsyncMethod();

  // Here i want to update the state of my component
  if (isMounted) {
   setState(result);
  }
 })();

  return () => {
    isMounted = false;
  };
})

My test now it is like this:

it('test', () => {
 const { getByText, update } = render(<Component />);

 fireEvent(getByText('idx-1'), 'onPress');

  act(() => {
    update(<Component />);
  });
});

Hope this might help 😃

One last thing! Don’t forget to use the done function 😋

@jsamr thanks for posting repo, I’ll try to reproduce that tomorrow.

@mdjastrzebski I did update my code and still get the issue. Here’s the updated test:

import * as React from 'react';
import { fireEvent, render } 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 Record Type picker exposed', async () => {
    const { container } = setup();

    const input = await container.findByTestId('recordTypePicker');
    expect(input).toBeTruthy();

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

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

    const input = await container.findByTestId('recordTypePicker');
    expect(input).toBeTruthy();

    fireEvent(input, 'onChange', { nativeEvent: { selectedSegmentIndex: 1 } });

    const layout = await container.findByTestId('newSObjectLayout');
    expect(layout).toBeTruthy();

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