@mswjs/interceptors 0.14.1 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +68 -49
  3. package/lib/BatchInterceptor.d.ts +18 -0
  4. package/lib/BatchInterceptor.js +79 -0
  5. package/lib/BatchInterceptor.js.map +1 -0
  6. package/lib/Interceptor.d.ts +49 -0
  7. package/lib/Interceptor.js +197 -0
  8. package/lib/Interceptor.js.map +1 -0
  9. package/lib/RemoteInterceptor.d.ts +24 -0
  10. package/lib/RemoteInterceptor.js +216 -0
  11. package/lib/RemoteInterceptor.js.map +1 -0
  12. package/lib/glossary.d.ts +32 -0
  13. package/lib/glossary.js +3 -0
  14. package/lib/glossary.js.map +1 -0
  15. package/lib/index.d.ts +2 -2
  16. package/lib/index.js +2 -2
  17. package/lib/index.js.map +1 -1
  18. package/lib/interceptors/ClientRequest/NodeClientRequest.d.ts +5 -5
  19. package/lib/interceptors/ClientRequest/NodeClientRequest.js +65 -16
  20. package/lib/interceptors/ClientRequest/NodeClientRequest.js.map +1 -1
  21. package/lib/interceptors/ClientRequest/http.get.d.ts +2 -3
  22. package/lib/interceptors/ClientRequest/http.get.js +2 -5
  23. package/lib/interceptors/ClientRequest/http.get.js.map +1 -1
  24. package/lib/interceptors/ClientRequest/http.request.d.ts +2 -3
  25. package/lib/interceptors/ClientRequest/http.request.js +3 -6
  26. package/lib/interceptors/ClientRequest/http.request.js.map +1 -1
  27. package/lib/interceptors/ClientRequest/index.d.ts +14 -4
  28. package/lib/interceptors/ClientRequest/index.js +59 -46
  29. package/lib/interceptors/ClientRequest/index.js.map +1 -1
  30. package/lib/interceptors/ClientRequest/utils/getIncomingMessageBody.js +7 -2
  31. package/lib/interceptors/ClientRequest/utils/getIncomingMessageBody.js.map +1 -1
  32. package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.d.ts +11 -4
  33. package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js +110 -58
  34. package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js.map +1 -1
  35. package/lib/interceptors/XMLHttpRequest/index.d.ts +11 -5
  36. package/lib/interceptors/XMLHttpRequest/index.js +43 -25
  37. package/lib/interceptors/XMLHttpRequest/index.js.map +1 -1
  38. package/lib/interceptors/fetch/index.d.ts +8 -2
  39. package/lib/interceptors/fetch/index.js +120 -68
  40. package/lib/interceptors/fetch/index.js.map +1 -1
  41. package/lib/presets/browser.d.ts +3 -1
  42. package/lib/presets/browser.js +2 -2
  43. package/lib/presets/browser.js.map +1 -1
  44. package/lib/presets/node.d.ts +3 -1
  45. package/lib/presets/node.js +1 -1
  46. package/lib/presets/node.js.map +1 -1
  47. package/lib/utils/AsyncEventEmitter.d.ts +29 -0
  48. package/lib/utils/AsyncEventEmitter.js +241 -0
  49. package/lib/utils/AsyncEventEmitter.js.map +1 -0
  50. package/lib/utils/createLazyCallback.d.ts +11 -0
  51. package/lib/utils/createLazyCallback.js +75 -0
  52. package/lib/utils/createLazyCallback.js.map +1 -0
  53. package/lib/utils/nextTick.d.ts +2 -0
  54. package/lib/utils/nextTick.js +16 -0
  55. package/lib/utils/nextTick.js.map +1 -0
  56. package/lib/utils/toIsoResponse.d.ts +1 -1
  57. package/package.json +7 -7
  58. package/src/BatchInterceptor.test.ts +113 -0
  59. package/src/BatchInterceptor.ts +60 -0
  60. package/src/Interceptor.test.ts +166 -0
  61. package/src/Interceptor.ts +226 -0
  62. package/src/RemoteInterceptor.ts +176 -0
  63. package/src/glossary.ts +42 -0
  64. package/src/index.ts +2 -2
  65. package/src/interceptors/ClientRequest/NodeClientRequest.test.ts +87 -70
  66. package/src/interceptors/ClientRequest/NodeClientRequest.ts +150 -99
  67. package/src/interceptors/ClientRequest/http.get.ts +7 -11
  68. package/src/interceptors/ClientRequest/http.request.ts +8 -12
  69. package/src/interceptors/ClientRequest/index.test.ts +43 -0
  70. package/src/interceptors/ClientRequest/index.ts +46 -46
  71. package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.test.ts +9 -0
  72. package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.ts +9 -2
  73. package/src/interceptors/XMLHttpRequest/XMLHttpRequestOverride.ts +215 -159
  74. package/src/interceptors/XMLHttpRequest/index.ts +41 -23
  75. package/src/interceptors/fetch/index.ts +81 -55
  76. package/src/presets/browser.ts +3 -3
  77. package/src/presets/node.ts +3 -3
  78. package/src/utils/AsyncEventEmitter.test.ts +68 -0
  79. package/src/utils/AsyncEventEmitter.ts +171 -0
  80. package/src/utils/createLazyCallback.ts +49 -0
  81. package/src/utils/nextTick.ts +11 -0
  82. package/src/utils/toIsoResponse.ts +1 -1
  83. package/lib/createInterceptor.d.ts +0 -54
  84. package/lib/createInterceptor.js +0 -27
  85. package/lib/createInterceptor.js.map +0 -1
  86. package/lib/remote.d.ts +0 -21
  87. package/lib/remote.js +0 -178
  88. package/lib/remote.js.map +0 -1
  89. package/src/createInterceptor.ts +0 -100
  90. package/src/remote.ts +0 -174
@@ -2,6 +2,7 @@
2
2
  * XMLHttpRequest override class.
3
3
  * Inspired by https://github.com/marvinhagemeister/xhr-mocklet.
4
4
  */
5
+ import type { Debugger } from 'debug'
5
6
  import { until } from '@open-draft/until'
6
7
  import {
7
8
  Headers,
@@ -10,14 +11,14 @@ import {
10
11
  headersToString,
11
12
  } from 'headers-polyfill'
12
13
  import { DOMParser } from '@xmldom/xmldom'
13
- import { IsomorphicRequest, Observer, Resolver } from '../../createInterceptor'
14
+ import { InteractiveIsomorphicRequest, IsomorphicRequest } from '../../glossary'
14
15
  import { parseJson } from '../../utils/parseJson'
15
16
  import { toIsoResponse } from '../../utils/toIsoResponse'
16
17
  import { uuidv4 } from '../../utils/uuid'
17
18
  import { bufferFrom } from './utils/bufferFrom'
18
19
  import { createEvent } from './utils/createEvent'
19
-
20
- const createDebug = require('debug')
20
+ import type { XMLHttpRequestEmitter } from '.'
21
+ import { createLazyCallback } from '../../utils/createLazyCallback'
21
22
 
22
23
  type XMLHttpRequestEventHandler = (
23
24
  this: XMLHttpRequest,
@@ -30,9 +31,9 @@ interface XMLHttpRequestEvent<EventMap extends any> {
30
31
  }
31
32
 
32
33
  interface CreateXMLHttpRequestOverrideOptions {
33
- pureXMLHttpRequest: typeof window.XMLHttpRequest
34
- observer: Observer
35
- resolver: Resolver
34
+ XMLHttpRequest: typeof window.XMLHttpRequest
35
+ emitter: XMLHttpRequestEmitter
36
+ log: Debugger
36
37
  }
37
38
 
38
39
  interface InternalXMLHttpRequestEventTargetEventMap
@@ -40,11 +41,16 @@ interface InternalXMLHttpRequestEventTargetEventMap
40
41
  readystatechange: Event
41
42
  }
42
43
 
44
+ export type ExtractCallbacks<Key extends string> = Key extends
45
+ | 'abort'
46
+ | `on${infer _CallbackName}`
47
+ ? Key
48
+ : never
49
+
43
50
  export const createXMLHttpRequestOverride = (
44
51
  options: CreateXMLHttpRequestOverrideOptions
45
52
  ) => {
46
- const { pureXMLHttpRequest, observer, resolver } = options
47
- let debug = createDebug('XHR')
53
+ const { XMLHttpRequest, emitter, log } = options
48
54
 
49
55
  return class XMLHttpRequestOverride implements XMLHttpRequest {
50
56
  _requestHeaders: Headers
@@ -54,6 +60,8 @@ export const createXMLHttpRequestOverride = (
54
60
  _events: XMLHttpRequestEvent<InternalXMLHttpRequestEventTargetEventMap>[] =
55
61
  []
56
62
 
63
+ log: Debugger = log
64
+
57
65
  /* Request state */
58
66
  public static readonly UNSENT = 0
59
67
  public static readonly OPENED = 1
@@ -142,11 +150,11 @@ export const createXMLHttpRequestOverride = (
142
150
  return
143
151
  }
144
152
 
145
- debug('readyState change %d -> %d', this.readyState, nextState)
153
+ this.log('readyState change %d -> %d', this.readyState, nextState)
146
154
  this.readyState = nextState
147
155
 
148
156
  if (nextState !== this.UNSENT) {
149
- debug('triggerring readystate change...')
157
+ this.log('triggerring readystate change...')
150
158
  this.trigger('readystatechange')
151
159
  }
152
160
  }
@@ -160,8 +168,8 @@ export const createXMLHttpRequestOverride = (
160
168
  readystatechange: ProgressEvent<XMLHttpRequestEventTarget>
161
169
  })
162
170
  >(eventName: K, options?: ProgressEventInit) {
163
- debug('trigger "%s" (%d)', eventName, this.readyState)
164
- debug('resolve listener for event "%s"', eventName)
171
+ this.log('trigger "%s" (%d)', eventName, this.readyState)
172
+ this.log('resolve listener for event "%s"', eventName)
165
173
 
166
174
  // @ts-expect-error XMLHttpRequest class has no index signature.
167
175
  const callback = this[`on${eventName}`] as XMLHttpRequestEventHandler
@@ -169,7 +177,7 @@ export const createXMLHttpRequestOverride = (
169
177
 
170
178
  for (const event of this._events) {
171
179
  if (event.name === eventName) {
172
- debug(
180
+ log(
173
181
  'calling mock event listener "%s" (%d)',
174
182
  eventName,
175
183
  this.readyState
@@ -182,7 +190,7 @@ export const createXMLHttpRequestOverride = (
182
190
  }
183
191
 
184
192
  reset() {
185
- debug('reset')
193
+ this.log('reset')
186
194
 
187
195
  this.setReadyState(this.UNSENT)
188
196
  this.status = 200
@@ -203,8 +211,8 @@ export const createXMLHttpRequestOverride = (
203
211
  user?: string,
204
212
  password?: string
205
213
  ) {
206
- debug = createDebug(`XHR ${method} ${url}`)
207
- debug('open', { method, url, async, user, password })
214
+ this.log = this.log.extend(`request ${method} ${url}`)
215
+ this.log('open', { method, url, async, user, password })
208
216
 
209
217
  this.reset()
210
218
  this.setReadyState(this.OPENED)
@@ -222,7 +230,7 @@ export const createXMLHttpRequestOverride = (
222
230
  }
223
231
 
224
232
  public send(data?: string) {
225
- debug('send %s %s', this.method, this.url)
233
+ this.log('send %s %s', this.method, this.url)
226
234
 
227
235
  this.data = data || ''
228
236
 
@@ -237,10 +245,10 @@ export const createXMLHttpRequestOverride = (
237
245
  url = new URL(this.url, window.location.href)
238
246
  }
239
247
 
240
- debug('request headers', this._requestHeaders)
248
+ this.log('request headers', this._requestHeaders)
241
249
 
242
250
  // Create an intercepted request instance exposed to the request intercepting middleware.
243
- const isoRequest: IsomorphicRequest = {
251
+ const isomorphicRequest: IsomorphicRequest = {
244
252
  id: uuidv4(),
245
253
  url,
246
254
  method: this.method,
@@ -249,160 +257,182 @@ export const createXMLHttpRequestOverride = (
249
257
  body: this.data,
250
258
  }
251
259
 
252
- observer.emit('request', isoRequest)
260
+ const interactiveIsomorphicRequest: InteractiveIsomorphicRequest = {
261
+ ...isomorphicRequest,
262
+ respondWith: createLazyCallback(),
263
+ }
253
264
 
254
- debug('awaiting mocked response...')
265
+ this.log(
266
+ 'emitting the "request" event for %d listener(s)...',
267
+ emitter.listenerCount('request')
268
+ )
269
+ emitter.emit('request', interactiveIsomorphicRequest)
270
+
271
+ this.log('awaiting mocked response...')
272
+
273
+ Promise.resolve(
274
+ until(async () => {
275
+ await emitter.untilIdle('request')
276
+ this.log('all request listeners have been resolved!')
277
+
278
+ const [mockedResponse] =
279
+ await interactiveIsomorphicRequest.respondWith.invoked()
280
+ this.log('event.respondWith called with:', mockedResponse)
281
+
282
+ return mockedResponse
283
+ })
284
+ ).then(([middlewareException, mockedResponse]) => {
285
+ // When the request middleware throws an exception, error the request.
286
+ // This cancels the request and is similar to a network error.
287
+ if (middlewareException) {
288
+ this.log(
289
+ 'middleware function threw an exception!',
290
+ middlewareException
291
+ )
255
292
 
256
- Promise.resolve(until(async () => resolver(isoRequest, this))).then(
257
- ([middlewareException, mockedResponse]) => {
258
- // When the request middleware throws an exception, error the request.
259
- // This cancels the request and is similar to a network error.
260
- if (middlewareException) {
261
- debug(
262
- 'middleware function threw an exception!',
263
- middlewareException
264
- )
293
+ // No way to propagate the actual error message.
294
+ this.trigger('error')
295
+ this.abort()
265
296
 
266
- // No way to propagate the actual error message.
267
- this.trigger('error')
268
- this.abort()
297
+ return
298
+ }
269
299
 
270
- return
271
- }
300
+ // Return a mocked response, if provided in the middleware.
301
+ if (mockedResponse) {
302
+ this.log('received mocked response', mockedResponse)
303
+
304
+ // Trigger a loadstart event to indicate the initialization of the fetch.
305
+ this.trigger('loadstart')
306
+
307
+ this.status = mockedResponse.status || 200
308
+ this.statusText = mockedResponse.statusText || 'OK'
309
+ this._responseHeaders = mockedResponse.headers
310
+ ? objectToHeaders(mockedResponse.headers)
311
+ : new Headers()
272
312
 
273
- // Return a mocked response, if provided in the middleware.
274
- if (mockedResponse) {
275
- debug('received mocked response', mockedResponse)
313
+ this.log('set response status', this.status, this.statusText)
314
+ this.log('set response headers', this._responseHeaders)
276
315
 
277
- // Trigger a loadstart event to indicate the initialization of the fetch.
278
- this.trigger('loadstart')
316
+ // Mark that response headers has been received
317
+ // and trigger a ready state event to reflect received headers
318
+ // in a custom `onreadystatechange` callback.
319
+ this.setReadyState(this.HEADERS_RECEIVED)
279
320
 
280
- this.status = mockedResponse.status || 200
281
- this.statusText = mockedResponse.statusText || 'OK'
282
- this._responseHeaders = mockedResponse.headers
283
- ? objectToHeaders(mockedResponse.headers)
284
- : new Headers()
321
+ this.log('response type', this.responseType)
322
+ this.response = this.getResponseBody(mockedResponse.body)
323
+ this.responseText = mockedResponse.body || ''
324
+ this.responseXML = this.getResponseXML()
285
325
 
286
- debug('set response status', this.status, this.statusText)
287
- debug('set response headers', this._responseHeaders)
326
+ this.log('set response body', this.response)
288
327
 
289
- // Mark that response headers has been received
290
- // and trigger a ready state event to reflect received headers
291
- // in a custom `onreadystatechange` callback.
292
- this.setReadyState(this.HEADERS_RECEIVED)
328
+ if (mockedResponse.body && this.response) {
329
+ this.setReadyState(this.LOADING)
293
330
 
294
- debug('response type', this.responseType)
295
- this.response = this.getResponseBody(mockedResponse.body)
296
- this.responseText = mockedResponse.body || ''
297
- this.responseXML = this.getResponseXML()
331
+ // Presense of the mocked response implies a response body (not null).
332
+ // Presense of the coerced `this.response` implies the mocked body is valid.
333
+ const bodyBuffer = bufferFrom(mockedResponse.body)
334
+
335
+ // Trigger a progress event based on the mocked response body.
336
+ this.trigger('progress', {
337
+ loaded: bodyBuffer.length,
338
+ total: bodyBuffer.length,
339
+ })
340
+ }
298
341
 
299
- debug('set response body', this.response)
342
+ /**
343
+ * Explicitly mark the request as done so its response never hangs.
344
+ * @see https://github.com/mswjs/interceptors/issues/13
345
+ */
346
+ this.setReadyState(this.DONE)
347
+
348
+ // Trigger a load event to indicate the fetch has succeeded.
349
+ this.trigger('load')
350
+ // Trigger a loadend event to indicate the fetch has completed.
351
+ this.trigger('loadend')
352
+
353
+ emitter.emit(
354
+ 'response',
355
+ isomorphicRequest,
356
+ toIsoResponse(mockedResponse)
357
+ )
358
+ } else {
359
+ this.log('no mocked response received!')
360
+
361
+ // Perform an original request, when the request middleware returned no mocked response.
362
+ const originalRequest = new XMLHttpRequest()
363
+
364
+ this.log('opening an original request %s %s', this.method, this.url)
365
+ originalRequest.open(
366
+ this.method,
367
+ this.url,
368
+ this.async ?? true,
369
+ this.user,
370
+ this.password
371
+ )
300
372
 
301
- if (mockedResponse.body && this.response) {
302
- this.setReadyState(this.LOADING)
373
+ // Reflect a successful state of the original request
374
+ // on the patched instance.
375
+ originalRequest.addEventListener('load', () => {
376
+ this.log('original "onload"')
303
377
 
304
- // Presense of the mocked response implies a response body (not null).
305
- // Presense of the coerced `this.response` implies the mocked body is valid.
306
- const bodyBuffer = bufferFrom(mockedResponse.body)
378
+ this.status = originalRequest.status
379
+ this.statusText = originalRequest.statusText
380
+ this.responseURL = originalRequest.responseURL
381
+ this.responseType = originalRequest.responseType
382
+ this.response = originalRequest.response
383
+ this.responseText = originalRequest.responseText
384
+ this.responseXML = originalRequest.responseXML
307
385
 
308
- // Trigger a progress event based on the mocked response body.
309
- this.trigger('progress', {
310
- loaded: bodyBuffer.length,
311
- total: bodyBuffer.length,
312
- })
313
- }
386
+ this.log('set mock request readyState to DONE')
314
387
 
388
+ // Explicitly mark the mocked request instance as done
389
+ // so the response never hangs.
315
390
  /**
316
- * Explicitly mark the request as done so its response never hangs.
317
- * @see https://github.com/mswjs/interceptors/issues/13
391
+ * @note `readystatechange` listener is called TWICE
392
+ * in the case of unhandled request.
318
393
  */
319
394
  this.setReadyState(this.DONE)
320
395
 
321
- // Trigger a load event to indicate the fetch has succeeded.
322
- this.trigger('load')
323
- // Trigger a loadend event to indicate the fetch has completed.
324
- this.trigger('loadend')
325
-
326
- observer.emit('response', isoRequest, toIsoResponse(mockedResponse))
327
- } else {
328
- debug('no mocked response received!')
329
-
330
- // Perform an original request, when the request middleware returned no mocked response.
331
- const originalRequest = new pureXMLHttpRequest()
332
-
333
- debug('opening an original request %s %s', this.method, this.url)
334
- originalRequest.open(
335
- this.method,
336
- this.url,
337
- this.async ?? true,
338
- this.user,
339
- this.password
396
+ this.log('received original response', this.status, this.statusText)
397
+ this.log('original response body:', this.response)
398
+
399
+ const responseHeaders = originalRequest.getAllResponseHeaders()
400
+ this.log('original response headers:\n', responseHeaders)
401
+
402
+ this._responseHeaders = stringToHeaders(responseHeaders)
403
+ this.log(
404
+ 'original response headers (normalized)',
405
+ this._responseHeaders
340
406
  )
341
407
 
342
- // Reflect a successful state of the original request
343
- // on the patched instance.
344
- originalRequest.addEventListener('load', () => {
345
- debug('original "onload"')
346
-
347
- this.status = originalRequest.status
348
- this.statusText = originalRequest.statusText
349
- this.responseURL = originalRequest.responseURL
350
- this.responseType = originalRequest.responseType
351
- this.response = originalRequest.response
352
- this.responseText = originalRequest.responseText
353
- this.responseXML = originalRequest.responseXML
354
-
355
- debug('set mock request readyState to DONE')
356
-
357
- // Explicitly mark the mocked request instance as done
358
- // so the response never hangs.
359
- /**
360
- * @note `readystatechange` listener is called TWICE
361
- * in the case of unhandled request.
362
- */
363
- this.setReadyState(this.DONE)
364
-
365
- debug('received original response', this.status, this.statusText)
366
- debug('original response body:', this.response)
367
-
368
- const responseHeaders = originalRequest.getAllResponseHeaders()
369
- debug('original response headers:\n', responseHeaders)
370
-
371
- this._responseHeaders = stringToHeaders(responseHeaders)
372
- debug(
373
- 'original response headers (normalized)',
374
- this._responseHeaders
375
- )
376
-
377
- debug('original response finished')
378
-
379
- observer.emit('response', isoRequest, {
380
- status: originalRequest.status,
381
- statusText: originalRequest.statusText,
382
- headers: this._responseHeaders,
383
- body: originalRequest.response,
384
- })
385
- })
408
+ this.log('original response finished')
386
409
 
387
- // Assign callbacks and event listeners from the intercepted XHR instance
388
- // to the original XHR instance.
389
- this.propagateCallbacks(originalRequest)
390
- this.propagateListeners(originalRequest)
391
- this.propagateHeaders(originalRequest, this._requestHeaders)
410
+ emitter.emit('response', isomorphicRequest, {
411
+ status: originalRequest.status,
412
+ statusText: originalRequest.statusText,
413
+ headers: this._responseHeaders,
414
+ body: originalRequest.response,
415
+ })
416
+ })
392
417
 
393
- if (this.async) {
394
- originalRequest.timeout = this.timeout
395
- }
418
+ // Assign callbacks and event listeners from the intercepted XHR instance
419
+ // to the original XHR instance.
420
+ this.propagateCallbacks(originalRequest)
421
+ this.propagateListeners(originalRequest)
422
+ this.propagateHeaders(originalRequest, this._requestHeaders)
396
423
 
397
- debug('send', this.data)
398
- originalRequest.send(this.data)
424
+ if (this.async) {
425
+ originalRequest.timeout = this.timeout
399
426
  }
427
+
428
+ this.log('send', this.data)
429
+ originalRequest.send(this.data)
400
430
  }
401
- )
431
+ })
402
432
  }
403
433
 
404
434
  public abort() {
405
- debug('abort')
435
+ this.log('abort')
406
436
 
407
437
  if (this.readyState > this.UNSENT && this.readyState < this.DONE) {
408
438
  this.setReadyState(this.UNSENT)
@@ -415,15 +445,15 @@ export const createXMLHttpRequestOverride = (
415
445
  }
416
446
 
417
447
  public setRequestHeader(name: string, value: string) {
418
- debug('set request header "%s" to "%s"', name, value)
448
+ this.log('set request header "%s" to "%s"', name, value)
419
449
  this._requestHeaders.append(name, value)
420
450
  }
421
451
 
422
452
  public getResponseHeader(name: string): string | null {
423
- debug('get response header "%s"', name)
453
+ this.log('get response header "%s"', name)
424
454
 
425
455
  if (this.readyState < this.HEADERS_RECEIVED) {
426
- debug(
456
+ this.log(
427
457
  'cannot return a header: headers not received (state: %s)',
428
458
  this.readyState
429
459
  )
@@ -432,7 +462,7 @@ export const createXMLHttpRequestOverride = (
432
462
 
433
463
  const headerValue = this._responseHeaders.get(name)
434
464
 
435
- debug(
465
+ this.log(
436
466
  'resolved response header "%s" to "%s"',
437
467
  name,
438
468
  headerValue,
@@ -443,10 +473,10 @@ export const createXMLHttpRequestOverride = (
443
473
  }
444
474
 
445
475
  public getAllResponseHeaders(): string {
446
- debug('get all response headers')
476
+ this.log('get all response headers')
447
477
 
448
478
  if (this.readyState < this.HEADERS_RECEIVED) {
449
- debug(
479
+ this.log(
450
480
  'cannot return headers: headers not received (state: %s)',
451
481
  this.readyState
452
482
  )
@@ -459,7 +489,7 @@ export const createXMLHttpRequestOverride = (
459
489
  public addEventListener<
460
490
  K extends keyof InternalXMLHttpRequestEventTargetEventMap
461
491
  >(name: K, listener: XMLHttpRequestEventHandler) {
462
- debug('addEventListener', name, listener)
492
+ this.log('addEventListener', name, listener)
463
493
  this._events.push({
464
494
  name,
465
495
  listener,
@@ -470,7 +500,7 @@ export const createXMLHttpRequestOverride = (
470
500
  name: K,
471
501
  listener: (event?: XMLHttpRequestEventMap[K]) => void
472
502
  ): void {
473
- debug('removeEventListener', name, listener)
503
+ this.log('removeEventListener', name, listener)
474
504
  this._events = this._events.filter((storedEvent) => {
475
505
  return storedEvent.name !== name && storedEvent.listener !== listener
476
506
  })
@@ -484,18 +514,18 @@ export const createXMLHttpRequestOverride = (
484
514
  getResponseBody(body: string | undefined) {
485
515
  // Handle an improperly set "null" value of the mocked response body.
486
516
  const textBody = body ?? ''
487
- debug('coerced response body to', textBody)
517
+ this.log('coerced response body to', textBody)
488
518
 
489
519
  switch (this.responseType) {
490
520
  case 'json': {
491
- debug('resolving response body as JSON')
521
+ this.log('resolving response body as JSON')
492
522
  return parseJson(textBody)
493
523
  }
494
524
 
495
525
  case 'blob': {
496
526
  const blobType =
497
527
  this.getResponseHeader('content-type') || 'text/plain'
498
- debug('resolving response body as Blob', { type: blobType })
528
+ this.log('resolving response body as Blob', { type: blobType })
499
529
 
500
530
  return new Blob([textBody], {
501
531
  type: blobType,
@@ -503,7 +533,7 @@ export const createXMLHttpRequestOverride = (
503
533
  }
504
534
 
505
535
  case 'arraybuffer': {
506
- debug('resolving response body as ArrayBuffer')
536
+ this.log('resolving response body as ArrayBuffer')
507
537
  const arrayBuffer = bufferFrom(textBody)
508
538
  return arrayBuffer
509
539
  }
@@ -526,6 +556,28 @@ export const createXMLHttpRequestOverride = (
526
556
  * to the given XMLHttpRequest instance.
527
557
  */
528
558
  propagateCallbacks(request: XMLHttpRequest) {
559
+ this.log('propagating request callbacks to the original request')
560
+ const callbackNames: Array<ExtractCallbacks<keyof XMLHttpRequest>> = [
561
+ 'abort',
562
+ 'onerror',
563
+ 'ontimeout',
564
+ 'onload',
565
+ 'onloadstart',
566
+ 'onloadend',
567
+ 'onprogress',
568
+ 'onreadystatechange',
569
+ ]
570
+
571
+ for (const callbackName of callbackNames) {
572
+ const callback = this[callbackName]
573
+
574
+ if (callback) {
575
+ request[callbackName] = this[callbackName] as any
576
+
577
+ this.log('propagated the "%s" callback', callbackName, callback)
578
+ }
579
+ }
580
+
529
581
  request.onabort = this.abort
530
582
  request.onerror = this.onerror
531
583
  request.ontimeout = this.ontimeout
@@ -541,7 +593,7 @@ export const createXMLHttpRequestOverride = (
541
593
  * to the given XMLHttpRequest instance.
542
594
  */
543
595
  propagateListeners(request: XMLHttpRequest) {
544
- debug(
596
+ this.log(
545
597
  'propagating request listeners (%d) to the original request',
546
598
  this._events.length,
547
599
  this._events
@@ -553,11 +605,15 @@ export const createXMLHttpRequestOverride = (
553
605
  }
554
606
 
555
607
  propagateHeaders(request: XMLHttpRequest, headers: Headers) {
556
- debug('propagating request headers to the original request', headers)
608
+ this.log('propagating request headers to the original request', headers)
557
609
 
558
610
  // Preserve the request headers casing.
559
611
  Object.entries(headers.raw()).forEach(([name, value]) => {
560
- debug('setting "%s" (%s) header on the original request', name, value)
612
+ this.log(
613
+ 'setting "%s" (%s) header on the original request',
614
+ name,
615
+ value
616
+ )
561
617
  request.setRequestHeader(name, value)
562
618
  })
563
619
  }
@@ -1,34 +1,52 @@
1
- import { Interceptor } from '../../createInterceptor'
1
+ import type {
2
+ HttpRequestEventMap,
3
+ InteractiveIsomorphicRequest,
4
+ } from '../../glossary'
5
+ import { Interceptor } from '../../Interceptor'
6
+ import { AsyncEventEmitter } from '../../utils/AsyncEventEmitter'
2
7
  import { createXMLHttpRequestOverride } from './XMLHttpRequestOverride'
3
8
 
4
- const debug = require('debug')('XHR')
9
+ export type XMLHttpRequestEventListener = (
10
+ request: InteractiveIsomorphicRequest
11
+ ) => Promise<void> | void
5
12
 
6
- const pureXMLHttpRequest =
7
- // Although executed in node, certain processes emulate the DOM-like environment
8
- // (i.e. `js-dom` in Jest). The `window` object would be avilable in such environments.
9
- typeof window === 'undefined' ? undefined : window.XMLHttpRequest
13
+ export type XMLHttpRequestEmitter = AsyncEventEmitter<HttpRequestEventMap>
10
14
 
11
- /**
12
- * Intercepts requests issued via `XMLHttpRequest`.
13
- */
14
- export const interceptXMLHttpRequest: Interceptor = (observer, resolver) => {
15
- if (pureXMLHttpRequest) {
16
- debug('patching "XMLHttpRequest" module...')
15
+ export class XMLHttpRequestInterceptor extends Interceptor<HttpRequestEventMap> {
16
+ static symbol = Symbol('xhr')
17
17
 
18
- const XMLHttpRequestOverride = createXMLHttpRequestOverride({
19
- pureXMLHttpRequest,
20
- observer,
21
- resolver,
22
- })
18
+ constructor() {
19
+ super(XMLHttpRequestInterceptor.symbol)
20
+ }
23
21
 
24
- window.XMLHttpRequest = XMLHttpRequestOverride
22
+ protected checkEnvironment() {
23
+ return (
24
+ typeof window !== 'undefined' &&
25
+ typeof window.XMLHttpRequest !== 'undefined'
26
+ )
25
27
  }
26
28
 
27
- return () => {
28
- if (pureXMLHttpRequest) {
29
- debug('restoring modules...')
29
+ protected setup() {
30
+ const log = this.log.extend('setup')
31
+
32
+ log('patching "XMLHttpRequest" module...')
33
+
34
+ const PureXMLHttpRequest = window.XMLHttpRequest
30
35
 
31
- window.XMLHttpRequest = pureXMLHttpRequest
32
- }
36
+ window.XMLHttpRequest = createXMLHttpRequestOverride({
37
+ XMLHttpRequest: PureXMLHttpRequest,
38
+ emitter: this.emitter,
39
+ log: this.log,
40
+ })
41
+
42
+ log('native "XMLHttpRequest" module patched!', window.XMLHttpRequest.name)
43
+
44
+ this.subscriptions.push(() => {
45
+ window.XMLHttpRequest = PureXMLHttpRequest
46
+ log(
47
+ 'native "XMLHttpRequest" module restored!',
48
+ window.XMLHttpRequest.name
49
+ )
50
+ })
33
51
  }
34
52
  }