我如何测试在玩笑中使用 `requestAnimationFrame` 的代码?

hth*_*tho 2 settimeout jsdom requestanimationframe jestjs

我想为使用requestAnimationFrame和的模块编写一个开玩笑的单元测试cancelAnimationFrame

我尝试用我自己的模拟覆盖 window.requestAnimationFrame(如本答案中所建议的那样),但该模块继续使用 jsdom 提供的实现。

我目前的方法是使用requestAnimationFrame来自 jsdom的(以某种方式)内置实现,它似乎在幕后使用setTimeout,应该可以通过使用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]);
    });
});
Run Code Online (Sandbox Code Playgroud)

第一次测试成功,第二次失败,因为order是空的。

测试依赖于requestAnimationFrame(). 特别是如果我需要测试帧被取消的条件?

Tom*_*yon 9

我不确定这个解决方案是否完美,但这适用于我的情况。

\n\n

这里有两个关键原则。

\n\n

1) 创建基于 requestAnimationFrame 的延迟:

\n\n
const waitRAF = () => new Promise(resolve => requestAnimationFrame(resolve));\n
Run Code Online (Sandbox Code Playgroud)\n\n

2)使我正在测试的动画运行得非常快:

\n\n

就我而言,我正在等待的动画有一个可配置的持续时间,在我的 props 数据中设置为 1。

\n\n

另一个解决方案可能是多次运行 waitRaf 方法,但这会减慢测试速度。

\n\n

您可能还需要模拟 requestAnimationFrame 但这取决于您的设置、测试框架和实现

\n\n

我的示例测试文件(带有 Jest 的 Vue 应用程序):

\n\n
import { mount } from \'@vue/test-utils\';\nimport AnimatedCount from \'@/components/AnimatedCount.vue\';\n\nconst waitRAF = () => new Promise(resolve => requestAnimationFrame(resolve));\n\nlet wrapper;\ndescribe(\'AnimatedCount.vue\', () => {\n  beforeEach(() => {\n    wrapper = mount(AnimatedCount, {\n      propsData: {\n        value: 9,\n        duration: 1,\n        formatDisplayFn: (val) => "\xc2\xa3" + val\n      }\n    });\n  });\n\n  it(\'renders a vue instance\', () => {\n    expect(wrapper.isVueInstance()).toBe(true);\n  });\n\n  describe(\'When a value is passed in\', () => {\n    it(\'should render the correct amount\', async () => {\n      const valueOutputElement = wrapper.get("span");\n      wrapper.setProps({ value: 10 });\n\n      await wrapper.vm.$nextTick();\n      await waitRAF();\n\n      expect(valueOutputElement.text()).toBe("\xc2\xa310");\n    })\n  })\n});\n
Run Code Online (Sandbox Code Playgroud)\n


hth*_*tho 7

所以,我自己找到了解决方案。

我真的需要重写window.requestAnimationFramewindow.cancelAnimationFrame

问题是,我没有正确包含模拟模块。

// 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);
Run Code Online (Sandbox Code Playgroud)

必须在导入任何可能调用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();
    })
});
Run Code Online (Sandbox Code Playgroud)


Pav*_*vel 7

这里是开玩笑问题的解决方案:

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

afterEach(() => {
  window.requestAnimationFrame.mockRestore();
});
Run Code Online (Sandbox Code Playgroud)

  • 知道如何用 Typescript 写这个吗? (2认同)
  • 上述模拟的一个问题是,如果您的回调再次调用 RAF,以便它在每个动画帧运行一次,则回调的直接调用可能会占用执行线程,并且还会因所有递归调用而耗尽堆栈。最好在模拟中使用 setTimeout 来允许 JS 事件循环运行,然后使用 Jest 的假计时器来允许模拟的 RAF 前进。 (2认同)