@mswjs/interceptors 0.19.5 → 0.20.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.
@@ -1,684 +0,0 @@
1
- /**
2
- * XMLHttpRequest override class.
3
- * Inspired by https://github.com/marvinhagemeister/xhr-mocklet.
4
- */
5
- import type { Debugger } from 'debug'
6
- import { until } from '@open-draft/until'
7
- import { Request } from '@remix-run/web-fetch'
8
- import { Headers, stringToHeaders, headersToString } from 'headers-polyfill'
9
- import { parseJson } from '../../utils/parseJson'
10
- import { createEvent } from './utils/createEvent'
11
- import type { XMLHttpRequestEmitter } from '.'
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 { isDomParserSupportedType } from './utils/isDomParserSupportedType'
22
-
23
- type XMLHttpRequestEventHandler = (
24
- this: XMLHttpRequest,
25
- event: Event | ProgressEvent<any>
26
- ) => void
27
-
28
- interface XMLHttpRequestEvent<EventMap extends any> {
29
- name: keyof EventMap
30
- listener: XMLHttpRequestEventHandler
31
- }
32
-
33
- interface CreateXMLHttpRequestOverrideOptions {
34
- XMLHttpRequest: typeof window.XMLHttpRequest
35
- emitter: XMLHttpRequestEmitter
36
- log: Debugger
37
- }
38
-
39
- interface InternalXMLHttpRequestEventTargetEventMap
40
- extends XMLHttpRequestEventTargetEventMap {
41
- readystatechange: Event
42
- }
43
-
44
- export type ExtractCallbacks<Key extends string> = Key extends
45
- | 'abort'
46
- | `on${infer _CallbackName}`
47
- ? Key
48
- : never
49
-
50
- export const createXMLHttpRequestOverride = (
51
- options: CreateXMLHttpRequestOverrideOptions
52
- ) => {
53
- const { XMLHttpRequest, emitter, log } = options
54
-
55
- return class XMLHttpRequestOverride implements XMLHttpRequest {
56
- _requestHeaders: Headers
57
- _responseHeaders: Headers
58
- _responseBuffer: Uint8Array
59
-
60
- // Collection of events modified by `addEventListener`/`removeEventListener` calls.
61
- _events: XMLHttpRequestEvent<InternalXMLHttpRequestEventTargetEventMap>[] =
62
- []
63
-
64
- log: Debugger = log
65
-
66
- /* Request state */
67
- public static readonly UNSENT = 0
68
- public static readonly OPENED = 1
69
- public static readonly HEADERS_RECEIVED = 2
70
- public static readonly LOADING = 3
71
- public static readonly DONE = 4
72
- public readonly UNSENT = 0
73
- public readonly OPENED = 1
74
- public readonly HEADERS_RECEIVED = 2
75
- public readonly LOADING = 3
76
- public readonly DONE = 4
77
-
78
- /* Custom public properties */
79
- public method: string
80
- public url: string
81
-
82
- /* XHR public properties */
83
- public withCredentials: boolean
84
- public status: number
85
- public statusText: string
86
- public user?: string
87
- public password?: string
88
- public async?: boolean
89
- public responseType: XMLHttpRequestResponseType
90
- public responseURL: string
91
- public upload: XMLHttpRequestUpload
92
- public readyState: number
93
- public onreadystatechange: (this: XMLHttpRequest, ev: Event) => any =
94
- null as any
95
- public timeout: number
96
-
97
- /* Events */
98
- public onabort: (
99
- this: XMLHttpRequestEventTarget,
100
- event: ProgressEvent
101
- ) => any = null as any
102
- public onerror: (this: XMLHttpRequestEventTarget, event: Event) => any =
103
- null as any
104
- public onload: (
105
- this: XMLHttpRequestEventTarget,
106
- event: ProgressEvent
107
- ) => any = null as any
108
- public onloadend: (
109
- this: XMLHttpRequestEventTarget,
110
- event: ProgressEvent
111
- ) => any = null as any
112
- public onloadstart: (
113
- this: XMLHttpRequestEventTarget,
114
- event: ProgressEvent
115
- ) => any = null as any
116
- public onprogress: (
117
- this: XMLHttpRequestEventTarget,
118
- event: ProgressEvent
119
- ) => any = null as any
120
- public ontimeout: (
121
- this: XMLHttpRequestEventTarget,
122
- event: ProgressEvent
123
- ) => any = null as any
124
-
125
- constructor() {
126
- this.url = ''
127
- this.method = 'GET'
128
- this.readyState = this.UNSENT
129
- this.withCredentials = false
130
- this.status = 0
131
- this.statusText = ''
132
- this.responseType = 'text'
133
- this.responseURL = ''
134
- this.upload = {} as any
135
- this.timeout = 0
136
-
137
- this._requestHeaders = new Headers()
138
- this._responseBuffer = new Uint8Array()
139
- this._responseHeaders = new Headers()
140
- }
141
-
142
- setReadyState(nextState: number): void {
143
- if (nextState === this.readyState) {
144
- return
145
- }
146
-
147
- this.log('readyState change %d -> %d', this.readyState, nextState)
148
- this.readyState = nextState
149
-
150
- if (nextState !== this.UNSENT) {
151
- this.log('triggering readystate change...')
152
- this.trigger('readystatechange')
153
- }
154
- }
155
-
156
- /**
157
- * Triggers both direct callback and attached event listeners
158
- * for the given event.
159
- */
160
- trigger<
161
- K extends keyof (XMLHttpRequestEventTargetEventMap & {
162
- readystatechange: ProgressEvent<XMLHttpRequestEventTarget>
163
- })
164
- >(eventName: K, options?: ProgressEventInit) {
165
- this.log('trigger "%s" (%d)', eventName, this.readyState)
166
- this.log('resolve listener for event "%s"', eventName)
167
-
168
- const callback = this[`on${eventName}`] as XMLHttpRequestEventHandler
169
- callback?.call(this, createEvent(this, eventName, options))
170
-
171
- for (const event of this._events) {
172
- if (event.name === eventName) {
173
- log(
174
- 'calling mock event listener "%s" (%d)',
175
- eventName,
176
- this.readyState
177
- )
178
- event.listener.call(this, createEvent(this, eventName, options))
179
- }
180
- }
181
-
182
- return this
183
- }
184
-
185
- reset() {
186
- this.log('reset')
187
-
188
- this.setReadyState(this.UNSENT)
189
- this.status = 0
190
- this.statusText = ''
191
-
192
- this._responseBuffer = new Uint8Array()
193
- this._requestHeaders = new Headers()
194
- this._responseHeaders = new Headers()
195
- }
196
-
197
- public async open(
198
- method: string,
199
- url: string,
200
- async: boolean = true,
201
- user?: string,
202
- password?: string
203
- ) {
204
- this.log = this.log.extend(`request ${method} ${url}`)
205
- this.log('open', { method, url, async, user, password })
206
-
207
- this.reset()
208
- this.setReadyState(this.OPENED)
209
-
210
- if (typeof url === 'undefined') {
211
- this.url = method
212
- this.method = 'GET'
213
- } else {
214
- this.url = url
215
- this.method = method
216
- this.async = async
217
- this.user = user
218
- this.password = password
219
- }
220
- }
221
-
222
- public send(data?: string | ArrayBuffer) {
223
- this.log('send %s %s', this.method, this.url)
224
-
225
- const requestBuffer: ArrayBuffer | undefined =
226
- typeof data === 'string' ? encodeBuffer(data) : data
227
-
228
- let url: URL
229
-
230
- try {
231
- url = new URL(this.url)
232
- } catch (error) {
233
- // Assume a relative URL, if construction of a new `URL` instance fails.
234
- // Since `XMLHttpRequest` always executed in a DOM-like environment,
235
- // resolve the relative request URL against the current window location.
236
- url = new URL(this.url, window.location.href)
237
- }
238
-
239
- this.log('request headers', this._requestHeaders)
240
-
241
- // Create an intercepted request instance exposed to the request intercepting middleware.
242
- const requestId = uuidv4()
243
- const capturedRequest = new Request(url, {
244
- method: this.method,
245
- headers: this._requestHeaders,
246
- credentials: this.withCredentials ? 'include' : 'omit',
247
- body: requestBuffer,
248
- })
249
-
250
- const interactiveRequest = toInteractiveRequest(capturedRequest)
251
-
252
- this.log(
253
- 'emitting the "request" event for %d listener(s)...',
254
- emitter.listenerCount('request')
255
- )
256
- emitter.emit('request', interactiveRequest, requestId)
257
-
258
- this.log('awaiting mocked response...')
259
-
260
- Promise.resolve(
261
- until(async () => {
262
- await emitter.untilIdle(
263
- 'request',
264
- ({ args: [, pendingRequestId] }) => {
265
- return pendingRequestId === requestId
266
- }
267
- )
268
- this.log('all request listeners have been resolved!')
269
-
270
- const [mockedResponse] =
271
- await interactiveRequest.respondWith.invoked()
272
- this.log('event.respondWith called with:', mockedResponse)
273
-
274
- return mockedResponse
275
- })
276
- ).then(([middlewareException, mockedResponse]) => {
277
- // When the request middleware throws an exception, error the request.
278
- // This cancels the request and is similar to a network error.
279
- if (middlewareException) {
280
- this.log(
281
- 'middleware function threw an exception!',
282
- middlewareException
283
- )
284
-
285
- // Mark the request as complete.
286
- this.setReadyState(this.DONE)
287
-
288
- // No way to propagate the actual error message.
289
- this.trigger('error')
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()
298
-
299
- return
300
- }
301
-
302
- // Forward request headers modified in the "request" listener.
303
- this._requestHeaders = new Headers(capturedRequest.headers)
304
-
305
- // Return a mocked response, if provided in the middleware.
306
- if (mockedResponse) {
307
- const responseClone = mockedResponse.clone()
308
- this.log('received mocked response', mockedResponse)
309
-
310
- this.status = mockedResponse.status ?? 200
311
- this.statusText = mockedResponse.statusText || 'OK'
312
- this.log('set response status', this.status, this.statusText)
313
-
314
- this._responseHeaders = new Headers(mockedResponse.headers || {})
315
- this.log('set response headers', this._responseHeaders)
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
-
327
- // Mark that response headers has been received
328
- // and trigger a ready state event to reflect received headers
329
- // in a custom "onreadystatechange" callback.
330
- this.setReadyState(this.HEADERS_RECEIVED)
331
-
332
- this.setReadyState(this.LOADING)
333
-
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)
340
-
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
- })
347
-
348
- // Trigger a loadend event to indicate the fetch has completed.
349
- this.trigger('loadend', {
350
- loaded: this._responseBuffer.byteLength,
351
- total: totalLength,
352
- })
353
-
354
- emitter.emit('response', responseClone, capturedRequest, requestId)
355
- }
356
-
357
- if (mockedResponse.body) {
358
- const reader = mockedResponse.body.getReader()
359
-
360
- const readNextChunk = async (): Promise<void> => {
361
- const { value, done } = await reader.read()
362
-
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
- }
387
- } else {
388
- this.log('no mocked response received!')
389
-
390
- // Perform an original request, when the request middleware returned no mocked response.
391
- const originalRequest = new XMLHttpRequest()
392
-
393
- this.log('opening an original request %s %s', this.method, this.url)
394
- originalRequest.open(
395
- this.method,
396
- this.url,
397
- this.async ?? true,
398
- this.user,
399
- this.password
400
- )
401
-
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
- originalRequest.addEventListener('load', () => {
433
- this.status = originalRequest.status
434
- this.statusText = originalRequest.statusText
435
- this.responseURL = originalRequest.responseURL
436
- this.log('received original response', this.status, this.statusText)
437
-
438
- // Explicitly mark the mocked request instance as done
439
- // so the response never hangs.
440
- this.setReadyState(this.DONE)
441
- this.log('set mock request readyState to DONE')
442
-
443
- this.log('original response body:', this.response)
444
- this.log('original response finished!')
445
- });
446
-
447
- // Update the patched instance on the "loadend" event
448
- // because it fires when the request settles (succeeds/errors).
449
- originalRequest.addEventListener('loadend', () => {
450
- this.log('original "loadend"')
451
-
452
- emitter.emit(
453
- 'response',
454
- createResponse(originalRequest, this._responseBuffer),
455
- capturedRequest,
456
- requestId
457
- )
458
- })
459
-
460
- this.propagateHeaders(originalRequest, this._requestHeaders)
461
-
462
- // Assign callbacks and event listeners from the intercepted XHR instance
463
- // to the original XHR instance.
464
- this.propagateCallbacks(originalRequest)
465
- this.propagateListeners(originalRequest)
466
-
467
- if (this.async) {
468
- originalRequest.timeout = this.timeout
469
- }
470
-
471
- /**
472
- * @note Set the intercepted request ID on the original request
473
- * so that if it triggers any other interceptors, they don't attempt
474
- * to process it once again. This happens when bypassing XMLHttpRequest
475
- * because it's polyfilled with "http.ClientRequest" in JSDOM.
476
- */
477
- originalRequest.setRequestHeader('X-Request-Id', requestId)
478
-
479
- this.log('send', data)
480
- originalRequest.send(data)
481
- }
482
- })
483
- }
484
-
485
- public get responseText(): string {
486
- this.log('responseText()')
487
-
488
- return decodeBuffer(this._responseBuffer)
489
- }
490
-
491
- public get response(): unknown {
492
- switch (this.responseType) {
493
- case 'json': {
494
- this.log('resolving response body as JSON')
495
- return parseJson(this.responseText)
496
- }
497
-
498
- case 'arraybuffer': {
499
- this.log('resolving response body as ArrayBuffer')
500
- return toArrayBuffer(this._responseBuffer)
501
- }
502
-
503
- case 'blob': {
504
- const mimeType =
505
- this.getResponseHeader('content-type') || 'text/plain'
506
- this.log('resolving response body as blog (%s)', mimeType)
507
- return new Blob([this.responseText], { type: mimeType })
508
- }
509
-
510
- case 'document': {
511
- this.log('resolving response body as XML')
512
- return this.responseXML
513
- }
514
-
515
- default: {
516
- return this.responseText
517
- }
518
- }
519
- }
520
-
521
- public get responseXML(): Document | null {
522
- const contentType = this.getResponseHeader('content-type') || ''
523
- this.log('responseXML() %s', contentType)
524
-
525
- if (typeof DOMParser === 'undefined') {
526
- console.warn(
527
- 'Cannot retrieve XMLHttpRequest response body as XML: DOMParser is not defined. You are likely using an environment that is not browser or does not polyfill browser globals correctly.'
528
- )
529
- return null
530
- }
531
-
532
- if (isDomParserSupportedType(contentType)) {
533
- this.log('response content-type is XML, parsing...')
534
- return new DOMParser().parseFromString(this.responseText, contentType)
535
- }
536
-
537
- this.log('response content type is not XML, returning null...')
538
- return null
539
- }
540
-
541
- public abort() {
542
- this.log('abort()')
543
-
544
- if (this.readyState > this.UNSENT && this.readyState < this.DONE) {
545
- this.reset()
546
- this.trigger('abort')
547
- }
548
- }
549
-
550
- public dispatchEvent() {
551
- return false
552
- }
553
-
554
- public setRequestHeader(name: string, value: string) {
555
- this.log('setRequestHeader() "%s" to "%s"', name, value)
556
- this._requestHeaders.append(name, value)
557
- }
558
-
559
- public getResponseHeader(name: string): string | null {
560
- this.log('getResponseHeader() "%s"', name)
561
-
562
- if (this.readyState < this.HEADERS_RECEIVED) {
563
- this.log(
564
- 'cannot return a header: headers not received (state: %s)',
565
- this.readyState
566
- )
567
- return null
568
- }
569
-
570
- const headerValue = this._responseHeaders.get(name)
571
-
572
- this.log(
573
- 'resolved response header "%s" to "%s"',
574
- name,
575
- headerValue,
576
- this._responseHeaders
577
- )
578
-
579
- return headerValue
580
- }
581
-
582
- public getAllResponseHeaders(): string {
583
- this.log('getAllResponseHeaders()')
584
-
585
- if (this.readyState < this.HEADERS_RECEIVED) {
586
- this.log(
587
- 'cannot return headers: headers not received (state: %s)',
588
- this.readyState
589
- )
590
- return ''
591
- }
592
-
593
- return headersToString(this._responseHeaders)
594
- }
595
-
596
- public addEventListener<
597
- Event extends keyof InternalXMLHttpRequestEventTargetEventMap
598
- >(event: Event, listener: XMLHttpRequestEventHandler) {
599
- this.log('addEventListener', event, listener)
600
- this._events.push({
601
- name: event,
602
- listener,
603
- })
604
- }
605
-
606
- public removeEventListener<Event extends keyof XMLHttpRequestEventMap>(
607
- event: Event,
608
- listener: (event?: XMLHttpRequestEventMap[Event]) => void
609
- ): void {
610
- this.log('removeEventListener', name, listener)
611
- this._events = this._events.filter((storedEvent) => {
612
- return storedEvent.name !== event && storedEvent.listener !== listener
613
- })
614
- }
615
-
616
- public overrideMimeType() {}
617
-
618
- /**
619
- * Propagates mock XMLHttpRequest instance callbacks
620
- * to the given XMLHttpRequest instance.
621
- */
622
- propagateCallbacks(request: XMLHttpRequest) {
623
- this.log('propagating request callbacks to the original request')
624
- const callbackNames: Array<ExtractCallbacks<keyof XMLHttpRequest>> = [
625
- 'abort',
626
- 'onerror',
627
- 'ontimeout',
628
- 'onload',
629
- 'onloadstart',
630
- 'onloadend',
631
- 'onprogress',
632
- 'onreadystatechange',
633
- ]
634
-
635
- for (const callbackName of callbackNames) {
636
- const callback = this[callbackName]
637
-
638
- if (callback) {
639
- request[callbackName] = this[callbackName] as any
640
-
641
- this.log('propagated the "%s" callback', callbackName, callback)
642
- }
643
- }
644
-
645
- request.onabort = this.abort
646
- request.onerror = this.onerror
647
- request.ontimeout = this.ontimeout
648
- request.onload = this.onload
649
- request.onloadstart = this.onloadstart
650
- request.onloadend = this.onloadend
651
- request.onprogress = this.onprogress
652
- request.onreadystatechange = this.onreadystatechange
653
- }
654
-
655
- /**
656
- * Propagates the mock XMLHttpRequest instance listeners
657
- * to the given XMLHttpRequest instance.
658
- */
659
- propagateListeners(request: XMLHttpRequest) {
660
- this.log(
661
- 'propagating request listeners (%d) to the original request',
662
- this._events.length,
663
- this._events
664
- )
665
-
666
- this._events.forEach(({ name, listener }) => {
667
- request.addEventListener(name, listener)
668
- })
669
- }
670
-
671
- propagateHeaders(request: XMLHttpRequest, headers: Headers) {
672
- this.log('propagating request headers to the original request', headers)
673
-
674
- for (const [headerName, headerValue] of headers) {
675
- this.log(
676
- 'setting "%s" (%s) header on the original request',
677
- headerName,
678
- headerValue
679
- )
680
- request.setRequestHeader(headerName, headerValue)
681
- }
682
- }
683
- }
684
- }