Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
jpmonette opened this issue Jun 9, 2020 · 112 comments
Closed
Labels
needs triage question Further information is requested

Comments

@jpmonette
Copy link

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.
@jpmonette jpmonette added the question Further information is requested label Jun 9, 2020
@jpmonette jpmonette changed the title Error thrown - 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 () => ...); Error thrown - Warning: You called act(async () => ...) without await. Jun 9, 2020
@mdjastrzebski
Copy link
Member

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.

@jpmonette
Copy link
Author

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

@mdjastrzebski
Copy link
Member

@jpmonette can you provide a repro repo?

@jsamr
Copy link

jsamr commented Aug 3, 2020

@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

@pillu00
Copy link

pillu00 commented Aug 3, 2020

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.

@mdjastrzebski
Copy link
Member

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

@mdjastrzebski
Copy link
Member

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

@pillu00
Copy link

pillu00 commented Aug 4, 2020

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

@DracotMolver
Copy link

DracotMolver commented Aug 6, 2020

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 😋

@renanmav
Copy link

renanmav commented Aug 12, 2020

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

@GleidsonDaniel
Copy link

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.

@florian-milky
Copy link

Same issue and I'm not doing anything in my test, just rendering and await findBy

@mym0404
Copy link

mym0404 commented Aug 19, 2020

I suggest use wait-for-expect directly.

@hyunjinjeong
Copy link

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.

@StarryFire
Copy link

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

@renanmav
Copy link

renanmav commented Aug 27, 2020

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

@kieran-osgood
Copy link

I am getting this without having any awaits or findBy's, this is the simplest example I could reproduce it, the error appears when I get to the second changeText fireEvent.changeText(passwordInput, '12345')

  const { getByPlaceholderText, getByText } = render(<EmailPassword />);

  const emailInput = getByPlaceholderText(/email@example.com/i);
  const passwordInput = getByPlaceholderText(/\*\*\*\*\*\*\*\*/i);
  const loginButton = getByText(/Login/i);

  fireEvent.changeText(emailInput, 'test@gmail.com'); // valid email
  expect(loginButton).toBeDisabled(); // short password so disabled
  fireEvent.changeText(passwordInput, '12345');
  expect(loginButton).toBeDisabled(); // short password so disabled

  fireEvent.changeText(passwordInput, '123456');
  expect(loginButton).toBeEnabled();
});

Not entirely sure what I need to do?

@milesj
Copy link

milesj commented Sep 29, 2020

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.

@ccfz
Copy link

ccfz commented Sep 30, 2020

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.

@kieran-osgood
Copy link

kieran-osgood commented Sep 30, 2020

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

@ryanhobsonsmith
Copy link

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?

@milesj
Copy link

milesj commented Oct 6, 2020

@ryanhobsonsmith By default fireEvent wraps in act and the handler being fired must be synchronous, but because of setTimeout, that onPress is technically asynchronous. So in your example above, the setValue is happening after act has ran.

Maybe try:

await waitFor(() => fireEvent.press(button));

@ryanhobsonsmith
Copy link

Thanks @milesj, your notes about the act handler make sense. Unfortunately, your proposed solution didn't work.

My understanding is that waitFor just periodically executes the interior function until it returns a value and, as you said, the fireEvent.press() looks like it returns synchronously (regardless of sync/async nature of the event handler) right?

Just in case fireEvent.press() was actually passing back the result of the event handler somehow, I switched it around so handler would actually return something to await on:

const asyncIncrement = useCallback(() => {
    return new Promise((resolve) => {
      setTimeout(() => {
        setValue((v) => v + 1);
        resolve();
      }, 0);
    });
  }, []);

Feels unnecessary, but with or without that above event handler change I'm still seeing that same error.

Again, reason I'm leaning towards this being a bug is that the test actually seems to pass and work as expected outside of this console error message. I.e. subsequent findByText calls properly wait for the value to appear like I'd expect, just getting the console spam that makes me feel like I must be doing something wrong or may have a subtle bug somewhere.

@ryanhobsonsmith
Copy link

ryanhobsonsmith commented Oct 6, 2020

Ok, Miles' solution was too far off actually. In case anyone else finds this and wants a quick workaround inspired by: facebook/react#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.

@milesj
Copy link

milesj commented Oct 6, 2020

Yeah, I actually do something similar, like this.

async function waitForEvent(cb) {
  await waitFor(async () => {
    await cb();
    await new Promise(resolve => process.nextTick(resolve));
  });
}

await waitForEvent(() => fireEvent.press());

@ccfz
Copy link

ccfz commented Oct 6, 2020

@ryanhobsonsmith I played around with your examples and I would agree that there is a bug. waitFor seems to not play nice with timeouts, I had a similar issue with a Animated callback. As the findBy queries use waitFor on the inside I think that it is related to waitFor.

My workaround is:

fireEvent.press(button);
await act(wait);
expect(...)

@ryanhobsonsmith could you maybe create a PR with your example from above

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

because that should have definitely worked and the issue does not actually have a PR yet.

@mmomtchev
Copy link

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

mmomtchev added a commit to mmomtchev/react-native-settings that referenced this issue May 20, 2022
@Norfeldt
Copy link

Norfeldt commented May 23, 2022

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

@mdjastrzebski
Copy link
Member

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
Copy link

@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
Copy link
Member

@Norfeldt awesome, thanks for feedback!

@Norfeldt
Copy link

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

@mdjastrzebski
Copy link
Member

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

@Norfeldt
Copy link

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
Copy link
Member

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

@Norfeldt
Copy link

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
Copy link
Member

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

@Norfeldt
Copy link

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

Yes, it works like magic 🪄

@mdjastrzebski
Copy link
Member

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
Copy link

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
Copy link

milesj commented Aug 20, 2022

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.

@mmomtchev
Copy link

I think that the only solution which addresses the root cause is to remove asap from React Native. If there is a good reason for having it there, then the next best one is to make sure RNTL skips it in some way. Everything else is covering it up.

@mdjastrzebski
Copy link
Member

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

@milesj
Copy link

milesj commented Aug 22, 2022

Never tried. I don't work on the RN stuff anymore.

@Norfeldt
Copy link

Norfeldt commented Aug 25, 2022

I'm trying to upgrade to jest 28 as part of my expo upgrade to SDK 46, but it's giving me some trouble.

software-mansion/react-native-reanimated#3215

Will report back if anything has change regarding this - once I resolve the blockers.

@mdjastrzebski
Copy link
Member

@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

@Norfeldt
Copy link

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

@Norfeldt
Copy link

@mdjastrzebski I'm now on jest 29 and expo SDK 46 and the issue is still there. I believe that it's because I want to render components that uses react-query and fetches data from a local or staging server. I have a way to (almost) know when it's done, but checking that the queryClient has saved/cached the data.

Is there some way to render with an act - so I can tell RNTL that state updates should be done?

Currently I'm using .waitFor and not act

export function renderWithProviders(ui: React.ReactElement) {
  const testQueryClient = createTestReactQueryClient()

  const AppProviders = ({ ui }: { ui: React.ReactElement }) => (
    <QueryClientProvider client={testQueryClient}>
      <CurrentPortfolioContextProvider>{ui}</CurrentPortfolioContextProvider>
    </QueryClientProvider>
  )

  const { rerender, ...result } = ReactNativeTestingLibrary.render(<AppProviders ui={ui} />)
  return {
    utils: {
      ...result,
      testQueryClient,
      rerender: (rerenderUi: React.ReactElement) => rerender(<AppProviders ui={rerenderUi} />),
    },
  }
}

export async function renderWithUserAsync(
  Element: JSX.Element,
  email: keyof typeof userCredentials,
  awaitQueryClient?: boolean
) {
  await api.signOut() // allows avoiding to clean-up session after each test
  const user = await getSessionAsync(email)
  const { utils } = renderWithProviders(Element)

  if (awaitQueryClient === undefined || awaitQueryClient) {
    expect(utils.testQueryClient.getQueryData(QUERY_KEY_USER)).toBeUndefined()
    await waitFor(() => {
      expect(utils.testQueryClient.getQueryData(QUERY_KEY_USER)).toEqual(user)
    })
  }

  return { utils, user }
}

@mdjastrzebski
Copy link
Member

@Norfeldt @milesj @mmomtchev we've released RNTL v11.2.0 that might have improved this issue. Could you verify if it improves things for you. Pls report your React, React Test Renderer & React Native version along with feedback info so we can get a better understanding of the issue.

@Norfeldt
Copy link

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

@stevehanson
Copy link
Contributor

stevehanson commented Sep 26, 2022

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)

@mdjastrzebski
Copy link
Member

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 ^

@mdjastrzebski mdjastrzebski closed this as not planned Won't fix, can't repro, duplicate, stale Sep 30, 2022
@damdafayton
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs triage question Further information is requested
Projects
None yet
Development

No branches or pull requests