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

在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 10:13:14