Jest.js 如何测试在玩笑中使用'requestAnimationFrame'的代码?

djp7away  于 2022-12-08  发布在  Jest
关注(0)|答案(6)|浏览(202)

I want to write a jest unit test for a module that uses requestAnimationFrame and cancelAnimationFrame .
I tried overriding window.requestAnimationFrame with my own mock (as suggested in this answer ), but the module keeps on using the implementation provided by jsdom.
My current approach is to use the (somehow) builtin requestAnimationFrame implementation from jsdom, which seems to use setTimeout under the hood, which should be mockable by using jest.useFakeTimers() .

jest.useFakeTimers();

describe("fakeTimers", () => {
    test.only("setTimeout and trigger", () => {
        const order: number[] = [];
        
        expect(order).toEqual([]);
        setTimeout(t => order.push(1));
        expect(order).toEqual([]);
        jest.runAllTimers();
        expect(order).toEqual([1]);
    });

    test.only("requestAnimationFrame and runAllTimers", () => {
        const order: number[] = [];
        
        expect(order).toEqual([]);
        requestAnimationFrame(t => order.push(1));
        expect(order).toEqual([]);
        jest.runAllTimers();
        expect(order).toEqual([1]);
    });
});

The first test is successful, while the second fails, because order is empty.
What is the correct way to test code that relies on requestAnimationFrame() . Especially if I need to test conditions where a frame was cancelled?

h4cxqtbf

h4cxqtbf1#

这里的解决方案来自玩笑问题:

beforeEach(() => {
  jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => cb());
});

afterEach(() => {
  window.requestAnimationFrame.mockRestore();
});
q1qsirdb

q1qsirdb2#

I'm not sure this solution is perfect but this works for my case.
There are two key principles working here.

1) Create a delay that is based on requestAnimationFrame:

const waitRAF = () => new Promise(resolve => requestAnimationFrame(resolve));

2) Make the animation I am testing run very fast:

In my case the animation I was waiting on has a configurable duration which is set to 1 in my props data.
Another solution to this could potentially be running the waitRaf method multiple times but this will slow down tests.

  • You may also need to mock requestAnimationFrame but that is dependant on your setup, testing framework and implementation*
    My example test file (Vue app with Jest):
import { mount } from '@vue/test-utils';
import AnimatedCount from '@/components/AnimatedCount.vue';

const waitRAF = () => new Promise(resolve => requestAnimationFrame(resolve));

let wrapper;
describe('AnimatedCount.vue', () => {
  beforeEach(() => {
    wrapper = mount(AnimatedCount, {
      propsData: {
        value: 9,
        duration: 1,
        formatDisplayFn: (val) => "£" + val
      }
    });
  });

  it('renders a vue instance', () => {
    expect(wrapper.isVueInstance()).toBe(true);
  });

  describe('When a value is passed in', () => {
    it('should render the correct amount', async () => {
      const valueOutputElement = wrapper.get("span");
      wrapper.setProps({ value: 10 });

      await wrapper.vm.$nextTick();
      await waitRAF();

      expect(valueOutputElement.text()).toBe("£10");
    })
  })
});
wbrvyc0a

wbrvyc0a3#

所以,我自己找到了解决办法。
我确实需要覆盖window.requestAnimationFramewindow.cancelAnimationFrame
问题是,我没有正确地包含mock模块。

// mock_requestAnimationFrame.js

class RequestAnimationFrameMockSession {
    handleCounter = 0;
    queue = new Map();
    requestAnimationFrame(callback) {
        const handle = this.handleCounter++;
        this.queue.set(handle, callback);
        return handle;
    }
    cancelAnimationFrame(handle) {
        this.queue.delete(handle);
    }
    triggerNextAnimationFrame(time=performance.now()) {
        const nextEntry = this.queue.entries().next().value;
        if(nextEntry === undefined) return;

        const [nextHandle, nextCallback] = nextEntry;

        nextCallback(time);
        this.queue.delete(nextHandle);
    }
    triggerAllAnimationFrames(time=performance.now()) {
        while(this.queue.size > 0) this.triggerNextAnimationFrame(time);
    }
    reset() {
        this.queue.clear();
        this.handleCounter = 0;
    }
};

export const requestAnimationFrameMock = new RequestAnimationFrameMockSession();

window.requestAnimationFrame = requestAnimationFrameMock.requestAnimationFrame.bind(requestAnimationFrameMock);
window.cancelAnimationFrame = requestAnimationFrameMock.cancelAnimationFrame.bind(requestAnimationFrameMock);

mock必须在导入任何可能调用requestAnimationFrame的模块 * 之前 * 导入。

// mock_requestAnimationFrame.test.js

import { requestAnimationFrameMock } from "./mock_requestAnimationFrame";

describe("mock_requestAnimationFrame", () => {
    beforeEach(() => {
        requestAnimationFrameMock.reset();
    })
    test("reqest -> trigger", () => {
        const order = [];
        expect(requestAnimationFrameMock.queue.size).toBe(0);
        expect(order).toEqual([]);

        requestAnimationFrame(t => order.push(1));

        expect(requestAnimationFrameMock.queue.size).toBe(1);
        expect(order).toEqual([]);

        requestAnimationFrameMock.triggerNextAnimationFrame();

        expect(requestAnimationFrameMock.queue.size).toBe(0);
        expect(order).toEqual([1]);
    });

    test("reqest -> request -> trigger -> trigger", () => {
        const order = [];
        expect(requestAnimationFrameMock.queue.size).toBe(0);
        expect(order).toEqual([]);

        requestAnimationFrame(t => order.push(1));
        requestAnimationFrame(t => order.push(2));

        expect(requestAnimationFrameMock.queue.size).toBe(2);
        expect(order).toEqual([]);

        requestAnimationFrameMock.triggerNextAnimationFrame();

        expect(requestAnimationFrameMock.queue.size).toBe(1);
        expect(order).toEqual([1]);

        requestAnimationFrameMock.triggerNextAnimationFrame();

        expect(requestAnimationFrameMock.queue.size).toBe(0);
        expect(order).toEqual([1, 2]);
    });

    test("reqest -> cancel", () => {
        const order = [];
        expect(requestAnimationFrameMock.queue.size).toBe(0);
        expect(order).toEqual([]);

        const handle = requestAnimationFrame(t => order.push(1));

        expect(requestAnimationFrameMock.queue.size).toBe(1);
        expect(order).toEqual([]);

        cancelAnimationFrame(handle);

        expect(requestAnimationFrameMock.queue.size).toBe(0);
        expect(order).toEqual([]);
    });

    test("reqest -> request -> cancel(1) -> trigger", () => {
        const order = [];
        expect(requestAnimationFrameMock.queue.size).toBe(0);
        expect(order).toEqual([]);

        const handle = requestAnimationFrame(t => order.push(1));
        requestAnimationFrame(t => order.push(2));

        expect(requestAnimationFrameMock.queue.size).toBe(2);
        expect(order).toEqual([]);

        cancelAnimationFrame(handle);

        expect(requestAnimationFrameMock.queue.size).toBe(1);
        expect(order).toEqual([]);

        requestAnimationFrameMock.triggerNextAnimationFrame();

        expect(requestAnimationFrameMock.queue.size).toBe(0);
        expect(order).toEqual([2]);
    });

    test("reqest -> request -> cancel(2) -> trigger", () => {
        const order = [];
        expect(requestAnimationFrameMock.queue.size).toBe(0);
        expect(order).toEqual([]);

        requestAnimationFrame(t => order.push(1));
        const handle = requestAnimationFrame(t => order.push(2));

        expect(requestAnimationFrameMock.queue.size).toBe(2);
        expect(order).toEqual([]);

        cancelAnimationFrame(handle);

        expect(requestAnimationFrameMock.queue.size).toBe(1);
        expect(order).toEqual([]);

        requestAnimationFrameMock.triggerNextAnimationFrame();

        expect(requestAnimationFrameMock.queue.size).toBe(0);
        expect(order).toEqual([1]);
    });

    test("triggerAllAnimationFrames", () => {
        const order = [];
        expect(requestAnimationFrameMock.queue.size).toBe(0);
        expect(order).toEqual([]);

        requestAnimationFrame(t => order.push(1));
        requestAnimationFrame(t => order.push(2));

        requestAnimationFrameMock.triggerAllAnimationFrames();

        expect(order).toEqual([1,2]);

    });

    test("does not fail if triggerNextAnimationFrame() is called with an empty queue.", () => {
        requestAnimationFrameMock.triggerNextAnimationFrame();
    })
});
yws3nbqq

yws3nbqq4#

Here is my solution inspired by the first answer.

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

Directly call cb in mockImplementation will produce infinite call loop. So I make use of the Jest Timer Mocks to get it under control.

qxgroojn

qxgroojn5#

我的 typescript 解决方案。我认为通过让每一帧的时间过得非常快,它会使动画非常(基本上是即时的)快。在某些情况下可能不是正确的解决方案,但我会说这将有助于许多。

let requestAnimationFrameSpy: jest.SpyInstance<number, [callback: FrameRequestCallback]>;

beforeEach(() => {
    let time = 0;
    requestAnimationFrameSpy = jest.spyOn(window, 'requestAnimationFrame')
      .mockImplementation((callback: FrameRequestCallback): number => {
        callback(time+=1000000);
        return 0;
      });
});

afterEach(() => {
    requestAnimationFrameSpy.mockRestore();
});
jum4pzuy

jum4pzuy6#

以前版本的问题是直接调用回调,这不反映requestAnimationFrame的异步性质。
下面是一个使用jest.useFakeTimers()来实现这一点的模拟,同时在代码执行时提供控制权:

beforeAll(() => {
  jest.useFakeTimers()
  let time = 0
  jest.spyOn(window, 'requestAnimationFrame').mockImplementation(
    // @ts-expect-error
    (cb) => {
    // we can then use fake timers to preserve the async nature of this call

    setTimeout(() => {
      time = time + 16 // 16 ms
      cb(time)
    }, 0)
  })
})
afterAll(() => {
  jest.useRealTimers()

  // @ts-expect-error
  window.requestAnimationFrame.mockRestore()
})

在测试中,您可以使用:

yourFunction() // will schedule a requestAnimation
jest.runAllTimers() // execute the callback
expect(....) // check that it happened

这有助于包含Zalgo

相关问题