如何在非Angular测试中使用zone.js处理事件队列替代硬编码延迟?
问题背景与需求
我们基于原生JS和浏览器API开发UI应用,在测试响应scroll/resize事件的复杂UI组件时,当前采用触发事件后等待硬编码时长(如100ms)的方式,代码示例如下:
it('does something', async () => { const [element, otherElement] = await someSetup(); expect(element).isAlignedWith(otherElement); element.parentElement!.scrollTop -= 100; await delay(100); // 等待滚动事件处理完成 expect(element).isStillAlignedWith(otherElement); });
这种方式存在明显弊端:等待时长过短会导致测试不稳定,过长则拖慢整个测试套件。希望能像Angular的Fake Zones那样,不依赖Angular,直接清空任务队列中的所有事件后再执行断言。
解决方案
可以通过以下几种方式实现无需硬编码等待时长的测试,确保所有事件处理逻辑完成后再执行断言:
1. 递归刷新动画帧与微任务
浏览器的滚动/resize事件处理通常和重排、重绘绑定,requestAnimationFrame会在重绘前执行。通过递归刷新所有动画帧和微任务,能确保所有关联逻辑执行完毕:
function flushAllTasks() { return new Promise(resolve => { // 先执行所有微任务 queueMicrotask(() => { // 递归触发所有requestAnimationFrame回调 function rafLoop() { requestAnimationFrame(rafLoop); } rafLoop(); // 等待最后一帧执行完成后结束 requestAnimationFrame(resolve); }); }); } // 测试中替换硬编码delay it('does something', async () => { const [element, otherElement] = await someSetup(); expect(element).isAlignedWith(otherElement); element.parentElement!.scrollTop -= 100; await flushAllTasks(); expect(element).isStillAlignedWith(otherElement); });
2. 手动触发事件并刷新微任务
如果UI逻辑直接绑定在scroll/resize事件上,可以手动触发事件并同步执行回调,再刷新微任务队列:
it('does something', async () => { const [element, otherElement] = await someSetup(); expect(element).isAlignedWith(otherElement); element.parentElement!.scrollTop -= 100; // 手动触发scroll事件,同步执行绑定的回调 element.parentElement!.dispatchEvent(new Event('scroll', { bubbles: true, cancelable: true })); // 刷新所有微任务 await Promise.resolve(); expect(element).isStillAlignedWith(otherElement); });
注意:这种方式仅适用于同步或微任务级别的事件回调,如果逻辑中包含setTimeout等宏任务,需要结合第一种方法处理。
3. 结合测试框架工具(如Jest)
如果使用Jest,可以模拟requestAnimationFrame并执行所有定时器,避免真实等待:
beforeEach(() => { // 模拟requestAnimationFrame,立即执行回调 jest.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => { cb(Date.now()); return 0; }); }); it('does something', async () => { const [element, otherElement] = await someSetup(); expect(element).isAlignedWith(otherElement); element.parentElement!.scrollTop -= 100; // 执行所有定时器和模拟的动画帧回调 jest.runAllTimers(); // 刷新微任务队列 await Promise.resolve(); expect(element).isStillAlignedWith(otherElement); });
内容的提问来源于stack exchange,提问作者C. V. Moore
相关产品推荐
相关产品推荐

