I am trying to write wrap up writing some tests for this component. I implemented a custom useInterval hook following this blog post from Dan Abramov (code). It basically makes a declarative setInterval function. However, I'm having a difficult time of testing it with Jest and Enzyme. The application was bootstrapped with create-react-app. I have installed the latest versions. When I click the start button, the elapsed time increases and displays on the page perfectly. In the parent component, I update state, which stores elapsed time. It passes the updated elapsed time timer props to Timer.js. Thus, it sows on the screen the correct elapsedTime. This works as expected in the browser. However, the timer doesn't run when running the tests.
// Timer.js
const TimerDisplay = ({ timer, updateTimer }) => {
useInterval(() => {
const delta = Math.floor((Date.now() - timer.offsetTime) / 1000);
updateTimer({ ...timer, elapsedTime: delta });
}, timer.isTicking ? 300 : null);
const handleStartButton = () => {
updateTimer({ ...timer, isTicking: true });
}
return (
<React.Fragment>
<div>{timer.elapsedTime}</div>
<button className='start' onClick={() => handleStartButton()}>Start</button>
<button className='stop' {/* removed for brevity*/}>Stop</button>
</React.Fragment>
);
};
The test's code is below. I use Jest's spy function and enzyme's mount. I read that I needed to mount and not use shallow because of the hooks. I set Jest to use the fake timers. Then, I simulate a button press of the start button to verify that the button is working correctly. However, in this test, I've already set isTicking: true so I really wouldn't even need to simulate the start. Though it's a sanity check that the function spy works as expected - which it does. The expected result is that it calls the spy callback after 300 ms. Thus, when I jest.advanceTimersByTime(500), the spy function should have been called at least once in the useInterval callback. However, this isn't occurring in the tests.
// Timer.spec.js
describe('Timer', () => {
const spyFunction = jest.fn();
const timer = {
offsetTime: new Date(),
isTicking: true,
elapsedTime: 0
};
let wrapper = null;
beforeEach(() => {
wrapper = mount(<Timer updateTimer={spyFunction} timer={timer} />);
});
afterEach(() => {
wrapper.unmount();
wrapper = null;
});
it('timer should run', () => {
jest.useFakeTimers();
expect(spyFunction).not.toHaveBeenCalled();
wrapper.find('button.start').simulate('click', { button: 0 });
// works as expected
expect(spyFunction).toHaveBeenCalled();
// doesn't seem to work for some reason
jest.advanceTimersByTime(500);
// will fail; function only called once by the button.start. It should have been called at least twice.
expect(spyFunction).toHaveBeenCalledTimes(2);
});
});
I'm thinking the issue has to do with the useInterval hook. I suspect garbage collection is happening before the callback is able to be called. Is there any way to test the useInterval hook's callback's calls updateTimer aka the Jest.Fn?
// useInterval hook
import { useEffect, useRef } from 'react';
export const useInterval = (callback, delay) => {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
};