fix(jsdom): support AbortSignal API (#8704) · vitest-dev/vitest@f6690ed

1+

import type { DOMWindow } from 'jsdom'

12

import type { Environment } from '../../types/environment'

23

import type { JSDOMOptions } from '../../types/jsdom-options'

34

import { populateGlobal } from './utils'

455-

function catchWindowErrors(window: Window) {

6+

function catchWindowErrors(window: DOMWindow) {

67

let userErrorListenerCount = 0

78

function throwUnhandlerError(e: ErrorEvent) {

89

if (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

7680

dom.window.Buffer = Buffer

@@ -120,6 +124,7 @@ export default <Environment>{

120124

return dom.getInternalVMContext()

121125

},

122126

teardown() {

127+

clearAddEventListenerPatch()

123128

clearWindowErrors()

124129

dom.window.close()

125130

dom = undefined as any

@@ -161,6 +166,8 @@ export default <Environment>{

161166

...restOptions,

162167

})

163168169+

const clearAddEventListenerPatch = patchAddEventListener(dom.window)

170+164171

const { keys, originals } = populateGlobal(global, dom.window, {

165172

bindFunctions: true,

166173

})

@@ -171,6 +178,7 @@ export default <Environment>{

171178172179

return {

173180

teardown(global) {

181+

clearAddEventListenerPatch()

174182

clearWindowErrors()

175183

dom.window.close()

176184

delete 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+

}