在Vitest的浏览器模式下,window.addEventListener与 EventTarget.prototype.addEventListener并不相同

前端开发 2026-07-08

在尝试用Vitest的浏览器模式测试我的npm代码库时,我发现了一个我不理解的不一致之处。

我在编写一些函数,通过在 EventTarget.prototype 的定义上将 addEventListenerremoveEventListener 全局包装起来,以实现Chrome的 getEventListeners() DevTools功能所具备的效果。我想用Vitest测试我的包装器的功能。由于代码库的其他部分需要浏览器行为,而这些行为并非上述DOM模拟器所支持,我无法用happy-dom / jsdom进行测试,正如我所发现的。因此我选择Playwright作为Vitest的 浏览器提供者。在测试期间,我发现 window.addEventListener 并没有调用我的包装器代码。

在Firefox、Chrome和 Edge中,
EventTarget.prototype.addEventListener 就是 window.addEventListener
通过打开开发者工具DevTools窗口并输入 EventTarget.prototype.addEventListener === window.addEventListener 即可轻松测试;它会返回 true

然而,在Vitest的浏览器模式中,
EventTarget.prototype.addEventListener !== window.addEventListener
可以通过 设置Vitest的浏览器模式,创建一个测试js文件并添加期望 expect(EventTarget.prototype.addEventListener).toBe(window.addEventListener),这将会失败。

我找不到关于这是为何的清晰文档。

在Vitest中测试 documentdocument.body 以替代 window 的结果,与在浏览器中一样成功:
expect(document.body.addEventListener).toBe(EventTarget.prototype.addEventListener); expect(document.addEventListener).toBe(EventTarget.prototype.addEventListener);

  • 在浏览器中,window.hasOwnProperty('addEventListener') === false。这表明window继承自EventTarget的 addEventListener。
  • 在Vitest中,expect(window.hasOwnProperty('addEventListener')).toBe(false); 失败,这表明window确实拥有自己的addEventListener集。

在浏览器的最新版本中测试:
- Firefox 152.0.2
- Chrome 149.0.7827.196
- Edge 149.0.4022.80

在Node的 Vitest测试中:
- Node v22.14.0
- Vitest 4.1.8
- @vitest/browser-playwright 4.1.9

解决方案

这个差异是由Vitest在浏览器模式下的错误处理机制引起的,而不是Playwright。

在我的Vitest测试文件中,输出 window.addEventListener.toString() 得到了:

function (...args) {
    if (args[0] === errorEvent) {
      userErrorListenerCount++
    }
    return addEventListener.apply(this, args)
  }

我在vitest的源代码中找到了如下位置,位于 https://github.com/vitest-dev/vitest/blob/a60ded0fb1a15a5bc2eb6c26681d9e934d3e17eb/packages/browser/src/client/public/error-catcher.js#L27-L59 :

function catchWindowErrors(errorEvent, prop, cb) {
  let userErrorListenerCount = 0
  function throwUnhandlerError(e) {
    if (userErrorListenerCount === 0 && e[prop] != null) {
      cb(e)
    }
    else {
      // `ErrorEvent` doesn't necessary have `ErrorEvent.error` defined
      // but some has `ErrorEvent.message` defined, e.g. ResizeObserver error.
      // https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent/error
      // https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors
      console.error(e.message ? new Error(e.message) : e)
    }
  }
  const addEventListener = window.addEventListener.bind(window)
  const removeEventListener = window.removeEventListener.bind(window)
  window.addEventListener(errorEvent, throwUnhandlerError)
  window.addEventListener = function (...args) {
    if (args[0] === errorEvent) {
      userErrorListenerCount++
    }
    return addEventListener.apply(this, args)
  }
  window.removeEventListener = function (...args) {
    if (args[0] === errorEvent && userErrorListenerCount) {
      userErrorListenerCount--
    }
    return removeEventListener.apply(this, args)
  }
  return function clearErrorHandlers() {
    window.removeEventListener(errorEvent, throwUnhandlerError)
  }
}

因此Vitest确实对window .addEventListener和 .removeEventListener进行了专门的包装。查看 blame 链接,主要可追溯到2024年 6月 10日和6 月31日的两次提交,这些提交似乎表明这全部是为促进Vitest在浏览器模式下对未处理错误的增强展示而进行的改进。对我来说,这就足以解释这些包装存在的原因。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章