Closed jpmonette closed 1 year 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 ait
statement
This 🤨. I just tested out the theory on some of my tests and spot on.
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
Same issue here, with multiple
await
. Workaround from this comment works for me: #379 (comment)
DUDE! 😘
I don't thing we support putting jest's
expect
intowaitFor
.
how came, expect is even used in the sample in the doc: https://testing-library.com/docs/dom-testing-library/api-async/#waitfor
A clear winner without the global promise hacks. #379 (comment)
Thank you kind sir! @babylone-star
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
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 () => ...);'); } });
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:
@getsaf have you posted this to the React core team maybe? I wonder what's their stand on it
@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
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();
});
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...");
});
});
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()
})
@alterx What version are you using? Didn't warn for me on 7.1
or on 9.0
.
@AugustinLF I'm on 9.0.0, literally just copy and pasting that example throws the warning (the test passes, tho)
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()
})
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
Maybe this will be helpful for someone. In my team we have a set of rules for writing tests with jest and testing library:
waitFor
in one it
causes problems with jest promises - you can fix this by using Promise polyfillawait
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 testsfindBy*
inside expect
leads to race conditions problems - instead of direct use, define variable before expect:
const element = await findBytestId('elementTestId')
expect(() => element.toBeDefined())
fireEvent
inside act
may lead to problems with Promises and race conditions - use async act: await act(async () => fireEvent...)
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*
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.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 :(
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()
})()
})
@Terceramayor The IIFE itself doesn't seem to be awaited?
@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.
@Terceramayor I don't think the tests are correct if you do that.. any change I make still results in a pass
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.
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 ait
statementSame 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.
@cgorski28 it solved some of my tests but what if you do need multiple findBy*
in your test? Has anyone encountered this?
@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.
@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)
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);
});
});
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
@tcank that's a very interesting trail. Great thanks for posting it here. I will try to dig deeper in the coming days/weeks.
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
@mdjastrzebski Do we have any news in this area?
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.
In fact a possible solution will be exactly the opposite of the previous proposal: try to make jest to use the fake Promises
@mmomtchev do you have a hack to use asap in jest?
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 🙊.
if (argsWithFormat[0].includes("not wrapped in act(")) return
@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
@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'
},
thanks a lot @mmomtchev you just improved my testing experience ❤️ Seemed like I did not have to install the asap npm package?
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
@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.
@Norfeldt awesome, thanks for feedback!
@mdjastrzebski a few hours after my reply I can conclude that the it has changed from always happing to sometimes happing (flaky) 😢
@Norfeldt can you explain what exactly do you mean by flacky here?
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.
@Norfeldt Hmmm, so it get's non-deterministic. Not sure if that better or worse. Does your previous patch resolve the flakyness?
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.. 🙈
@Norfeldt: if (argsWithFormat[0].includes("not wrapped in act(")) return
, no wonder there are no warnings ;-)
@Norfeldt:
if (argsWithFormat[0].includes("not wrapped in act(")) return
, no wonder there are no warnings ;-)
Yes, it works like magic 🪄
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?
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.
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.
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:
useEffect
asynchronous logic to be executed so that theSegmentedControlIOS
is exposed (testID
=recordTypePicker
)selectedSegmentIndex
to"1"
newSObjectLayout
testID
The test
The console log