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');
});