Why does react hook throw the act error when used with fetch api? - reactjs

I keep getting Warning: An update to App inside a test was not wrapped in act(...). in my test suite whenever I make an API request and update the state.
I'm making use of react-testing-library. I also tried using ReactDOM test utils, got the same result. One other thing I tried was wrapping the container in act, still got the same result.
Please note that: My App works and my test passes. I just need to know what I was doing wrong or if it's a bug in the react-dom package that's making that error show up. And it's bad to mock the console error and mute it.
global.fetch = require('jest-fetch-mock');
it('should clear select content item', async () => {
fetch.mockResponseOnce(JSON.stringify({ results: data }));
const { container } = render(<App />);
const content = container.querySelector('.content');
await wait();
expect(content.querySelectorAll('.content--item').length).toBe(2);
});
Here's the hook implementation:
const [data, setData] = useState([]);
const [error, setError] = useState('');
const fetchInitData = async () => {
try {
const res = await fetch(API_URL);
const data = await res.json();
if (data.fault) {
setError('Rate limit Exceeded');
} else {
setData(data.results);
}
} catch(e) {
setError(e.message);
}
};
useEffect(() => {
fetchInitData();
}, [isEqual(data)]);

It's a known problem, check this issue in Github https://github.com/kentcdodds/react-testing-library/issues/281

For anyone who stumbles upon this more than a year later as I did, the issue Giorgio mentions has since been resolved, and wait has since been replaced with waitFor, as documented here:
https://testing-library.com/docs/dom-testing-library/api-async/
That being the case, I believe the solution to the warning now should be something like this:
import { render, waitFor } from '#testing-library/react';
// ...
it('should clear select content item', async () => {
fetch.mockResponseOnce(JSON.stringify({ results: data }));
const { container } = render(<App />);
const content = container.querySelector('.content');
await waitFor(() =>
expect(content.querySelectorAll('.content--item').length).toBe(2);
);
});
In my case, I had an App component loading data asynchronously in a useEffect hook, and so I was getting this warning on every single test, using beforeEach to render App. This was the specific solution for my case:
beforeEach(async () => {
await waitFor(() => render(<App />));
});

To get rid of the act() warning you need to make sure your promises resolve synchronously. You can read here how to do this.
Summary:
The solution for this is a bit involved:
we polyfill Promise globally with an implementation that can resolve
promises 'immediately', such as promise
transpile your javascript with a custom babel setup like the one in this repo
use jest.runAllTimers(); this will also now flush the promise task queue

I had this problem and gave up using wait and async instead used jest faketimers and so on, so your code should be something like this.
global.fetch = require('jest-fetch-mock');
it('should clear select content item', /*async */ () => {
jest.useFakeTimers();
fetch.mockResponseOnce(JSON.stringify({ results: data }));
const { container } = render(<App />);
const content = container.querySelector('.content');
// await wait();
act(() => {
jest.runAllTimers();
});
expect(content.querySelectorAll('.content--item').length).toBe(2);
});

Related

React testing error Cannot log after tests are done. Did you forget to wait for something async in your test?

I have a functional component which I am trying to test and there seems to be some issue around the endpoint call via axios.
const url = user.myUrl + "/someEndpoint";
const RESPONSE = await axios.post(url);
console.log("RESPONSE ::::::::::::::::::::::::" + RESPONSE);
The test is as below;
test("Validate something", async () => {
const {container} = render(
<MyComponent url={url} />
);
expect(await container.getElementsByClassName('someGrid').length).toBe(2);
});
When I run the test, I get the below error;
Cannot log after tests are done. Did you forget to wait for something async in your test?
Attempted to log "RESPONSE ::::::::::::::::::::::::[object Object]".
PS: I am mocking endpoints via msw.
As MyComponent has an effect in it i.e. an axios POST, you need to wait for the result. Hence try changing your test to:
test("Validate something", async () => {
const {container} = render(<MyComponent url={url} />);
await waitFor(() => expect(container.getElementsByClassName('someGrid').length).toBe(2));
});
(You'll most likely need to import waitFor if you don't already use it)

How to test(assert) intermediate states of a React Component which uses hooks

This question is regarding: how to test a component which uses the useEffect hook and the useState hook, using the react-testing-library.
I have the following component:
function MyComponent() {
const [state, setState] = useState(0);
useEffect(() => {
// 'request' is an async call which takes ~2seconds to complete
request().then(() => {
setState(1);
});
}, [state]);
return <div>{state}</div>
}
When I render this application, the behavior I see is as follows:
The app initially renders 0
After ~2seconds, the app renders 1
Now, I want to test and assert this behavior using the react-testing-library and jest.
This is what I have so far:
import {render, act} from '#testing-library/react';
// Ignoring the describe wrapper for the simplicity
test('MyComponent', async () => {
let result;
await act(async () => {
result = render(<MyComponent />);
});
expect(result.container.firstChild.textContent).toBe(1);
})
The test passes. However, I also want to assert the fact that the user initially sees the app rendering 0 (before it renders 1 after 2 seconds).
How do I do that?
Thanks in advance.
As pointed out by Sunil Pai in this blog: https://github.com/threepointone/react-act-examples/blob/master/sync.md
Here's how I managed to solve this:
import {request} from '../request';
jest.mock('../request');
test('MyComponent', async () => {
let resolve;
request.mockImplementation(() => new Promise(resolve => {
// Save the resolver to a local variable
// so that we can trigger the resolve action later
resolve = _resolve;
}));
let result;
await act(async () => {
result = render(<MyComponent />);
});
// Unlike the non-mocked example in the question, we see '0' as the result
// This is because the value is not resolved yet
expect(result.container.firstChild.textContent).toBe('0');
// Now the setState will be called inside the useEffect hook
await act(async () => resolve());
// So now, the rendered value will be 1
expect(result.container.firstChild.textContent).toBe('1');
})

When testing, code that causes React state updates should be wrapped into act

I have this test:
import {
render,
cleanup,
waitForElement
} from '#testing-library/react'
const TestApp = () => {
const { loading, data, error } = useFetch<Person>('https://example.com', { onMount: true });
return (
<>
{loading && <div data-testid="loading">loading...</div>}
{error && <div data-testid="error">{error.message}</div>}
{data &&
<div>
<div data-testid="person-name">{data.name}</div>
<div data-testid="person-age">{data.age}</div>
</div>
}
</>
);
};
describe("useFetch", () => {
const renderComponent = () => render(<TestApp/>);
it('should be initially loading', () => {
const { getByTestId } = renderComponent();
expect(getByTestId('loading')).toBeDefined();
})
});
The test passes but I get the following warning:
Warning: An update to TestApp 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
in TestApp
console.error
node_modules/react-dom/cjs/react-dom.development.js:506
Warning: An update to TestApp 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
in TestApp
The key is to await act and then use async arrow function.
await act( async () => render(<TestApp/>));
Source:
https://stackoverflow.com/a/59839513/3850405
Try asserting inside 'await waitFor()' - for this your it() function should be async
it('should be initially loading', async () => {
const { getByTestId } = renderComponent();
await waitFor(() => {
expect(getByTestId('loading')).toBeDefined();
});
});
Keep calm and happy coding
I was getting the same issue which gets resolved by using async queries (findBy*) instead of getBy* or queryBy*.
expect(await screen.findByText(/textonscreen/i)).toBeInTheDocument();
Async query returns a Promise instead of element, which resolves when an element is found which matches the given query. The promise is rejected if no element is found or if more than one element is found after a default timeout of 1000ms. If you need to find more than one element, use findAllBy.
https://testing-library.com/docs/dom-testing-library/api-async/
But as you know it wont work properly if something is not on screen. So for queryBy* one might need to update test case accordingly
[Note: Here there is no user event just simple render so findBy will work otherwise we need to put user Event in act ]
Try using await inside act
import { act } from 'react-dom/test-utils';
await act(async () => {
wrapper = mount(Commponent);
wrapper.find('button').simulate('click');
});
test('handles server ok', async () => {
render(
<MemoryRouter>
<Login />
</MemoryRouter>
)
await waitFor(() => fireEvent.click(screen.getByRole('register')))
let domInfo
await waitFor(() => (domInfo = screen.getByRole('infoOk')))
// expect(domInfo).toHaveTextContent('登陆成功')
})
I solved the problem in this way,you can try it.
I don't see the stack of the act error, but I guess, it is triggered by the end of the loading when this causes to change the TestApp state to change and rerender after the test finished. So waiting for the loading to disappear at the end of the test should solve this issue.
describe("useFetch", () => {
const renderComponent = () => render(<TestApp/>);
it('should be initially loading', async () => {
const { getByTestId } = renderComponent();
expect(getByTestId('loading')).toBeDefined();
await waitForElementToBeRemoved(() => queryByTestId('loading'));
});
});
React app with react testing library:
I tried a lot of things, what worked for me was to wait for something after the fireevent so that nothing happens after the test is finished.
In my case it was a calendar that opened when the input field got focus. I fireed the focus event and checked that the resulting focus event occured and finished the test. I think maybe that the calendar opened after my test was finished but before the system was done, and that triggered the warning. Waiting for the calendar to show before finishing did the trick.
fireEvent.focus(inputElement);
await waitFor(async () => {
expect(await screen.findByText('December 2022')).not.toBeNull();
});
expect(onFocusJestFunction).toHaveBeenCalledTimes(1);
// End
Hopes this helps someone, I just spent half a day on this.
This is just a warning in react-testing-library (RTL). you do not have to use act in RTL because it is already using it behind the scenes. If you are not using RTL, you have to use act
import {act} from "react-dom/test-utils"
test('',{
act(()=>{
render(<TestApp/>)
})
})
You will see that warning when your component does data fetching. Because data fetching is async, when you render the component inside act(), behing the scene all the data fetching and state update will be completed first and then act() will finish. So you will be rendering the component, with the latest state update
Easiest way to get rid of this warning in RTL, you should run async query functions findBy*
test("test", async () => {
render(
<MemoryRouter>
<TestApp />
</MemoryRouter>
);
await screen.findByRole("button");
});

Wrapping async moxios call in act callback

I am trying to test a react functional component using hooks. The useEffect hook makes a call to a third part API which then calls setState on return.
I have the test working but keep getting a warning that an update to the component was not wrapped in act.
The problem I have is that the expectation is inside a moxios.wait promise and therefore I cannot wrap that in an act function and then assert on the result of that.
The test passes but I know not wrapping code that updates state in an act function could lead to false positives or uncovered bugs. I'm just wondering how I should be testing this.
I've tried using the new async await act function in the react 16.9.0 alpha release as well as numerous suggestions I've found in many github issues like jest setTimers and none seem to solve the issue.
The component
const Benefits = props => {
const [benefits, setBenefits] = useState([])
const [editing, setEditing] = useState(false)
const [editingBenefit, setEditingBenefit] = useState({id: null, name: '', category: ''})
useEffect(() => {
axios.get('#someurl')
.then(response => {
setBenefits(response.data)
})
}, [])
}
The test
describe('Benefits', () => {
it('fetches the list of benefits from an api and populates the benefits table', (done) => {
const { rerender } = render(<Benefits />)
moxios.wait(() => {
const request = moxios.requests.mostRecent()
request.respondWith({
status: 200,
response: benefits
}).then(() => {
expect(document.querySelectorAll('tbody > tr').length).toBe(2)
done()
})
})
})
})
The test passes but I get the following warning
Warning: An update to Benefits 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.
in Benefits (at benefits.spec.js:28)
from react 16.9.0 you can use async/await act
Your code should look like this
describe('Benefits', () => {
it('fetches the list of benefits from an api and populates the benefits table', async() => {
const { rerender } = render(<Benefits />);
await moxios.wait(jest.fn);
await act(async() => {
const request = moxios.requests.mostRecent()
await request.respondWith({
status: 200,
response: benefits
});
});
expect(document.querySelectorAll('tbody > tr').length).toBe(2)
})
I use jest.fn in moxios.wait because it needs callback function

How to mock or assert whether window.alert has fired in React & Jest with typescript?

I am using jest tests to test my React project written in #typescript created with Create React App. I'm using react-testing-library. I have a form component which shows an alert if the form is submitted empty. I wanted to test this feature (maybe) by spying/mocking the window.alert but it doesn't work.
I tried using jest.fn() as suggested in many SO answers but that's not working too.
window.alert = jest.fn();
expect(window.alert).toHaveBeenCalledTimes(1);
Here's how I implemented it: Form.tsx
async handleSubmit(event: React.FormEvent<HTMLFormElement>) {
// check for errors
if (errors) {
window.alert('Some Error occurred');
return;
}
}
Here's how I built the React+Jest+react-testing-library test: Form.test.tsx
it('alerts on submit click', async () => {
const alertMock = jest.spyOn(window,'alert');
const { getByText, getByTestId } = render(<Form />)
fireEvent.click(getByText('Submit'))
expect(alertMock).toHaveBeenCalledTimes(1)
})
I think you might need to tweak your test ever so slightly by adding .mockImplementation() to your spyOn like so:
it('alerts on submit click', async () => {
const alertMock = jest.spyOn(window,'alert').mockImplementation();
const { getByText, getByTestId } = render(<Form />)
fireEvent.click(getByText('Submit'))
expect(alertMock).toHaveBeenCalledTimes(1)
})
You could try to use global instead of window:
global.alert = jest.fn();
expect(global.alert).toHaveBeenCalledTimes(1);
Alternatively, try to Object.assign
const alert = jest.fn()
Object.defineProperty(window, 'alert', alert);

Resources