Reputation: 463
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.
Upvotes: 6
Views: 2816
Reputation: 46249
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.
Upvotes: 4
Reputation: 131
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.
Upvotes: 8