在Vitest中触发visibilitychange事件时像素未触发的问题排查
问题:visibilitychange事件在Vitest测试中无法触发预期行为
我写的代码监听了visibilitychange事件,在浏览器环境下运行完全符合预期,但在Vitest单元测试中无法复现该行为。尝试过用延迟为0的setTimeout断言,也试过使用@testing-library/dom的fireEvent,都没有效果。
实现代码(implementation.ts)
const visibilitychangeCreativeViewHandler = () => { if (document.visibilityState === 'visible') { logger.info('Firing visibilitychange event handler'); fireCreativeView(); } }; // 实际触发创意视图像素的函数 - 仅执行一次。如果文档在触发时可见,之后切换到隐藏再切回可见,不会重复触发creativeView像素 const fireCreativeView = once(() => { for (const companion of companions) { if (companion.events.creativeView) { for (const viewEvent of companion.events.creativeView) { const { uri } = viewEvent; logger.info(`Firing creativeView pixel`); const pixel = document.createElement('img'); pixel.src = uri; pixel.height = 1; pixel.width = 1; pixel.addEventListener('load', () => pixel.remove()); document.body.append(pixel); } } } }); // 后续代码... // 如果文档可见,触发'creativeView'事件 if (document.visibilityState === 'visible') { fireCreativeView(); } // 添加事件监听器,在广告时段浏览器变为可见时触发creativeView事件 document.addEventListener( 'visibilitychange', visibilitychangeCreativeViewHandler, );
测试代码(test.ts)
it('fires the pixel after visibilitychange', () => { const bodyAppendSpy = vi.spyOn(document.body, 'append'); // 监听器处理函数中会调用该方法 Object.defineProperty(document, 'visibilityState', { value: 'hidden', writable: true, }); Object.defineProperty(document, 'hidden', { value: true, writable: true, }); // 一些关于文档初始为隐藏状态的断言... Object.defineProperty(document, 'visibilityState', { value: 'visible', writable: true, }); Object.defineProperty(document, 'hidden', { value: false, writable: true, }); document.dispatchEvent(new Event('visibilitychange')); expect(bodyAppendSpy).toHaveBeenCalled(); // 该断言失败 });
解决方法
1. 重置once函数的单次执行限制
fireCreativeView被once包裹,只会执行一次。如果测试用例之间没有重置状态,或者初始化时意外触发过,都会导致当前测试中函数不执行。可以在测试中mock掉once,让它直接返回原函数:
// 在测试顶部mock once函数(假设once是自定义或第三方库导出的) vi.mock('../path/to/your/once-module', () => ({ once: (fn: Function) => fn // 取消单次执行限制 })); // 然后重新导入实现代码,确保mock生效 const { fireCreativeView } = await import('../implementation');
2. 确保事件监听器已正确注册
测试时要确认document.addEventListener已经被调用,监听器已挂载。可以添加断言验证:
const addEventListenerSpy = vi.spyOn(document, 'addEventListener'); // 执行初始化代码(比如调用注册监听器的函数) initYourAdModule(); expect(addEventListenerSpy).toHaveBeenCalledWith('visibilitychange', expect.any(Function));
3. 更可靠地模拟document属性
使用Vitest的vi.stubGlobal代替Object.defineProperty,模拟更稳定:
// 初始设置为hidden vi.stubGlobal('document', { ...document, visibilityState: 'hidden', hidden: true, body: { append: bodyAppendSpy } }); // 切换为visible vi.stubGlobal('document', { ...document, visibilityState: 'visible', hidden: false, body: { append: bodyAppendSpy } });
4. 直接调用事件处理函数测试
如果事件触发有问题,可以跳过事件触发,直接调用visibilitychangeCreativeViewHandler来验证逻辑:
// 在设置visibilityState为visible后 visibilitychangeCreativeViewHandler(); expect(bodyAppendSpy).toHaveBeenCalled();
内容的提问来源于stack exchange,提问作者daniel0mullins
相关产品推荐
相关产品推荐

