Make Unit Test Wait for Data filled by Asynchronous Fetch - reactjs

I have a unit test which includes rendering a component that uses useSWR to fetch data. But the data is not ready before the expect() is being called so the test fails.
test("StyleOptionDetails contains correct style option number", () => {
renderWithMockSwr(
<StyleOptionDetails
{...mockIStyleOptionDetailsProps}
/>
)
expect(screen.getByText(123)).toBeInTheDocument();
});
But if I put in a delay setTimeout(), it will pass the test.
setTimeout(() => {
console.log('This will run after 2 second')
expect(screen.getByText(123)).toBeInTheDocument();
}, 2000);
What is the correct way to create a delay or wait for the data?

Although I think you are already doing this, the first thing to note is that you shouldn't be actually fetching any data from your tests-- you should be mocking the result.
Once you are doing that, you will use the waitFor utility to aid in your async testing-- this utility basically accepts a function that returns an expectation (expect), and will hold at that point of the test until the expectation is met.
Let's provide an example. Take the imaginary component I've created below:
const MyComponent = () => {
const [data, setData] = useState();
useEffect(() => {
MyService.fetchData().then((response) => setData(response));
}, []);
if (!data) {
return (<p>Loading...</p>);
}
// else
return (
<div>
<h1>DATA!</h1>
<div>
{data.map((datum) => (<p>{datum}</p>))}
</div>
</div>
);
}
So for your test, you would do
import MyService from './MyService';
import MyComponent from './MyComponent';
describe('MyComponent', () => {
const mockData = ['Spengler', 'Stanz', 'Venkman', 'Zeddmore'];
beforeEach(() => {
jest.spyOn(MyService, 'fetchData')
.mockImplementation(
() => new Promise((res) => setTimeout(() => res(mockData), 200))
);
});
afterEach(() => {
MyService.fetchData.mockRestore();
});
it('displays the loading first and the data once fetched', async () => {
render(<MyComponent />);
// before fetch completes
expect(screen.getByText(/Loading/)).toBeInTheDocument();
expect(screen.queryByText('DATA!')).toBeNull();
// after fetch completes..
// await waitFor will wait here for this to be true; if it doesn't happen after about five seconds the test fails
await waitFor(() => expect(screen.getByText('DATA!')).toBeInTheDocument());
expect(screen.queryByText(/Loading/)).toBeNull(); // we don't have to await this one because we awaited the proper condition in the previous line
});
});
This isn't tested but something like this should work. Your approach to mocking may vary a bit on account of however you've structured your fetch.

I have a similar answer elsewhere for a ReactJS question, Web Fetch API (waiting the fetch to complete and then executed the next instruction). Let me thresh out my solution for your problem.
If your function renderWithMockSwr() is asynchronous, then if you want it to wait to finish executing before calling the next line, use the await command.
await renderWithMockSwr(
<StyleOptionDetails
{...mockIStyleOptionDetailsProps}
/>
)
async is wonderful. So is await. Check it out: Mozilla Developer Network: Async Function

Related

Repeat call to React Component every 60 seconds to automatically refresh front end display

I have a React component that fetches data from a url fed by a Flask backend and displays a line graph using that data. I want to refresh the graph on the front end without the user having to manually refresh the page.
The code is:
function Graph() {
const [graphData, setGraphData] = useState([]);
useEffect(() => {
fetchGraphData();
}, []);
const fetchGraphData = async () => {
const result = await fetch("url")
const fetchedGraphData = await result.json();
if(res.ok) {
setGraphData(fetchedGraphData);
}
};
return (
<div>
<Container>
<LineChart>
....
</LineChart>
</Container>
</div>
);
}
I tried using setInterval and setTimeout everywhere and any way I could think of but I can't get it to work at all.
I've gather that setTimeout should be better because it would be guaranteed to wait for a server response before executing again, as opposed to setInterval which could queue up queries in case of delays.
Just need a timer to achieve that goal.
In your useEffect try writing this:
useEffect(() => {
const interval = setInterval(() => {
fetchGraphData();
},60*1000);
return () => clearInterval(interval);
}, []);
In return all is just a cleanup function to ensure there is no timer running after unmounting the component.
To know more about cleanup function https://reactjs.org/docs/hooks-effect.html#example-using-hooks-1

How many render operations does act() support?

I am using act function from library https://testing-library.com/docs/react-testing-library/intro/.
At this point I have something like this:
await act(async () => {
page = renderPage([mockData, mockData2]);
});
await act(async () => {
onLoadSuccessFn?.({ numPages: 15 } as PDFDocumentProxy);
await waitForExpect(() => {
expect(page?.find('[id*="idToBeFound"]')).toBeTruthy();
});
});
In the first act I am waiting for the page/component to be rendered. In the second act I am calling onLoadSuccessFn, which will trigger the rendering of another component.
However, if I group the 2 pieces of code(render of the component and function call described above) under the same act, my test will fail, basically because only one statement will be executed(out of the 2)

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

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

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

How to unit test a React component that renders after fetch has finished?

I'm a Jest/React beginner. In jest's it I need to wait until all promises have executed before actually checking.
My code is similar to this:
export class MyComponent extends Component {
constructor(props) {
super(props);
this.state = { /* Some state */ };
}
componentDidMount() {
fetch(some_url)
.then(response => response.json())
.then(json => this.setState(some_state);
}
render() {
// Do some rendering based on the state
}
}
When the component is mounted, render() runs twice: once after the constructor runs, and once after fetch() (in componentDidMount()) finishes and the chained promises finish executing).
My testing code is similar to this:
describe('MyComponent', () => {
fetchMock.get('*', some_response);
it('renders something', () => {
let wrapper = mount(<MyComponent />);
expect(wrapper.find(...)).to.have.something();
};
}
Whatever I return from it, it runs after the first time render() executes but before the second time. If, for example, I return fetchMock.flush().then(() => expect(...)), the returned promise executes before the second call to render() (I believe I can understand why).
How can I wait until the second time render() is called before running expect()?
I'd separate concerns, mainly because is easier to maintain and to test. Instead of declaring the fetch inside the component I'd do it somewhere else, for example in a redux action (if using redux).
Then test individually the fetch and the component, after all this is unit testing.
For async tests you can use the done parameter on the test. For example:
describe('Some tests', () => {
fetchMock.get('*', some_response);
it('should fetch data', (done) => { // <---- Param
fetchSomething({ some: 'Params' })
.then(result => {
expect(result).toBe({ whatever: 'here' });
done(); // <--- When you are done
});
});
})
The you can tests your component by just sending the loaded data in the props.
describe('MyComponent', () => {
it('renders something', () => {
const mockResponse = { some: 'data' };
let wrapper = mount(<MyComponent data={mockResponse}/>);
expect(wrapper.find(...)).to.have.something();
});
});
When it comes to testing you need to keep it simple, if your component is difficult to test, then there's something wrong with your design ;)
I've had some success with this, as it doesn't require wrapping or modifying components. It is however assuming there's only one fetch() in the component, but it can be easily modified if needed.
// testhelper.js
class testhelper
{
static async waitUntil(fnWait) {
return new Promise((resolve, reject) => {
let count = 0;
function check() {
if (++count > 20) {
reject(new TypeError('Timeout waiting for fetch call to begin'));
return;
}
if (fnWait()) resolve();
setTimeout(check, 10);
}
check();
});
}
static async waitForFetch(fetchMock)
{
// Wait until at least one fetch() call has started.
await this.waitUntil(() => fetchMock.called());
// Wait until active fetch calls have completed.
await fetchMock.flush();
}
}
export default testhelper;
Then you can use it just before your assertions:
import testhelper from './testhelper.js';
it('example', async () => {
const wrapper = mount(<MyComponent/>);
// Wait until all fetch() calls have completed
await testhelper.waitForFetch(fetchMock);
expect(wrapper.html()).toMatchSnapshot();
});
I found a way to do what I originally asked. I have no opinion (yet) whether it is good strategy or not (in fact I had to refactor the component immediately afterwards, so this question is no longer relevant to what I'm doing). Anyway, here is the testing code (explanation below):
import React from 'react';
import { mount } from 'enzyme';
import { MyComponent } from 'wherever';
import fetchMock from 'fetch-mock';
let _resolveHoldingPromise = false;
class WrappedMyComponent extends MyComponent {
render() {
const result = super.render();
_resolveHoldingPromise && _resolveHoldingPromise();
_resolveHoldingPromise = false;
return result;
}
static waitUntilRender() {
// Create a promise that can be manually resolved
let _holdingPromise = new Promise(resolve =>
_resolveHoldingPromise = resolve);
// Return a promise that will resolve when the component renders
return Promise.all([_holdingPromise]);
}
}
describe('MyComponent', () => {
fetchMock.get('*', 'some_response');
const onError = () => { throw 'Internal test error'; };
it('renders MyComponent appropriately', done => {
let component = <WrappedMyComponent />;
let wrapper = mount(component);
WrappedMyComponent.waitUntilRender().then(
() => {
expect(wrapper.find('whatever')).toBe('whatever');
done();
},
onError);
});
});
The main idea is that, in the testing code, I subclass the component (if this was Python I'd probably monkey-patch it, which works more or less the same way in this case) so that its render() method sends a signal that it executed. The way to send the signal is by manually resolving a promise. When a promise is created, it creates two functions, resolve and reject, which when called terminate the promise. The way to have code outside the promise resolve the promise is by having the promise store a reference to its resolve function in an external variable.
Thanks to fetch-mock author Rhys Evans who kindly explained the manually-resolve-promise trick to me.

Resources