RhinoBomb
RhinoBomb

Reputation: 463

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.

Upvotes: 6

Views: 2816

Answers (2)

Dave
Dave

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

thabemmz
thabemmz

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

Related Questions