Mock requestAnimationFrame in Jest
When I wrote test for my tiny rhythm game, I realized that all the browser based actions needed to be mocked, including window.requestAnimationFrame. I created a custom hook useCanvas which takes a draw function and renders the content under canvas every frame using requestAnimationFrame. I decided to write a test to test this functionality.
export default function useCanvas(draw) {
const canvasRef = useRef(null);
const loopId = useRef();
const previousTime = useRef();
useEffect(() => {
const canvas = canvasRef.current;
const context = canvas.getContext('2d');
const loop = time => {
if (previousTime.current) {
context.clearRect(0, 0, canvas.width, canvas.height);
draw(context, 0.001 * (time - previousTime.current));
}
previousTime.current = time;
loopId.current = requestAnimationFrame(loop);
}
loopId.current = requestAnimationFrame(loop);
return () => cancelAnimationFrame(loopId.current);
}, [draw]);
return canvasRef;
}
I searched "mock requestAnimationFrame" directly on the Jest doc, but got no hint. Then googled it and got a potential solution:
beforeEach(() => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb());
});
afterEach(() => {
window.requestAnimationFrame.mockRestore();
});
However, this solution produces infinite call loop, because it calls cb immediately, and then in cb the requestAnimationFrame calls cb again, and again… Hm, I realized that it was close. I only needed to fix this issue. How about delaying the call of cb?
Then setTimeout came to help. But also setTimeout needs to be mocked in Jest to make it move forward. Fortunately, Jest has already worked it out, see Timer Mocks. So my final solution is:
beforeEach(() => {
jest.useFakeTimers();
let count = 0;
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => setTimeout(() => cb(100*(++count)), 100));
});
afterEach(() => {
window.requestAnimationFrame.mockRestore();
jest.clearAllTimers();
});
Then in test mock the timer:
act(() => {
jest.advanceTimersByTime(200);
});
The 100 in setTimeout acts like the deltaTime between frames, and can be custom defined.