You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Jest为自定义requestAnimationFrame(RAF)工具编写单元测试

测试实现方案

核心思路

你需要先Mock浏览器的requestAnimationFrame和cancelAnimationFrameAPI,把原本由浏览器调度的异步逻辑改成可手动控制的同步逻辑,这样就能精确控制每一帧的执行时机,验证回调触发的时机是否符合预期。同时因为你的代码里有模块级的全局变量i和idMap,每次测试前需要重置模块状态,避免不同测试用例互相干扰。


具体实现步骤

1. 编写Mock函数

首先实现RAF的模拟逻辑,把所有待执行的RAF回调存在队列里,提供手动执行指定帧数的方法:

// 放在测试文件顶部的mock逻辑
let rafQueue: Array<() => void> = []
let rafIdCounter = 0

beforeEach(() => {
  // 每次测试前重置队列和id计数
  rafQueue = []
  rafIdCounter = 0
  // 重置模块,清空i和idMap的状态
  jest.resetModules()

  // Mock requestAnimationFrame
  global.requestAnimationFrame = jest.fn((cb) => {
    const id = rafIdCounter++
    rafQueue.push(cb)
    return id
  })

  // Mock cancelAnimationFrame
  global.cancelAnimationFrame = jest.fn((id: number) => {
    rafQueue = rafQueue.filter((_, index) => index !== id)
  })
})

// 手动执行指定帧数的工具函数
function runRAFFrames(count: number) {
  for (let i = 0; i < count; i++) {
    // 每次执行前拷贝队列,避免执行过程中新增的回调影响当前帧
    const currentQueue = [...rafQueue]
    rafQueue = []
    currentQueue.forEach(cb => cb())
  }
}

2. 编写测试用例

测试场景1:默认1帧延迟下回调正常触发

test('createRAF 延迟1帧时回调正常触发', () => {
  const { createRAF } = require('./你的模块路径')
  const mockCallback = jest.fn()

  createRAF(mockCallback)
  // 还没执行帧,回调不触发
  expect(mockCallback).not.toHaveBeenCalled()

  // 执行1帧
  runRAFFrames(1)
  // 回调触发1次
  expect(mockCallback).toHaveBeenCalledTimes(1)
})

测试场景2:自定义延迟帧数下回调触发时机正确

test('createRAF 自定义延迟3帧时回调在第3帧后触发', () => {
  const { createRAF } = require('./你的模块路径')
  const mockCallback = jest.fn()

  createRAF(mockCallback, 3)
  // 执行2帧
  runRAFFrames(2)
  expect(mockCallback).not.toHaveBeenCalled()

  // 执行第3帧
  runRAFFrames(1)
  expect(mockCallback).toHaveBeenCalledTimes(1)
})

测试场景3:cancelRAF 可以正常取消未执行的回调

test('cancelRAF 取消后回调不会触发', () => {
  const { createRAF, cancelRAF } = require('./你的模块路径')
  const mockCallback = jest.fn()

  const rafId = createRAF(mockCallback, 2)
  cancelRAF(rafId)

  // 执行2帧
  runRAFFrames(2)
  expect(mockCallback).not.toHaveBeenCalled()
  expect(global.cancelAnimationFrame).toHaveBeenCalled()
})

测试场景4:多个RAF实例并行执行互不干扰

test('多个createRAF实例并行时互不影响', () => {
  const { createRAF } = require('./你的模块路径')
  const cb1 = jest.fn()
  const cb2 = jest.fn()

  createRAF(cb1, 1)
  createRAF(cb2, 3)

  runRAFFrames(1)
  expect(cb1).toHaveBeenCalledTimes(1)
  expect(cb2).not.toHaveBeenCalled()

  runRAFFrames(2)
  expect(cb2).toHaveBeenCalledTimes(1)
})

额外注意点

如果你的测试环境是jsdom,默认已经有requestAnimationFrame的实现,但默认是用微队列模拟的,无法精确控制帧数,所以必须手动Mock覆盖原生的实现才能达到逐帧验证的效果。

内容的提问来源于stack exchange,提问作者GuangWu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.30 12:06:03