How to test a delayed event handler with requestAnimationFrame in Jest?
I've got a function that should be get executed in near future and I want to test it with jest but have no idea how to do this properly. Actually, I'm using React + testing react library but that doesn't change anything I suppose.
Here's the function itself:
let start;
const holdClick = (el, delayedClickHandler, time = 0) => {
if (time === 0) return delayedClickHandler();
let counter = 0;
function countToExecute() {
if (counter === time) delayedClickHandler();
else {
start = requestAnimationFrame(countToExecute);
counter += 1;
}
}
start = window.requestAnimationFrame(countToExecute);
el.ontouchend = () => {
window.cancelAnimationFrame(start);
};
};
My Component
function Component() {
const [clicked, setClicked] = useState(false);
return (
<span
// data attribute must be changed to "active" after 30 ticks
data-testid={clicked ? 'active' : 'static'}
onTouchStart={e => holdClick(e.target, () => setClicked(true), 30)}
>
click me
</span>
);
}
And my test
test('execute delayed click', () => {
beforeEach(() => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb());
});
const { getByTestId } = render(<App />);
const el = getByTestId('static');
fireEvent.click(el);
jest.useFakeTimers();
setTimeout(() => {
expect(el).toHaveAttribute('data-testid', 'active');
}, 10000);
jest.runAllTimers();
});
My guess is that the problem lays in requestAnimationFrame
but as I've mentioned before, I'm pretty much clueless of any ways to fix this. I've tried to add jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb());
but it didn't change anything. So I'm wondering if there's a solution that doesn't require a troublesome mock of window
object.
Solution 1:
First, your callback is set on the onTouchStart
prop, meaning you'll need to fire the touchStart
event in order to trigger it.
fireEvent.touchStart(el);
Second, in your countToExecute
function the counter
variable should be increased before the requestAnimationFrame
call, otherwise counter
will never get to increase. This is because the actual requestAnimationFrame
calls its callback on each repaint (each "tick"), but since it's mocked in the tests the callback gets called immediately.
function countToExecute() {
if (counter === time) {
delayedClickHandler();
} else {
counter += 1;
start = requestAnimationFrame(countToExecute);
}
}
Your test should then look like the following:
test('execute delayed click', () => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb());
const { getByTestId } = render(<MyComponent />);
const el = getByTestId('static');
fireEvent.touchStart(el);
expect(el).toHaveAttribute('data-testid', 'active');
});