@mswjs/interceptors 0.17.6 → 0.18.0

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 (114) hide show
  1. package/README.md +49 -47
  2. package/lib/Interceptor.js +4 -4
  3. package/lib/Interceptor.js.map +1 -1
  4. package/lib/RemoteHttpInterceptor.d.ts +15 -0
  5. package/lib/RemoteHttpInterceptor.js +86 -56
  6. package/lib/RemoteHttpInterceptor.js.map +1 -1
  7. package/lib/glossary.d.ts +3 -14
  8. package/lib/glossary.js.map +1 -1
  9. package/lib/index.d.ts +0 -2
  10. package/lib/index.js +0 -2
  11. package/lib/index.js.map +1 -1
  12. package/lib/interceptors/ClientRequest/NodeClientRequest.d.ts +13 -5
  13. package/lib/interceptors/ClientRequest/NodeClientRequest.js +179 -166
  14. package/lib/interceptors/ClientRequest/NodeClientRequest.js.map +1 -1
  15. package/lib/interceptors/ClientRequest/http.get.js +9 -5
  16. package/lib/interceptors/ClientRequest/http.get.js.map +1 -1
  17. package/lib/interceptors/ClientRequest/http.request.js +10 -6
  18. package/lib/interceptors/ClientRequest/http.request.js.map +1 -1
  19. package/lib/interceptors/ClientRequest/index.d.ts +2 -5
  20. package/lib/interceptors/ClientRequest/index.js +2 -13
  21. package/lib/interceptors/ClientRequest/index.js.map +1 -1
  22. package/lib/interceptors/ClientRequest/utils/cloneIncomingMessage.js +9 -5
  23. package/lib/interceptors/ClientRequest/utils/cloneIncomingMessage.js.map +1 -1
  24. package/lib/interceptors/ClientRequest/utils/createRequest.d.ts +6 -0
  25. package/lib/interceptors/ClientRequest/utils/createRequest.js +52 -0
  26. package/lib/interceptors/ClientRequest/utils/createRequest.js.map +1 -0
  27. package/lib/interceptors/ClientRequest/utils/createResponse.d.ts +8 -0
  28. package/lib/interceptors/ClientRequest/utils/createResponse.js +24 -0
  29. package/lib/interceptors/ClientRequest/utils/createResponse.js.map +1 -0
  30. package/lib/interceptors/ClientRequest/utils/getIncomingMessageBody.js +1 -1
  31. package/lib/interceptors/ClientRequest/utils/getIncomingMessageBody.js.map +1 -1
  32. package/lib/interceptors/ClientRequest/utils/normalizeClientRequestArgs.js +8 -8
  33. package/lib/interceptors/ClientRequest/utils/normalizeClientRequestArgs.js.map +1 -1
  34. package/lib/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.d.ts +1 -1
  35. package/lib/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.js +1 -1
  36. package/lib/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.js.map +1 -1
  37. package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.d.ts +6 -10
  38. package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js +203 -143
  39. package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js.map +1 -1
  40. package/lib/interceptors/XMLHttpRequest/index.d.ts +2 -2
  41. package/lib/interceptors/XMLHttpRequest/index.js +2 -2
  42. package/lib/interceptors/XMLHttpRequest/index.js.map +1 -1
  43. package/lib/interceptors/XMLHttpRequest/utils/concatArrayBuffer.d.ts +4 -0
  44. package/lib/interceptors/XMLHttpRequest/utils/concatArrayBuffer.js +14 -0
  45. package/lib/interceptors/XMLHttpRequest/utils/concatArrayBuffer.js.map +1 -0
  46. package/lib/interceptors/XMLHttpRequest/utils/createResponse.d.ts +2 -0
  47. package/lib/interceptors/XMLHttpRequest/utils/createResponse.js +14 -0
  48. package/lib/interceptors/XMLHttpRequest/utils/createResponse.js.map +1 -0
  49. package/lib/interceptors/fetch/index.js +24 -81
  50. package/lib/interceptors/fetch/index.js.map +1 -1
  51. package/lib/utils/AsyncEventEmitter.js +12 -8
  52. package/lib/utils/AsyncEventEmitter.js.map +1 -1
  53. package/lib/utils/RequestWithCredentials.d.ts +7 -0
  54. package/lib/utils/RequestWithCredentials.js +20 -0
  55. package/lib/utils/RequestWithCredentials.js.map +1 -0
  56. package/lib/utils/bufferUtils.d.ts +7 -2
  57. package/lib/utils/bufferUtils.js +10 -6
  58. package/lib/utils/bufferUtils.js.map +1 -1
  59. package/lib/utils/parseJson.d.ts +1 -1
  60. package/lib/utils/toInteractiveRequest.d.ts +7 -0
  61. package/lib/utils/toInteractiveRequest.js +20 -0
  62. package/lib/utils/toInteractiveRequest.js.map +1 -0
  63. package/package.json +3 -2
  64. package/src/RemoteHttpInterceptor.ts +84 -34
  65. package/src/glossary.ts +5 -18
  66. package/src/index.ts +0 -2
  67. package/src/interceptors/ClientRequest/NodeClientRequest.test.ts +17 -23
  68. package/src/interceptors/ClientRequest/NodeClientRequest.ts +177 -153
  69. package/src/interceptors/ClientRequest/index.test.ts +5 -3
  70. package/src/interceptors/ClientRequest/index.ts +2 -26
  71. package/src/interceptors/ClientRequest/utils/createRequest.test.ts +61 -0
  72. package/src/interceptors/ClientRequest/utils/createRequest.ts +32 -0
  73. package/src/interceptors/ClientRequest/utils/createResponse.test.ts +24 -0
  74. package/src/interceptors/ClientRequest/utils/createResponse.ts +22 -0
  75. package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.ts +1 -1
  76. package/src/interceptors/XMLHttpRequest/XMLHttpRequestOverride.ts +234 -174
  77. package/src/interceptors/XMLHttpRequest/index.ts +3 -2
  78. package/src/interceptors/XMLHttpRequest/utils/concatArrayBuffer.ts +12 -0
  79. package/src/interceptors/XMLHttpRequest/utils/concateArrayBuffer.test.ts +14 -0
  80. package/src/interceptors/XMLHttpRequest/utils/createResponse.ts +13 -0
  81. package/src/interceptors/fetch/index.ts +30 -69
  82. package/src/utils/RequestWithCredentials.ts +21 -0
  83. package/src/utils/bufferUtils.ts +10 -5
  84. package/src/utils/parseJson.ts +1 -1
  85. package/src/utils/toInteractiveRequest.ts +29 -0
  86. package/lib/InteractiveIsomorphicRequest.d.ts +0 -7
  87. package/lib/InteractiveIsomorphicRequest.js +0 -37
  88. package/lib/InteractiveIsomorphicRequest.js.map +0 -1
  89. package/lib/IsomorphicRequest.d.ts +0 -24
  90. package/lib/IsomorphicRequest.js +0 -107
  91. package/lib/IsomorphicRequest.js.map +0 -1
  92. package/lib/interceptors/ClientRequest/utils/bodyBufferToString.d.ts +0 -2
  93. package/lib/interceptors/ClientRequest/utils/bodyBufferToString.js +0 -11
  94. package/lib/interceptors/ClientRequest/utils/bodyBufferToString.js.map +0 -1
  95. package/lib/interceptors/ClientRequest/utils/concatChunkToBuffer.d.ts +0 -2
  96. package/lib/interceptors/ClientRequest/utils/concatChunkToBuffer.js +0 -11
  97. package/lib/interceptors/ClientRequest/utils/concatChunkToBuffer.js.map +0 -1
  98. package/lib/interceptors/XMLHttpRequest/utils/bufferFrom.d.ts +0 -5
  99. package/lib/interceptors/XMLHttpRequest/utils/bufferFrom.js +0 -20
  100. package/lib/interceptors/XMLHttpRequest/utils/bufferFrom.js.map +0 -1
  101. package/lib/utils/toIsoResponse.d.ts +0 -5
  102. package/lib/utils/toIsoResponse.js +0 -18
  103. package/lib/utils/toIsoResponse.js.map +0 -1
  104. package/src/InteractiveIsomorphicRequest.ts +0 -24
  105. package/src/IsomorphicRequest.test.ts +0 -106
  106. package/src/IsomorphicRequest.ts +0 -86
  107. package/src/interceptors/ClientRequest/utils/bodyBufferToString.test.ts +0 -16
  108. package/src/interceptors/ClientRequest/utils/bodyBufferToString.ts +0 -7
  109. package/src/interceptors/ClientRequest/utils/concatChunkToBuffer.test.ts +0 -13
  110. package/src/interceptors/ClientRequest/utils/concatChunkToBuffer.ts +0 -10
  111. package/src/interceptors/XMLHttpRequest/utils/bufferFrom.test.ts +0 -11
  112. package/src/interceptors/XMLHttpRequest/utils/bufferFrom.ts +0 -16
  113. package/src/utils/toIsoResponse.test.ts +0 -39
  114. package/src/utils/toIsoResponse.ts +0 -14
@@ -4,21 +4,21 @@
4
4
  */
5
5
  import type { Debugger } from 'debug'
6
6
  import { until } from '@open-draft/until'
7
- import {
8
- Headers,
9
- stringToHeaders,
10
- objectToHeaders,
11
- headersToString,
12
- } from 'headers-polyfill'
7
+ import { Headers, stringToHeaders, headersToString } from 'headers-polyfill'
13
8
  import { DOMParser } from '@xmldom/xmldom'
14
9
  import { parseJson } from '../../utils/parseJson'
15
- import { toIsoResponse } from '../../utils/toIsoResponse'
16
- import { bufferFrom } from './utils/bufferFrom'
17
10
  import { createEvent } from './utils/createEvent'
18
11
  import type { XMLHttpRequestEmitter } from '.'
19
- import { IsomorphicRequest } from '../../IsomorphicRequest'
20
- import { encodeBuffer } from '../../utils/bufferUtils'
21
- import { InteractiveIsomorphicRequest } from '../../InteractiveIsomorphicRequest'
12
+ import {
13
+ encodeBuffer,
14
+ decodeBuffer,
15
+ toArrayBuffer,
16
+ } from '../../utils/bufferUtils'
17
+ import { createResponse } from './utils/createResponse'
18
+ import { concatArrayBuffer } from './utils/concatArrayBuffer'
19
+ import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
20
+ import { uuidv4 } from '../../utils/uuid'
21
+ import { createRequestWithCredentials } from '../../utils/RequestWithCredentials'
22
22
 
23
23
  type XMLHttpRequestEventHandler = (
24
24
  this: XMLHttpRequest,
@@ -55,6 +55,7 @@ export const createXMLHttpRequestOverride = (
55
55
  return class XMLHttpRequestOverride implements XMLHttpRequest {
56
56
  _requestHeaders: Headers
57
57
  _responseHeaders: Headers
58
+ _responseBuffer: Uint8Array
58
59
 
59
60
  // Collection of events modified by `addEventListener`/`removeEventListener` calls.
60
61
  _events: XMLHttpRequestEvent<InternalXMLHttpRequestEventTargetEventMap>[] =
@@ -85,10 +86,7 @@ export const createXMLHttpRequestOverride = (
85
86
  public user?: string
86
87
  public password?: string
87
88
  public async?: boolean
88
- public response: any
89
- public responseText: string
90
89
  public responseType: XMLHttpRequestResponseType
91
- public responseXML: Document | null
92
90
  public responseURL: string
93
91
  public upload: XMLHttpRequestUpload
94
92
  public readyState: number
@@ -129,17 +127,15 @@ export const createXMLHttpRequestOverride = (
129
127
  this.method = 'GET'
130
128
  this.readyState = this.UNSENT
131
129
  this.withCredentials = false
132
- this.status = 200
133
- this.statusText = 'OK'
134
- this.response = ''
130
+ this.status = 0
131
+ this.statusText = ''
135
132
  this.responseType = 'text'
136
- this.responseText = ''
137
- this.responseXML = null
138
133
  this.responseURL = ''
139
134
  this.upload = {} as any
140
135
  this.timeout = 0
141
136
 
142
137
  this._requestHeaders = new Headers()
138
+ this._responseBuffer = new Uint8Array()
143
139
  this._responseHeaders = new Headers()
144
140
  }
145
141
 
@@ -169,7 +165,6 @@ export const createXMLHttpRequestOverride = (
169
165
  this.log('trigger "%s" (%d)', eventName, this.readyState)
170
166
  this.log('resolve listener for event "%s"', eventName)
171
167
 
172
- // @ts-expect-error XMLHttpRequest class has no index signature.
173
168
  const callback = this[`on${eventName}`] as XMLHttpRequestEventHandler
174
169
  callback?.call(this, createEvent(this, eventName, options))
175
170
 
@@ -191,12 +186,10 @@ export const createXMLHttpRequestOverride = (
191
186
  this.log('reset')
192
187
 
193
188
  this.setReadyState(this.UNSENT)
194
- this.status = 200
195
- this.statusText = 'OK'
196
- this.response = null as any
197
- this.responseText = null as any
198
- this.responseXML = null as any
189
+ this.status = 0
190
+ this.statusText = ''
199
191
 
192
+ this._responseBuffer = new Uint8Array()
200
193
  this._requestHeaders = new Headers()
201
194
  this._responseHeaders = new Headers()
202
195
  }
@@ -228,12 +221,9 @@ export const createXMLHttpRequestOverride = (
228
221
 
229
222
  public send(data?: string | ArrayBuffer) {
230
223
  this.log('send %s %s', this.method, this.url)
231
- let buffer: ArrayBuffer
232
- if (typeof data === 'string') {
233
- buffer = encodeBuffer(data)
234
- } else {
235
- buffer = data || new ArrayBuffer(0)
236
- }
224
+
225
+ const requestBuffer: ArrayBuffer | undefined =
226
+ typeof data === 'string' ? encodeBuffer(data) : data
237
227
 
238
228
  let url: URL
239
229
 
@@ -249,34 +239,36 @@ export const createXMLHttpRequestOverride = (
249
239
  this.log('request headers', this._requestHeaders)
250
240
 
251
241
  // Create an intercepted request instance exposed to the request intercepting middleware.
252
- const isomorphicRequest = new IsomorphicRequest(url, {
253
- body: buffer,
242
+ const requestId = uuidv4()
243
+ const capturedRequest = createRequestWithCredentials(url, {
254
244
  method: this.method,
255
245
  headers: this._requestHeaders,
256
246
  credentials: this.withCredentials ? 'include' : 'omit',
247
+ body: requestBuffer,
257
248
  })
258
249
 
259
- const interactiveIsomorphicRequest = new InteractiveIsomorphicRequest(
260
- isomorphicRequest
261
- )
250
+ const interactiveRequest = toInteractiveRequest(capturedRequest)
262
251
 
263
252
  this.log(
264
253
  'emitting the "request" event for %d listener(s)...',
265
254
  emitter.listenerCount('request')
266
255
  )
267
- emitter.emit('request', interactiveIsomorphicRequest)
256
+ emitter.emit('request', interactiveRequest, requestId)
268
257
 
269
258
  this.log('awaiting mocked response...')
270
259
 
271
260
  Promise.resolve(
272
261
  until(async () => {
273
- await emitter.untilIdle('request', ({ args: [request] }) => {
274
- return request.id === interactiveIsomorphicRequest.id
275
- })
262
+ await emitter.untilIdle(
263
+ 'request',
264
+ ({ args: [, pendingRequestId] }) => {
265
+ return pendingRequestId === requestId
266
+ }
267
+ )
276
268
  this.log('all request listeners have been resolved!')
277
269
 
278
270
  const [mockedResponse] =
279
- await interactiveIsomorphicRequest.respondWith.invoked()
271
+ await interactiveRequest.respondWith.invoked()
280
272
  this.log('event.respondWith called with:', mockedResponse)
281
273
 
282
274
  return mockedResponse
@@ -290,72 +282,108 @@ export const createXMLHttpRequestOverride = (
290
282
  middlewareException
291
283
  )
292
284
 
285
+ // Mark the request as complete.
286
+ this.setReadyState(this.DONE)
287
+
293
288
  // No way to propagate the actual error message.
294
289
  this.trigger('error')
295
- this.abort()
290
+
291
+ // Emit the "loadend" event to notify that the request has settled.
292
+ // In this case, there's been an error with the request so
293
+ // we must not emit the "load" event.
294
+ this.trigger('loadend')
295
+
296
+ // Abort must not be called when request fails!
297
+ // this.abort()
296
298
 
297
299
  return
298
300
  }
299
301
 
302
+ // Forward request headers modified in the "request" listener.
303
+ this._requestHeaders = new Headers(capturedRequest.headers)
304
+
300
305
  // Return a mocked response, if provided in the middleware.
301
306
  if (mockedResponse) {
307
+ const responseClone = mockedResponse.clone()
302
308
  this.log('received mocked response', mockedResponse)
303
309
 
304
- // Trigger a loadstart event to indicate the initialization of the fetch.
305
- this.trigger('loadstart')
306
-
307
310
  this.status = mockedResponse.status ?? 200
308
311
  this.statusText = mockedResponse.statusText || 'OK'
309
- this._responseHeaders = mockedResponse.headers
310
- ? objectToHeaders(mockedResponse.headers)
311
- : new Headers()
312
-
313
312
  this.log('set response status', this.status, this.statusText)
313
+
314
+ this._responseHeaders = new Headers(mockedResponse.headers || {})
314
315
  this.log('set response headers', this._responseHeaders)
315
316
 
317
+ this.log('response type', this.responseType)
318
+ this.responseURL = this.url
319
+
320
+ const totalLength = this._responseHeaders.has('Content-Length')
321
+ ? Number(this._responseHeaders.get('Content-Length'))
322
+ : undefined
323
+
324
+ // Trigger a loadstart event to indicate the initialization of the fetch.
325
+ this.trigger('loadstart', { loaded: 0, total: totalLength })
326
+
316
327
  // Mark that response headers has been received
317
328
  // and trigger a ready state event to reflect received headers
318
- // in a custom `onreadystatechange` callback.
329
+ // in a custom "onreadystatechange" callback.
319
330
  this.setReadyState(this.HEADERS_RECEIVED)
320
331
 
321
- this.log('response type', this.responseType)
322
- this.response = this.getResponseBody(mockedResponse.body)
323
- this.responseURL = this.url
324
- this.responseText = mockedResponse.body || ''
325
- this.responseXML = this.getResponseXML()
332
+ this.setReadyState(this.LOADING)
326
333
 
327
- this.log('set response body', this.response)
328
-
329
- if (mockedResponse.body && this.response) {
330
- this.setReadyState(this.LOADING)
334
+ const closeResponseStream = () => {
335
+ /**
336
+ * Explicitly mark the request as done so its response never hangs.
337
+ * @see https://github.com/mswjs/interceptors/issues/13
338
+ */
339
+ this.setReadyState(this.DONE)
331
340
 
332
- // Presence of the mocked response implies a response body (not null).
333
- // Presence of the coerced `this.response` implies the mocked body is valid.
334
- const bodyBuffer = bufferFrom(mockedResponse.body)
341
+ // Always trigger the "load" event because at this point
342
+ // the request has been performed successfully.
343
+ this.trigger('load', {
344
+ loaded: this._responseBuffer.byteLength,
345
+ total: totalLength,
346
+ })
335
347
 
336
- // Trigger a progress event based on the mocked response body.
337
- this.trigger('progress', {
338
- loaded: bodyBuffer.length,
339
- total: bodyBuffer.length,
348
+ // Trigger a loadend event to indicate the fetch has completed.
349
+ this.trigger('loadend', {
350
+ loaded: this._responseBuffer.byteLength,
351
+ total: totalLength,
340
352
  })
353
+
354
+ emitter.emit('response', responseClone, capturedRequest, requestId)
341
355
  }
342
356
 
343
- /**
344
- * Explicitly mark the request as done so its response never hangs.
345
- * @see https://github.com/mswjs/interceptors/issues/13
346
- */
347
- this.setReadyState(this.DONE)
357
+ if (mockedResponse.body) {
358
+ const reader = mockedResponse.body.getReader()
348
359
 
349
- // Trigger a load event to indicate the fetch has succeeded.
350
- this.trigger('load')
351
- // Trigger a loadend event to indicate the fetch has completed.
352
- this.trigger('loadend')
360
+ const readNextChunk = async (): Promise<void> => {
361
+ const { value, done } = await reader.read()
353
362
 
354
- emitter.emit(
355
- 'response',
356
- isomorphicRequest,
357
- toIsoResponse(mockedResponse)
358
- )
363
+ if (done) {
364
+ closeResponseStream()
365
+ return
366
+ }
367
+
368
+ if (value) {
369
+ this._responseBuffer = concatArrayBuffer(
370
+ this._responseBuffer,
371
+ value
372
+ )
373
+
374
+ this.trigger('progress', {
375
+ loaded: this._responseBuffer.byteLength,
376
+ total: totalLength,
377
+ })
378
+ }
379
+
380
+ readNextChunk()
381
+ }
382
+
383
+ readNextChunk()
384
+ } else {
385
+ closeResponseStream()
386
+ }
359
387
  } else {
360
388
  this.log('no mocked response received!')
361
389
 
@@ -371,72 +399,148 @@ export const createXMLHttpRequestOverride = (
371
399
  this.password
372
400
  )
373
401
 
374
- // Reflect a successful state of the original request
375
- // on the patched instance.
376
- originalRequest.addEventListener('load', () => {
377
- this.log('original "onload"')
402
+ originalRequest.addEventListener('readystatechange', () => {
403
+ // Forward the original response headers to the patched instance
404
+ // immediately as they are received.
405
+ if (
406
+ originalRequest.readyState === XMLHttpRequest.HEADERS_RECEIVED
407
+ ) {
408
+ const responseHeaders = originalRequest.getAllResponseHeaders()
409
+ this.log('original response headers:\n', responseHeaders)
410
+
411
+ this._responseHeaders = stringToHeaders(responseHeaders)
412
+ this.log(
413
+ 'original response headers (normalized)',
414
+ this._responseHeaders
415
+ )
416
+ }
417
+ })
418
+
419
+ originalRequest.addEventListener('loadstart', () => {
420
+ // Forward the response type to the patched instance immediately.
421
+ // Response type affects how response reading properties are resolved.
422
+ this.responseType = originalRequest.responseType
423
+ })
424
+
425
+ originalRequest.addEventListener('progress', () => {
426
+ this._responseBuffer = concatArrayBuffer(
427
+ this._responseBuffer,
428
+ encodeBuffer(originalRequest.responseText)
429
+ )
430
+ })
431
+
432
+ // Update the patched instance on the "loadend" event
433
+ // because it fires when the request settles (succeeds/errors).
434
+ originalRequest.addEventListener('loadend', () => {
435
+ this.log('original "loadend"')
378
436
 
379
437
  this.status = originalRequest.status
380
438
  this.statusText = originalRequest.statusText
381
439
  this.responseURL = originalRequest.responseURL
382
- this.responseType = originalRequest.responseType
383
- this.response = originalRequest.response
384
- this.responseText = originalRequest.responseText
385
- this.responseXML = originalRequest.responseXML
386
-
387
- this.log('set mock request readyState to DONE')
440
+ this.log('received original response', this.status, this.statusText)
388
441
 
389
442
  // Explicitly mark the mocked request instance as done
390
443
  // so the response never hangs.
391
- /**
392
- * @note `readystatechange` listener is called TWICE
393
- * in the case of unhandled request.
394
- */
395
444
  this.setReadyState(this.DONE)
445
+ this.log('set mock request readyState to DONE')
396
446
 
397
- this.log('received original response', this.status, this.statusText)
398
447
  this.log('original response body:', this.response)
448
+ this.log('original response finished!')
399
449
 
400
- const responseHeaders = originalRequest.getAllResponseHeaders()
401
- this.log('original response headers:\n', responseHeaders)
402
-
403
- this._responseHeaders = stringToHeaders(responseHeaders)
404
- this.log(
405
- 'original response headers (normalized)',
406
- this._responseHeaders
450
+ emitter.emit(
451
+ 'response',
452
+ createResponse(originalRequest, this._responseBuffer),
453
+ capturedRequest,
454
+ requestId
407
455
  )
408
-
409
- this.log('original response finished')
410
-
411
- emitter.emit('response', isomorphicRequest, {
412
- status: originalRequest.status,
413
- statusText: originalRequest.statusText,
414
- headers: this._responseHeaders,
415
- body: originalRequest.response,
416
- })
417
456
  })
418
457
 
458
+ this.propagateHeaders(originalRequest, this._requestHeaders)
459
+
419
460
  // Assign callbacks and event listeners from the intercepted XHR instance
420
461
  // to the original XHR instance.
421
462
  this.propagateCallbacks(originalRequest)
422
463
  this.propagateListeners(originalRequest)
423
- this.propagateHeaders(originalRequest, this._requestHeaders)
424
464
 
425
465
  if (this.async) {
426
466
  originalRequest.timeout = this.timeout
427
467
  }
428
468
 
469
+ /**
470
+ * @note Set the intercepted request ID on the original request
471
+ * so that if it triggers any other interceptors, they don't attempt
472
+ * to process it once again. This happens when bypassing XMLHttpRequest
473
+ * because it's polyfilled with "http.ClientRequest" in JSDOM.
474
+ */
475
+ originalRequest.setRequestHeader('X-Request-Id', requestId)
476
+
429
477
  this.log('send', data)
430
478
  originalRequest.send(data)
431
479
  }
432
480
  })
433
481
  }
434
482
 
483
+ public get responseText(): string {
484
+ this.log('responseText()')
485
+
486
+ const encoding = this.getResponseHeader('Content-Encoding') as
487
+ | BufferEncoding
488
+ | undefined
489
+
490
+ return decodeBuffer(this._responseBuffer, encoding || undefined)
491
+ }
492
+
493
+ public get response(): unknown {
494
+ switch (this.responseType) {
495
+ case 'json': {
496
+ this.log('resolving response body as JSON')
497
+ return parseJson(this.responseText)
498
+ }
499
+
500
+ case 'arraybuffer': {
501
+ this.log('resolving response body as ArrayBuffer')
502
+ return toArrayBuffer(this._responseBuffer)
503
+ }
504
+
505
+ case 'blob': {
506
+ const mimeType =
507
+ this.getResponseHeader('content-type') || 'text/plain'
508
+ this.log('resolving response body as blog (%s)', mimeType)
509
+ return new Blob([this.responseText], { type: mimeType })
510
+ }
511
+
512
+ case 'document': {
513
+ this.log('resolving response body as XML')
514
+ return this.responseXML
515
+ }
516
+
517
+ default: {
518
+ return this.responseText
519
+ }
520
+ }
521
+ }
522
+
523
+ public get responseXML(): Document | null {
524
+ const contentType = this.getResponseHeader('content-type') || ''
525
+ this.log('responseXML() %s', contentType)
526
+
527
+ if (
528
+ contentType.startsWith('application/xml') ||
529
+ contentType.startsWith('text/xml')
530
+ ) {
531
+ this.log('response content-type is XML, parsing...')
532
+ return new DOMParser().parseFromString(this.responseText, contentType)
533
+ }
534
+
535
+ this.log('response content type is not XML, returning null...')
536
+ return null
537
+ }
538
+
435
539
  public abort() {
436
- this.log('abort')
540
+ this.log('abort()')
437
541
 
438
542
  if (this.readyState > this.UNSENT && this.readyState < this.DONE) {
439
- this.setReadyState(this.UNSENT)
543
+ this.reset()
440
544
  this.trigger('abort')
441
545
  }
442
546
  }
@@ -446,12 +550,12 @@ export const createXMLHttpRequestOverride = (
446
550
  }
447
551
 
448
552
  public setRequestHeader(name: string, value: string) {
449
- this.log('set request header "%s" to "%s"', name, value)
553
+ this.log('setRequestHeader() "%s" to "%s"', name, value)
450
554
  this._requestHeaders.append(name, value)
451
555
  }
452
556
 
453
557
  public getResponseHeader(name: string): string | null {
454
- this.log('get response header "%s"', name)
558
+ this.log('getResponseHeader() "%s"', name)
455
559
 
456
560
  if (this.readyState < this.HEADERS_RECEIVED) {
457
561
  this.log(
@@ -474,7 +578,7 @@ export const createXMLHttpRequestOverride = (
474
578
  }
475
579
 
476
580
  public getAllResponseHeaders(): string {
477
- this.log('get all response headers')
581
+ this.log('getAllResponseHeaders()')
478
582
 
479
583
  if (this.readyState < this.HEADERS_RECEIVED) {
480
584
  this.log(
@@ -488,70 +592,27 @@ export const createXMLHttpRequestOverride = (
488
592
  }
489
593
 
490
594
  public addEventListener<
491
- K extends keyof InternalXMLHttpRequestEventTargetEventMap
492
- >(name: K, listener: XMLHttpRequestEventHandler) {
493
- this.log('addEventListener', name, listener)
595
+ Event extends keyof InternalXMLHttpRequestEventTargetEventMap
596
+ >(event: Event, listener: XMLHttpRequestEventHandler) {
597
+ this.log('addEventListener', event, listener)
494
598
  this._events.push({
495
- name,
599
+ name: event,
496
600
  listener,
497
601
  })
498
602
  }
499
603
 
500
- public removeEventListener<K extends keyof XMLHttpRequestEventMap>(
501
- name: K,
502
- listener: (event?: XMLHttpRequestEventMap[K]) => void
604
+ public removeEventListener<Event extends keyof XMLHttpRequestEventMap>(
605
+ event: Event,
606
+ listener: (event?: XMLHttpRequestEventMap[Event]) => void
503
607
  ): void {
504
608
  this.log('removeEventListener', name, listener)
505
609
  this._events = this._events.filter((storedEvent) => {
506
- return storedEvent.name !== name && storedEvent.listener !== listener
610
+ return storedEvent.name !== event && storedEvent.listener !== listener
507
611
  })
508
612
  }
509
613
 
510
614
  public overrideMimeType() {}
511
615
 
512
- /**
513
- * Resolves the response based on the `responseType` value.
514
- */
515
- getResponseBody(body: string | undefined) {
516
- // Handle an improperly set "null" value of the mocked response body.
517
- const textBody = body ?? ''
518
- this.log('coerced response body to', textBody)
519
-
520
- switch (this.responseType) {
521
- case 'json': {
522
- this.log('resolving response body as JSON')
523
- return parseJson(textBody)
524
- }
525
-
526
- case 'blob': {
527
- const blobType =
528
- this.getResponseHeader('content-type') || 'text/plain'
529
- this.log('resolving response body as Blob', { type: blobType })
530
-
531
- return new Blob([textBody], {
532
- type: blobType,
533
- })
534
- }
535
-
536
- case 'arraybuffer': {
537
- this.log('resolving response body as ArrayBuffer')
538
- const arrayBuffer = bufferFrom(textBody)
539
- return arrayBuffer
540
- }
541
-
542
- default:
543
- return textBody
544
- }
545
- }
546
-
547
- getResponseXML() {
548
- const contentType = this.getResponseHeader('Content-Type')
549
- if (contentType === 'application/xml' || contentType === 'text/xml') {
550
- return new DOMParser().parseFromString(this.responseText, contentType)
551
- }
552
- return null
553
- }
554
-
555
616
  /**
556
617
  * Propagates mock XMLHttpRequest instance callbacks
557
618
  * to the given XMLHttpRequest instance.
@@ -608,15 +669,14 @@ export const createXMLHttpRequestOverride = (
608
669
  propagateHeaders(request: XMLHttpRequest, headers: Headers) {
609
670
  this.log('propagating request headers to the original request', headers)
610
671
 
611
- // Preserve the request headers casing.
612
- Object.entries(headers.raw()).forEach(([name, value]) => {
672
+ for (const [headerName, headerValue] of headers) {
613
673
  this.log(
614
674
  'setting "%s" (%s) header on the original request',
615
- name,
616
- value
675
+ headerName,
676
+ headerValue
617
677
  )
618
- request.setRequestHeader(name, value)
619
- })
678
+ request.setRequestHeader(headerName, headerValue)
679
+ }
620
680
  }
621
681
  }
622
682
  }
@@ -1,12 +1,13 @@
1
1
  import { invariant } from 'outvariant'
2
2
  import { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'
3
- import { InteractiveIsomorphicRequest } from '../../InteractiveIsomorphicRequest'
3
+ import { InteractiveRequest } from '../../utils/toInteractiveRequest'
4
4
  import { Interceptor } from '../../Interceptor'
5
5
  import { AsyncEventEmitter } from '../../utils/AsyncEventEmitter'
6
6
  import { createXMLHttpRequestOverride } from './XMLHttpRequestOverride'
7
7
 
8
8
  export type XMLHttpRequestEventListener = (
9
- request: InteractiveIsomorphicRequest
9
+ request: InteractiveRequest,
10
+ requestId: string
10
11
  ) => Promise<void> | void
11
12
 
12
13
  export type XMLHttpRequestEmitter = AsyncEventEmitter<HttpRequestEventMap>
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Concatenate two `Uint8Array` buffers.
3
+ */
4
+ export function concatArrayBuffer(
5
+ left: Uint8Array,
6
+ right: Uint8Array
7
+ ): Uint8Array {
8
+ const result = new Uint8Array(left.byteLength + right.byteLength)
9
+ result.set(left, 0)
10
+ result.set(right, left.byteLength)
11
+ return result
12
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @jest-environment node
3
+ */
4
+ import { concatArrayBuffer } from './concatArrayBuffer'
5
+
6
+ const encoder = new TextEncoder()
7
+
8
+ it('concatenates two Uint8Array buffers', () => {
9
+ const result = concatArrayBuffer(
10
+ encoder.encode('hello'),
11
+ encoder.encode('world')
12
+ )
13
+ expect(result).toEqual(encoder.encode('helloworld'))
14
+ })
@@ -0,0 +1,13 @@
1
+ import { Response } from '@remix-run/web-fetch'
2
+ import { stringToHeaders } from 'headers-polyfill'
3
+
4
+ export function createResponse(
5
+ request: XMLHttpRequest,
6
+ responseBody: Uint8Array
7
+ ): Response {
8
+ return new Response(responseBody, {
9
+ status: request.status,
10
+ statusText: request.statusText,
11
+ headers: stringToHeaders(request.getAllResponseHeaders()),
12
+ })
13
+ }