React testing library - fakeTimers with waitFor/waitForElementToBeRemoved - reactjs

Last time I have updated testing-library/dom from version 7.29.4 to 8.0.0. After that tests which have jest.useFakeTimers stopped working whenever waitFor/waitForElementToBeRemoved is used.
export default function Test() {
const [loaded, setLoaded] = useState(false);
const getDataCallback = useCallback(() => {
return getData();
}, [])
useEffect(() => {
getDataCallback().then(data => {
setLoaded(true)
});
}, [])
return (
<>
{
loaded ?
<>
{new Date().toDateString()} //displays current date
</>
: <Loader/>
}
</>
)}
Test code:
const mockFunc = jest.spyOn(api, "getData");
const fakeData = [{ date: "2020-01"}, { date: "2020-02"},];
beforeEach(() => {
jest.useFakeTimers("modern").setSystemTime(new Date(2020, 2, 3));
mockFunc.mockResolvedValue(fakeData);
})
it("test", async () => {
render(<Test />);
await waitForElementToBeRemoved(screen.queryByTestId("loader"));
expect(screen.getByText(/tue mar 03 2020/i)).toBeInTheDocument();
})
In this code it's some fake api call, when it's done then we want to display the current date. If the call is not finished, then some loader/spinner is on the screen. When I remove loader state and waitForElementToBeRemoved() from code I have mocked date on the screen and everything works like expected, otherwise real date is displayed.

I'm not sure what is happening inside of your getData, but if it is using setTimeout or similar, then you need to tell jest to advance the fake timers or flush them.
I had a similar issue where I was using real timers and all tests passed, then when using fake timers they all failed. In my scenario I think it was because my tests were not waiting for the timeout to finish and just immediately executed assertions as if timeouts had passed when they really hadn't. Adding jest.advanceTimersByTime(theSetTimeoutTime) before the calls to waitForElementToBeRemoved fixed my tests in almost all cases.

Related

Previous data showing even though cleaning up in useEffect

I have a component in my react native app that loads sessions related to a particular individual. In the useEffect() of that component I both load the sessions when the component comes into focus, and unload those sessions within the cleanup.
export const ClientScreen = (props) => {
const isFocused = useIsFocused();
const client = useSelector((state) => selectActiveClient(state));
useEffect(() => {
if (isFocused) {
const loadSessions = async () => {
if (client?.id) {
dispatch(await loadClientSessions(client?.id));
}
return () => dispatch(unloadSessions()); // Cleaning up here...
};
loadSessions(props);
}
}, [isFocused, client?.id]);
const updatedProps = {
...props,
client,
};
return <ClientBottomTabNavigator {...updatedProps} />;
};
Generally the component is working as expected. However, I do notice that if I load the component with one client, then navigate away, and then come back to the component by loading a new client, that for a brief moment the sessions pertaining to the previous client show before being replaced the sessions relevant to the new client.
My question is, shouldn't the unloadVisits() that runs on cleanup -- which sets sessions to an empty array -- prevent this? Or is this some kind of react behavior that's holding onto the previous state of the component? How can I ensure this behavior doesn't occur?
Cleanup function should appear before the closing-brace of the useEffect hook
useEffect(() => {
if (isFocused) {
const loadSessions = async () => {
if (client?.id) {
dispatch(await loadClientSessions(client?.id));
}
};
loadSessions(props);
}
return () => dispatch(unloadSessions()); // Cleaning up here... // <--- here
}, [isFocused, client?.id]);
as commented, your loadSessions returns a cleanup function, but you don't do anything with it. And the effect where you call loadSessions(props) does not return anything, that's why it does not clean up.
Edit:
I made a mistake, loadSessions returns a Promise of a cleanup function. And it is impossible to "unwrap" this Promise and get to the cleanup function itself in a way that you can return it in your effect. You have to move the cleaup function out of the async function loadSessions.
But you don't need async/await for everything:
useEffect(() => {
if (isFocused && client?.id) {
loadClientSessions(client.id).then(dispatch);
return () => dispatch(unloadSessions());
}
}, [isFocused, client?.id]);

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

why is afterSetExtremes function being called twice highcharts using react

I want to implement lazy loading functionality as show here.
jsfiddle.net/uf34oecg/
If you check its console, it prints loadInitialData once only at the time of loading the chart for the first time.
Then it never prints it or once you select 1m (after first load), it calls afterSetExtremes function and prints afterSetExtremes.
I found the same example converted to reactjs code
https://codesandbox.io/s/highcharts-react-demo-forked-wvrd6?file=/demo.jsx:673-688
In this example when you load for the first time, it calls afterSetExtremes twice which is wrong. It should not call afterSetExtremes at initial loading.
I don't understand why is that happening? I'm unable to figure it out after spending couple of hours.
Because of it, it behaves badly in real project.
That's because there are different orders of action.
With React:
Chart is created without data
Data is received and the chart is updated
Without React:
Data is received
Chart is created
As a solution, you can call loadInitialData in useEffect hook and render HighchartsReact component after the data has been received.
const ChartComponent = () => {
const [options, setOptions] = useState(null);
useEffect(() => {
loadInitialData();
}, []);
function loadInitialData() {
fetch(dataURL)
.then((res) => res.ok && res.json())
.then((data) => {
...
setOptions({
...chartOptions,
series: [
{
data
}
],
...
});
});
}
function afterSetExtremes(e) {
...
}
return (
options && (
<HighchartsReact
constructorType="stockChart"
highcharts={Highcharts}
options={options}
/>
)
);
};
Live demo: https://codesandbox.io/s/highcharts-react-3tcdw7?file=/demo.jsx

Make Unit Test Wait for Data filled by Asynchronous Fetch

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

Jest: setTimeout is being called too many times

I am testing a react component that uses setTimeout. The problem is that Jest is saying that setTimeout is called even though it clearly isn't. There is a setTimeout to remove something from the ui and another one to pause the timer when the mouse is hovering over the component.
I tried adding a console.log() where the setTimeout is and the console log is never called, which means the setTimeout in the app isn't being called.
//app
const App = (props) => {
const [show, setShow] = useState(true);
const date = useRef(Date.now());
const remaining = useRef(props.duration);
let timeout;
useEffect(() => {
console.log('Should not run');
if (props.duration) {
timeout = setTimeout(() => {
setShow(false)
}, props.duration);
}
}, [props.duration]);
const pause = () => {
remaining.current -= Date.now() - date.current;
clearTimeout(timeout);
}
const play = () => {
date.current = Date.now();
clearTimeout(timeout);
console.log('should not run');
timeout = setTimeout(() => {
setIn(false);
}, remaining.current);
}
return (
<div onMouseOver={pause} onMouseLeave={play}>
{ show &&
props.content
}
</div>
)
}
//test
it('Should not setTimeout when duration is false', () => {
render(<Toast content="" duration={false} />);
//setTimeout is called once but does not come from App
expect(setTimeout).toHaveBeenCalledTimes(0);
});
it('Should pause the timer when pauseOnHover is true', () => {
const { container } = render(<Toast content="" pauseOnHover={true} />);
fireEvent.mouseOver(container.firstChild);
expect(clearTimeout).toHaveBeenCalledTimes(1);
fireEvent.mouseLeave(container.firstChild);
//setTimeout is called 3 times but does not come from App
expect(setTimeout).toHaveBeenCalledTimes(1);
});
So in the first test, setTimeout shouldn't be called but I receive that its called once. In the second test, setTimeout should be called once but is called 3 times. The app works fine I just don't understand what is going on with jest suggesting that setTimeout is being called more than it is.
I'm experiencing the exact same issue with the first of my Jest test always calling setTimeout once (without my component triggering it). By logging the arguments of this "unknown" setTimeout call, I found out it is invoked with a _flushCallback function and a delay of 0.
Looking into the repository of react-test-renderer shows a _flushCallback function is defined here. The Scheduler where _flushCallback is part of clearly states that it uses setTimeout when it runs in a non-DOM environment (which is the case when doing Jest tests).
I don't know how to properly proceed on researching this, for now, it seems like tests for the amount of times setTimeout is called are unreliable.
Thanks to #thabemmz for researching the cause of this, I have a hacked-together solution:
function countSetTimeoutCalls() {
return setTimeout.mock.calls.filter(([fn, t]) => (
t !== 0 ||
!String(fn).includes('_flushCallback')
));
}
Usage:
// expect(setTimeout).toHaveBeenCalledTimes(2);
// becomes:
expect(countSetTimeoutCalls()).toHaveLength(2);
It should be pretty clear what the code is doing; it filters out all calls which look like they are from that react-test-renderer line (i.e. the function contains _flushCallback and the timeout is 0.
It's brittle to changes in react-test-renderer's behaviour (or even function naming), but does the trick for now at least.

Resources