在Vitest的浏览器模式下,window.addEventListener与 EventTarget.prototype.addEventListener并不相同
在尝试用Vitest的浏览器模式测试我的npm代码库时,我发现了一个我不理解的不一致之处。
我在编写一些函数,通过在 EventTarget.prototype 的定义上将 addEventListener 和 removeEventListener 全局包装起来,以实现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中测试 document 和 document.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在浏览器模式下对未处理错误的增强展示而进行的改进。对我来说,这就足以解释这些包装存在的原因。