@mswjs/interceptors 0.34.1 → 0.34.3

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 (65) hide show
  1. package/lib/browser/{chunk-7A4UJNSW.mjs → chunk-73X2CAEH.mjs} +6 -6
  2. package/lib/browser/chunk-73X2CAEH.mjs.map +1 -0
  3. package/lib/browser/{chunk-VRKVKT62.js → chunk-EJYZ4HR3.js} +6 -6
  4. package/lib/browser/chunk-EJYZ4HR3.js.map +1 -0
  5. package/lib/browser/{chunk-ZXJSL23E.mjs → chunk-KOFJYPUE.mjs} +108 -37
  6. package/lib/browser/chunk-KOFJYPUE.mjs.map +1 -0
  7. package/lib/browser/{chunk-LERABB57.js → chunk-NBHFK2DJ.js} +110 -39
  8. package/lib/browser/chunk-NBHFK2DJ.js.map +1 -0
  9. package/lib/browser/{chunk-6MBJUL74.js → chunk-TPZUQHHY.js} +5 -5
  10. package/lib/browser/{chunk-NU2MPFD6.mjs → chunk-W7UG7MBM.mjs} +2 -2
  11. package/lib/browser/interceptors/XMLHttpRequest/index.js +3 -3
  12. package/lib/browser/interceptors/XMLHttpRequest/index.mjs +2 -2
  13. package/lib/browser/interceptors/fetch/index.js +3 -3
  14. package/lib/browser/interceptors/fetch/index.mjs +2 -2
  15. package/lib/browser/presets/browser.js +5 -5
  16. package/lib/browser/presets/browser.mjs +3 -3
  17. package/lib/node/RemoteHttpInterceptor.js +7 -7
  18. package/lib/node/RemoteHttpInterceptor.mjs +3 -3
  19. package/lib/node/{chunk-T34TGCMR.js → chunk-6L3UERDR.js} +110 -39
  20. package/lib/node/chunk-6L3UERDR.js.map +1 -0
  21. package/lib/node/{chunk-RTGLFNO3.mjs → chunk-GILG336Y.mjs} +2 -2
  22. package/lib/node/{chunk-DCMEIPZN.mjs → chunk-KA2KMHFU.mjs} +6 -23
  23. package/lib/node/chunk-KA2KMHFU.mjs.map +1 -0
  24. package/lib/node/{chunk-5WWNCLB3.js → chunk-TQD7SQGP.js} +6 -6
  25. package/lib/node/chunk-TQD7SQGP.js.map +1 -0
  26. package/lib/node/{chunk-UN335ZTD.js → chunk-W4AQXISM.js} +5 -5
  27. package/lib/node/{chunk-RJGP7FQP.mjs → chunk-WGBCEHTV.mjs} +108 -37
  28. package/lib/node/chunk-WGBCEHTV.mjs.map +1 -0
  29. package/lib/node/{chunk-I57YSVAV.js → chunk-WTJL7BRV.js} +10 -27
  30. package/lib/node/chunk-WTJL7BRV.js.map +1 -0
  31. package/lib/node/{chunk-KY3RJ2M3.mjs → chunk-XSXCGXEY.mjs} +6 -6
  32. package/lib/node/chunk-XSXCGXEY.mjs.map +1 -0
  33. package/lib/node/interceptors/ClientRequest/index.js +3 -3
  34. package/lib/node/interceptors/ClientRequest/index.mjs +2 -2
  35. package/lib/node/interceptors/XMLHttpRequest/index.js +3 -3
  36. package/lib/node/interceptors/XMLHttpRequest/index.mjs +2 -2
  37. package/lib/node/interceptors/fetch/index.js +3 -3
  38. package/lib/node/interceptors/fetch/index.mjs +2 -2
  39. package/lib/node/presets/node.js +7 -7
  40. package/lib/node/presets/node.mjs +4 -4
  41. package/package.json +3 -1
  42. package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.test.ts +12 -14
  43. package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.ts +12 -12
  44. package/src/interceptors/XMLHttpRequest/XMLHttpRequestController.ts +149 -43
  45. package/src/interceptors/XMLHttpRequest/XMLHttpRequestProxy.ts +2 -2
  46. package/src/interceptors/XMLHttpRequest/polyfills/EventPolyfill.ts +4 -4
  47. package/src/interceptors/XMLHttpRequest/utils/createEvent.ts +1 -1
  48. package/src/interceptors/XMLHttpRequest/utils/getBodyByteLength.test.ts +164 -0
  49. package/src/interceptors/XMLHttpRequest/utils/getBodyByteLength.ts +16 -0
  50. package/src/utils/handleRequest.ts +6 -6
  51. package/lib/browser/chunk-7A4UJNSW.mjs.map +0 -1
  52. package/lib/browser/chunk-LERABB57.js.map +0 -1
  53. package/lib/browser/chunk-VRKVKT62.js.map +0 -1
  54. package/lib/browser/chunk-ZXJSL23E.mjs.map +0 -1
  55. package/lib/node/chunk-5WWNCLB3.js.map +0 -1
  56. package/lib/node/chunk-DCMEIPZN.mjs.map +0 -1
  57. package/lib/node/chunk-I57YSVAV.js.map +0 -1
  58. package/lib/node/chunk-KY3RJ2M3.mjs.map +0 -1
  59. package/lib/node/chunk-RJGP7FQP.mjs.map +0 -1
  60. package/lib/node/chunk-T34TGCMR.js.map +0 -1
  61. package/src/utils/getRequestOptionsByUrl.ts +0 -29
  62. /package/lib/browser/{chunk-6MBJUL74.js.map → chunk-TPZUQHHY.js.map} +0 -0
  63. /package/lib/browser/{chunk-NU2MPFD6.mjs.map → chunk-W7UG7MBM.mjs.map} +0 -0
  64. /package/lib/node/{chunk-RTGLFNO3.mjs.map → chunk-GILG336Y.mjs.map} +0 -0
  65. /package/lib/node/{chunk-UN335ZTD.js.map → chunk-W4AQXISM.js.map} +0 -0
@@ -1,15 +1,15 @@
1
1
  import {
2
2
  ClientRequestInterceptor
3
- } from "../chunk-DCMEIPZN.mjs";
3
+ } from "../chunk-KA2KMHFU.mjs";
4
4
  import {
5
5
  XMLHttpRequestInterceptor
6
- } from "../chunk-RJGP7FQP.mjs";
6
+ } from "../chunk-WGBCEHTV.mjs";
7
7
  import "../chunk-6HYIRFX2.mjs";
8
8
  import {
9
9
  FetchInterceptor
10
- } from "../chunk-RTGLFNO3.mjs";
10
+ } from "../chunk-GILG336Y.mjs";
11
11
  import "../chunk-BZ3Y7YV5.mjs";
12
- import "../chunk-KY3RJ2M3.mjs";
12
+ import "../chunk-XSXCGXEY.mjs";
13
13
  import "../chunk-BUCULLYM.mjs";
14
14
 
15
15
  // src/presets/node.ts
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mswjs/interceptors",
3
3
  "description": "Low-level HTTP/HTTPS/XHR/fetch request interception library.",
4
- "version": "0.34.1",
4
+ "version": "0.34.3",
5
5
  "main": "./lib/node/index.js",
6
6
  "module": "./lib/node/index.mjs",
7
7
  "types": "./lib/node/index.d.ts",
@@ -117,6 +117,7 @@
117
117
  "@playwright/test": "^1.37.1",
118
118
  "@types/cors": "^2.8.12",
119
119
  "@types/express": "^4.17.13",
120
+ "@types/express-fileupload": "^1.5.0",
120
121
  "@types/express-rate-limit": "^6.0.0",
121
122
  "@types/follow-redirects": "^1.14.1",
122
123
  "@types/jest": "^27.0.3",
@@ -132,6 +133,7 @@
132
133
  "cz-conventional-changelog": "3.3.0",
133
134
  "engine.io-parser": "^5.2.1",
134
135
  "express": "^4.17.3",
136
+ "express-fileupload": "^1.5.1",
135
137
  "express-rate-limit": "^6.3.0",
136
138
  "follow-redirects": "^1.15.1",
137
139
  "got": "^11.8.3",
@@ -186,7 +186,9 @@ it('handles [URL, RequestOptions, callback] input', () => {
186
186
  expect(url.href).toEqual('https://mswjs.io/resource')
187
187
 
188
188
  // Options must be preserved.
189
- expect(options).toEqual<RequestOptions>({
189
+ // `urlToHttpOptions` from `node:url` generates additional
190
+ // ClientRequest options, some of which are not legally allowed.
191
+ expect(options).toMatchObject<RequestOptions>({
190
192
  agent: false,
191
193
  _defaultAgent: httpsGlobalAgent,
192
194
  protocol: url.protocol,
@@ -194,8 +196,6 @@ it('handles [URL, RequestOptions, callback] input', () => {
194
196
  headers: {
195
197
  'Content-Type': 'text/plain',
196
198
  },
197
-
198
- host: url.host,
199
199
  hostname: url.hostname,
200
200
  path: url.pathname,
201
201
  })
@@ -214,7 +214,7 @@ it('handles [URL, RequestOptions] where options have custom "hostname"', () => {
214
214
  ])
215
215
  expect(url.href).toBe('http://host-from-options.com/path-from-url')
216
216
  expect(options).toMatchObject({
217
- host: 'host-from-options.com',
217
+ hostname: 'host-from-options.com',
218
218
  path: '/path-from-url',
219
219
  })
220
220
  })
@@ -230,8 +230,8 @@ it('handles [URL, RequestOptions] where options contain "host" and "path" and "p
230
230
  ])
231
231
  // Must remove the query string since it's not specified in "options.path"
232
232
  expect(url.href).toBe('http://host-from-options.com:1234/path-from-options')
233
- expect(options).toMatchObject({
234
- host: 'host-from-options.com:1234',
233
+ expect(options).toMatchObject<RequestOptions>({
234
+ hostname: 'host-from-options.com',
235
235
  path: '/path-from-options',
236
236
  port: 1234,
237
237
  })
@@ -245,8 +245,8 @@ it('handles [URL, RequestOptions] where options contain "path" with query string
245
245
  },
246
246
  ])
247
247
  expect(url.href).toBe('http://example.com/path-from-options?foo=bar&baz=xyz')
248
- expect(options).toMatchObject({
249
- host: 'example.com',
248
+ expect(options).toMatchObject<RequestOptions>({
249
+ hostname: 'example.com',
250
250
  path: '/path-from-options?foo=bar&baz=xyz',
251
251
  })
252
252
  })
@@ -399,7 +399,6 @@ it('merges URL-based RequestOptions with the custom RequestOptions', () => {
399
399
 
400
400
  // Other options must be inferred from the URL.
401
401
  expect(options.protocol).toEqual(url.protocol)
402
- expect(options.host).toEqual(url.host)
403
402
  expect(options.hostname).toEqual(url.hostname)
404
403
  expect(options.path).toEqual(url.pathname)
405
404
  })
@@ -414,7 +413,6 @@ it('respects custom "options.path" over URL path', () => {
414
413
 
415
414
  expect(url.href).toBe('http://example.com/path-from-options')
416
415
  expect(options.protocol).toBe('http:')
417
- expect(options.host).toBe('example.com')
418
416
  expect(options.hostname).toBe('example.com')
419
417
  expect(options.path).toBe('/path-from-options')
420
418
  })
@@ -430,20 +428,20 @@ it('respects custom "options.path" over URL path with query string', () => {
430
428
  // Must replace both the path and the query string.
431
429
  expect(url.href).toBe('http://example.com/path-from-options')
432
430
  expect(options.protocol).toBe('http:')
433
- expect(options.host).toBe('example.com')
434
431
  expect(options.hostname).toBe('example.com')
435
432
  expect(options.path).toBe('/path-from-options')
436
433
  })
437
434
 
438
435
  it('preserves URL query string', () => {
439
436
  const [url, options] = normalizeClientRequestArgs('http:', [
440
- new URL('http://example.com/resource?a=b&c=d'),
437
+ new URL('http://example.com:8080/resource?a=b&c=d'),
441
438
  ])
442
439
 
443
- expect(url.href).toBe('http://example.com/resource?a=b&c=d')
440
+ expect(url.href).toBe('http://example.com:8080/resource?a=b&c=d')
444
441
  expect(options.protocol).toBe('http:')
445
- expect(options.host).toBe('example.com')
442
+ // expect(options.host).toBe('example.com:8080')
446
443
  expect(options.hostname).toBe('example.com')
447
444
  // Query string is a part of the options path.
448
445
  expect(options.path).toBe('/resource?a=b&c=d')
446
+ expect(options.port).toBe(8080)
449
447
  })
@@ -1,3 +1,4 @@
1
+ import { urlToHttpOptions } from 'node:url'
1
2
  import {
2
3
  Agent as HttpAgent,
3
4
  globalAgent as httpGlobalAgent,
@@ -20,7 +21,6 @@ import {
20
21
  parse as parseUrl,
21
22
  } from 'node:url'
22
23
  import { Logger } from '@open-draft/logger'
23
- import { getRequestOptionsByUrl } from '../../../utils/getRequestOptionsByUrl'
24
24
  import {
25
25
  ResolvedRequestOptions,
26
26
  getUrlByRequestOptions,
@@ -47,12 +47,12 @@ function resolveRequestOptions(
47
47
  // without any `RequestOptions` or callback.
48
48
  if (typeof args[1] === 'undefined' || typeof args[1] === 'function') {
49
49
  logger.info('request options not provided, deriving from the url', url)
50
- return getRequestOptionsByUrl(url)
50
+ return urlToHttpOptions(url)
51
51
  }
52
52
 
53
53
  if (args[1]) {
54
54
  logger.info('has custom RequestOptions!', args[1])
55
- const requestOptionsFromUrl = getRequestOptionsByUrl(url)
55
+ const requestOptionsFromUrl = urlToHttpOptions(url)
56
56
 
57
57
  logger.info('derived RequestOptions from the URL:', requestOptionsFromUrl)
58
58
 
@@ -103,7 +103,7 @@ function resolveCallback(
103
103
  export type NormalizedClientRequestArgs = [
104
104
  url: URL,
105
105
  options: ResolvedRequestOptions,
106
- callback?: HttpRequestCallback,
106
+ callback?: HttpRequestCallback
107
107
  ]
108
108
 
109
109
  /**
@@ -137,7 +137,7 @@ export function normalizeClientRequestArgs(
137
137
  url = new URL(args[0])
138
138
  logger.info('created a url:', url)
139
139
 
140
- const requestOptionsFromUrl = getRequestOptionsByUrl(url)
140
+ const requestOptionsFromUrl = urlToHttpOptions(url)
141
141
  logger.info('request options from url:', requestOptionsFromUrl)
142
142
 
143
143
  options = resolveRequestOptions(args, url)
@@ -200,17 +200,17 @@ export function normalizeClientRequestArgs(
200
200
  return args[1] === undefined
201
201
  ? normalizeClientRequestArgs(defaultProtocol, [resolvedUrl])
202
202
  : typeof args[1] === 'function'
203
- ? normalizeClientRequestArgs(defaultProtocol, [resolvedUrl, args[1]])
204
- : normalizeClientRequestArgs(defaultProtocol, [
205
- resolvedUrl,
206
- args[1],
207
- args[2],
208
- ])
203
+ ? normalizeClientRequestArgs(defaultProtocol, [resolvedUrl, args[1]])
204
+ : normalizeClientRequestArgs(defaultProtocol, [
205
+ resolvedUrl,
206
+ args[1],
207
+ args[2],
208
+ ])
209
209
  }
210
210
  // Handle a given "RequestOptions" object as-is
211
211
  // and derive the URL instance from it.
212
212
  else if (isObject(args[0])) {
213
- options = { ... args[0] as any }
213
+ options = { ...(args[0] as any) }
214
214
  logger.info('first argument is RequestOptions:', options)
215
215
 
216
216
  // When handling a "RequestOptions" object without an explicit "protocol",
@@ -14,9 +14,11 @@ import { parseJson } from '../../utils/parseJson'
14
14
  import { createResponse } from './utils/createResponse'
15
15
  import { INTERNAL_REQUEST_ID_HEADER_NAME } from '../../Interceptor'
16
16
  import { createRequestId } from '../../createRequestId'
17
+ import { getBodyByteLength } from './utils/getBodyByteLength'
17
18
 
18
- const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
19
+ const kIsRequestHandled = Symbol('kIsRequestHandled')
19
20
  const IS_NODE = isNodeProcess()
21
+ const kFetchRequest = Symbol('kFetchRequest')
20
22
 
21
23
  /**
22
24
  * An `XMLHttpRequest` instance controller that allows us
@@ -40,17 +42,25 @@ export class XMLHttpRequestController {
40
42
  request: Request
41
43
  requestId: string
42
44
  }
43
- ) => void
45
+ ) => void;
44
46
 
47
+ [kIsRequestHandled]: boolean;
48
+ [kFetchRequest]?: Request
45
49
  private method: string = 'GET'
46
50
  private url: URL = null as any
47
51
  private requestHeaders: Headers
48
- private requestBody?: XMLHttpRequestBodyInit | Document | null
49
52
  private responseBuffer: Uint8Array
50
53
  private events: Map<keyof XMLHttpRequestEventTargetEventMap, Array<Function>>
54
+ private uploadEvents: Map<
55
+ keyof XMLHttpRequestEventTargetEventMap,
56
+ Array<Function>
57
+ >
51
58
 
52
59
  constructor(readonly initialRequest: XMLHttpRequest, public logger: Logger) {
60
+ this[kIsRequestHandled] = false
61
+
53
62
  this.events = new Map()
63
+ this.uploadEvents = new Map()
54
64
  this.requestId = createRequestId()
55
65
  this.requestHeaders = new Headers()
56
66
  this.responseBuffer = new Uint8Array()
@@ -123,11 +133,6 @@ export class XMLHttpRequestController {
123
133
  body?: XMLHttpRequestBodyInit | Document | null
124
134
  ]
125
135
 
126
- if (body != null) {
127
- this.requestBody =
128
- typeof body === 'string' ? encodeBuffer(body) : body
129
- }
130
-
131
136
  this.request.addEventListener('load', () => {
132
137
  if (typeof this.onResponse !== 'undefined') {
133
138
  // Create a Fetch API Response representation of whichever
@@ -146,15 +151,20 @@ export class XMLHttpRequestController {
146
151
  // Notify the consumer about the response.
147
152
  this.onResponse.call(this, {
148
153
  response: fetchResponse,
149
- isMockedResponse: IS_MOCKED_RESPONSE in this.request,
154
+ isMockedResponse: this[kIsRequestHandled],
150
155
  request: fetchRequest,
151
156
  requestId: this.requestId!,
152
157
  })
153
158
  }
154
159
  })
155
160
 
161
+ const requestBody =
162
+ typeof body === 'string' ? encodeBuffer(body) : body
163
+
156
164
  // Delegate request handling to the consumer.
157
- const fetchRequest = this.toFetchApiRequest()
165
+ const fetchRequest = this.toFetchApiRequest(requestBody)
166
+ this[kFetchRequest] = fetchRequest
167
+
158
168
  const onceRequestSettled =
159
169
  this.onRequest?.call(this, {
160
170
  request: fetchRequest,
@@ -162,10 +172,8 @@ export class XMLHttpRequestController {
162
172
  }) || Promise.resolve()
163
173
 
164
174
  onceRequestSettled.finally(() => {
165
- // If the consumer didn't handle the request perform it as-is.
166
- // Note that the request may not yet be DONE and may, in fact,
167
- // be LOADING while the "respondWith" method does its magic.
168
- if (this.request.readyState < this.request.LOADING) {
175
+ // If the consumer didn't handle the request (called `.respondWith()`) perform it as-is.
176
+ if (!this[kIsRequestHandled]) {
169
177
  this.logger.info(
170
178
  'request callback settled but request has not been handled (readystate %d), performing as-is...',
171
179
  this.request.readyState
@@ -200,6 +208,49 @@ export class XMLHttpRequestController {
200
208
  }
201
209
  },
202
210
  })
211
+
212
+ /**
213
+ * Proxy the `.upload` property to gather the event listeners/callbacks.
214
+ */
215
+ define(
216
+ this.request,
217
+ 'upload',
218
+ createProxy(this.request.upload, {
219
+ setProperty: ([propertyName, nextValue], invoke) => {
220
+ switch (propertyName) {
221
+ case 'onloadstart':
222
+ case 'onprogress':
223
+ case 'onaboart':
224
+ case 'onerror':
225
+ case 'onload':
226
+ case 'ontimeout':
227
+ case 'onloadend': {
228
+ const eventName = propertyName.slice(
229
+ 2
230
+ ) as keyof XMLHttpRequestEventTargetEventMap
231
+
232
+ this.registerUploadEvent(eventName, nextValue as Function)
233
+ }
234
+ }
235
+
236
+ return invoke()
237
+ },
238
+ methodCall: ([methodName, args], invoke) => {
239
+ switch (methodName) {
240
+ case 'addEventListener': {
241
+ const [eventName, listener] = args as [
242
+ keyof XMLHttpRequestEventTargetEventMap,
243
+ Function
244
+ ]
245
+ this.registerUploadEvent(eventName, listener)
246
+ this.logger.info('upload.addEventListener', eventName, listener)
247
+
248
+ return invoke()
249
+ }
250
+ }
251
+ },
252
+ })
253
+ )
203
254
  }
204
255
 
205
256
  private registerEvent(
@@ -213,23 +264,64 @@ export class XMLHttpRequestController {
213
264
  this.logger.info('registered event "%s"', eventName, listener)
214
265
  }
215
266
 
267
+ private registerUploadEvent(
268
+ eventName: keyof XMLHttpRequestEventTargetEventMap,
269
+ listener: Function
270
+ ): void {
271
+ const prevEvents = this.uploadEvents.get(eventName) || []
272
+ const nextEvents = prevEvents.concat(listener)
273
+ this.uploadEvents.set(eventName, nextEvents)
274
+
275
+ this.logger.info('registered upload event "%s"', eventName, listener)
276
+ }
277
+
216
278
  /**
217
279
  * Responds to the current request with the given
218
280
  * Fetch API `Response` instance.
219
281
  */
220
- public respondWith(response: Response): void {
221
- this.logger.info(
222
- 'responding with a mocked response: %d %s',
223
- response.status,
224
- response.statusText
225
- )
226
-
282
+ public async respondWith(response: Response): Promise<void> {
227
283
  /**
228
284
  * @note Since `XMLHttpRequestController` delegates the handling of the responses
229
285
  * to the "load" event listener that doesn't distinguish between the mocked and original
230
286
  * responses, mark the request that had a mocked response with a corresponding symbol.
287
+ *
288
+ * Mark this request as having a mocked response immediately since
289
+ * calculating request/response total body length is asynchronous.
231
290
  */
232
- define(this.request, IS_MOCKED_RESPONSE, true)
291
+ this[kIsRequestHandled] = true
292
+
293
+ /**
294
+ * Dispatch request upload events for requests with a body.
295
+ * @see https://github.com/mswjs/interceptors/issues/573
296
+ */
297
+ if (this[kFetchRequest]) {
298
+ const totalRequestBodyLength = await getBodyByteLength(
299
+ this[kFetchRequest].clone()
300
+ )
301
+
302
+ this.trigger('loadstart', this.request.upload, {
303
+ loaded: 0,
304
+ total: totalRequestBodyLength,
305
+ })
306
+ this.trigger('progress', this.request.upload, {
307
+ loaded: totalRequestBodyLength,
308
+ total: totalRequestBodyLength,
309
+ })
310
+ this.trigger('load', this.request.upload, {
311
+ loaded: totalRequestBodyLength,
312
+ total: totalRequestBodyLength,
313
+ })
314
+ this.trigger('loadend', this.request.upload, {
315
+ loaded: totalRequestBodyLength,
316
+ total: totalRequestBodyLength,
317
+ })
318
+ }
319
+
320
+ this.logger.info(
321
+ 'responding with a mocked response: %d %s',
322
+ response.status,
323
+ response.statusText
324
+ )
233
325
 
234
326
  define(this.request, 'status', response.status)
235
327
  define(this.request, 'statusText', response.statusText)
@@ -303,16 +395,11 @@ export class XMLHttpRequestController {
303
395
  },
304
396
  })
305
397
 
306
- const totalResponseBodyLength = response.headers.has('Content-Length')
307
- ? Number(response.headers.get('Content-Length'))
308
- : /**
309
- * @todo Infer the response body length from the response body.
310
- */
311
- undefined
398
+ const totalResponseBodyLength = await getBodyByteLength(response.clone())
312
399
 
313
400
  this.logger.info('calculated response body length', totalResponseBodyLength)
314
401
 
315
- this.trigger('loadstart', {
402
+ this.trigger('loadstart', this.request, {
316
403
  loaded: 0,
317
404
  total: totalResponseBodyLength,
318
405
  })
@@ -325,12 +412,12 @@ export class XMLHttpRequestController {
325
412
 
326
413
  this.setReadyState(this.request.DONE)
327
414
 
328
- this.trigger('load', {
415
+ this.trigger('load', this.request, {
329
416
  loaded: this.responseBuffer.byteLength,
330
417
  total: totalResponseBodyLength,
331
418
  })
332
419
 
333
- this.trigger('loadend', {
420
+ this.trigger('loadend', this.request, {
334
421
  loaded: this.responseBuffer.byteLength,
335
422
  total: totalResponseBodyLength,
336
423
  })
@@ -354,7 +441,7 @@ export class XMLHttpRequestController {
354
441
  this.logger.info('read response body chunk:', value)
355
442
  this.responseBuffer = concatArrayBuffer(this.responseBuffer, value)
356
443
 
357
- this.trigger('progress', {
444
+ this.trigger('progress', this.request, {
358
445
  loaded: this.responseBuffer.byteLength,
359
446
  total: totalResponseBodyLength,
360
447
  })
@@ -482,11 +569,16 @@ export class XMLHttpRequestController {
482
569
  }
483
570
 
484
571
  public errorWith(error?: Error): void {
572
+ /**
573
+ * @note Mark this request as handled even if it received a mock error.
574
+ * This prevents the controller from trying to perform this request as-is.
575
+ */
576
+ this[kIsRequestHandled] = true
485
577
  this.logger.info('responding with an error')
486
578
 
487
579
  this.setReadyState(this.request.DONE)
488
- this.trigger('error')
489
- this.trigger('loadend')
580
+ this.trigger('error', this.request)
581
+ this.trigger('loadend', this.request)
490
582
  }
491
583
 
492
584
  /**
@@ -511,7 +603,7 @@ export class XMLHttpRequestController {
511
603
  if (nextReadyState !== this.request.UNSENT) {
512
604
  this.logger.info('triggerring "readystatechange" event...')
513
605
 
514
- this.trigger('readystatechange')
606
+ this.trigger('readystatechange', this.request)
515
607
  }
516
608
  }
517
609
 
@@ -522,20 +614,27 @@ export class XMLHttpRequestController {
522
614
  EventName extends keyof (XMLHttpRequestEventTargetEventMap & {
523
615
  readystatechange: ProgressEvent<XMLHttpRequestEventTarget>
524
616
  })
525
- >(eventName: EventName, options?: ProgressEventInit): void {
526
- const callback = this.request[`on${eventName}`]
527
- const event = createEvent(this.request, eventName, options)
617
+ >(
618
+ eventName: EventName,
619
+ target: XMLHttpRequest | XMLHttpRequestUpload,
620
+ options?: ProgressEventInit
621
+ ): void {
622
+ const callback = (target as XMLHttpRequest)[`on${eventName}`]
623
+ const event = createEvent(target, eventName, options)
528
624
 
529
625
  this.logger.info('trigger "%s"', eventName, options || '')
530
626
 
531
627
  // Invoke direct callbacks.
532
628
  if (typeof callback === 'function') {
533
629
  this.logger.info('found a direct "%s" callback, calling...', eventName)
534
- callback.call(this.request, event)
630
+ callback.call(target as XMLHttpRequest, event)
535
631
  }
536
632
 
537
633
  // Invoke event listeners.
538
- for (const [registeredEventName, listeners] of this.events) {
634
+ const events =
635
+ target instanceof XMLHttpRequestUpload ? this.uploadEvents : this.events
636
+
637
+ for (const [registeredEventName, listeners] of events) {
539
638
  if (registeredEventName === eventName) {
540
639
  this.logger.info(
541
640
  'found %d listener(s) for "%s" event, calling...',
@@ -543,7 +642,7 @@ export class XMLHttpRequestController {
543
642
  eventName
544
643
  )
545
644
 
546
- listeners.forEach((listener) => listener.call(this.request, event))
645
+ listeners.forEach((listener) => listener.call(target, event))
547
646
  }
548
647
  }
549
648
  }
@@ -551,9 +650,16 @@ export class XMLHttpRequestController {
551
650
  /**
552
651
  * Converts this `XMLHttpRequest` instance into a Fetch API `Request` instance.
553
652
  */
554
- public toFetchApiRequest(): Request {
653
+ private toFetchApiRequest(
654
+ body: XMLHttpRequestBodyInit | Document | null | undefined
655
+ ): Request {
555
656
  this.logger.info('converting request to a Fetch API Request...')
556
657
 
658
+ // If the `Document` is used as the body of this XMLHttpRequest,
659
+ // set its inner text as the Fetch API Request body.
660
+ const resolvedBody =
661
+ body instanceof Document ? body.documentElement.innerText : body
662
+
557
663
  const fetchRequest = new Request(this.url.href, {
558
664
  method: this.method,
559
665
  headers: this.requestHeaders,
@@ -563,7 +669,7 @@ export class XMLHttpRequestController {
563
669
  credentials: this.request.withCredentials ? 'include' : 'same-origin',
564
670
  body: ['GET', 'HEAD'].includes(this.method.toUpperCase())
565
671
  ? null
566
- : (this.requestBody as BodyInit),
672
+ : resolvedBody,
567
673
  })
568
674
 
569
675
  const proxyHeaders = createProxy(fetchRequest.headers, {
@@ -66,8 +66,8 @@ export function createXMLHttpRequestProxy({
66
66
  requestId,
67
67
  controller,
68
68
  emitter,
69
- onResponse: (response) => {
70
- this.respondWith(response)
69
+ onResponse: async (response) => {
70
+ await this.respondWith(response)
71
71
  },
72
72
  onRequestError: () => {
73
73
  this.errorWith(new TypeError('Network error'))
@@ -1,8 +1,8 @@
1
1
  export class EventPolyfill implements Event {
2
- readonly AT_TARGET: number = 0
3
- readonly BUBBLING_PHASE: number = 0
4
- readonly CAPTURING_PHASE: number = 0
5
- readonly NONE: number = 0
2
+ readonly NONE = 0
3
+ readonly CAPTURING_PHASE = 1
4
+ readonly AT_TARGET = 2
5
+ readonly BUBBLING_PHASE = 3
6
6
 
7
7
  public type: string = ''
8
8
  public srcElement: EventTarget | null = null
@@ -4,7 +4,7 @@ import { ProgressEventPolyfill } from '../polyfills/ProgressEventPolyfill'
4
4
  const SUPPORTS_PROGRESS_EVENT = typeof ProgressEvent !== 'undefined'
5
5
 
6
6
  export function createEvent(
7
- target: XMLHttpRequest,
7
+ target: XMLHttpRequest | XMLHttpRequestUpload,
8
8
  type: string,
9
9
  init?: ProgressEventInit
10
10
  ): EventPolyfill {