fix(jsdom): support AbortSignal API (#8704) · vitest-dev/vitest@f6690ed
1+import type { DOMWindow } from 'jsdom'
12import type { Environment } from '../../types/environment'
23import type { JSDOMOptions } from '../../types/jsdom-options'
34import { populateGlobal } from './utils'
455-function catchWindowErrors(window: Window) {
6+function catchWindowErrors(window: DOMWindow) {
67let userErrorListenerCount = 0
78function throwUnhandlerError(e: ErrorEvent) {
89if (userErrorListenerCount === 0 && e.error != null) {
@@ -70,7 +71,10 @@ export default <Environment>{
7071 userAgent,
7172 ...restOptions,
7273})
73-const clearWindowErrors = catchWindowErrors(dom.window as any)
74+75+const clearAddEventListenerPatch = patchAddEventListener(dom.window)
76+77+const clearWindowErrors = catchWindowErrors(dom.window)
74787579// TODO: browser doesn't expose Buffer, but a lot of dependencies use it
7680dom.window.Buffer = Buffer
@@ -120,6 +124,7 @@ export default <Environment>{
120124return dom.getInternalVMContext()
121125},
122126teardown() {
127+clearAddEventListenerPatch()
123128clearWindowErrors()
124129dom.window.close()
125130dom = undefined as any
@@ -161,6 +166,8 @@ export default <Environment>{
161166 ...restOptions,
162167})
163168169+const clearAddEventListenerPatch = patchAddEventListener(dom.window)
170+164171const { keys, originals } = populateGlobal(global, dom.window, {
165172bindFunctions: true,
166173})
@@ -171,6 +178,7 @@ export default <Environment>{
171178172179return {
173180teardown(global) {
181+clearAddEventListenerPatch()
174182clearWindowErrors()
175183dom.window.close()
176184delete global.jsdom
@@ -180,3 +188,43 @@ export default <Environment>{
180188}
181189},
182190}
191+192+function patchAddEventListener(window: DOMWindow) {
193+const JSDOMAbortSignal = window.AbortSignal
194+const JSDOMAbortController = window.AbortController
195+const originalAddEventListener = window.EventTarget.prototype.addEventListener
196+197+window.EventTarget.prototype.addEventListener = function addEventListener(
198+type: string,
199+callback: EventListenerOrEventListenerObject | null,
200+options?: AddEventListenerOptions | boolean,
201+) {
202+if (typeof options === 'object' && options.signal != null) {
203+const { signal, ...otherOptions } = options
204+// - this happens because AbortSignal is provided by Node.js,
205+// but jsdom APIs require jsdom's AbortSignal, while Node APIs
206+// (like fetch and Request) require a Node.js AbortSignal
207+// - disable narrow typing with "as any" because we need it later
208+if (!((signal as any) instanceof JSDOMAbortSignal)) {
209+const jsdomCompatOptions = Object.create(null)
210+Object.assign(jsdomCompatOptions, otherOptions)
211+212+// use jsdom-native abort controller instead and forward the
213+// previous one with `addEventListener`
214+const jsdomAbortController = new JSDOMAbortController()
215+signal.addEventListener('abort', () => {
216+jsdomAbortController.abort(signal.reason)
217+})
218+219+jsdomCompatOptions.signal = jsdomAbortController.signal
220+return originalAddEventListener.call(this, type, callback, jsdomCompatOptions)
221+}
222+}
223+224+return originalAddEventListener.call(this, type, callback, options)
225+}
226+227+return () => {
228+window.EventTarget.prototype.addEventListener = originalAddEventListener
229+}
230+}