@mswjs/interceptors 0.13.1 → 0.13.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.
- package/package.json +5 -4
- package/src/createInterceptor.ts +100 -0
- package/src/index.ts +5 -0
- package/src/interceptors/ClientRequest/NodeClientRequest.test.ts +283 -0
- package/src/interceptors/ClientRequest/NodeClientRequest.ts +377 -0
- package/src/interceptors/ClientRequest/http.get.ts +32 -0
- package/src/interceptors/ClientRequest/http.request.ts +29 -0
- package/src/interceptors/ClientRequest/index.ts +61 -0
- package/src/interceptors/ClientRequest/utils/bodyBufferToString.test.ts +16 -0
- package/src/interceptors/ClientRequest/utils/bodyBufferToString.ts +7 -0
- package/src/interceptors/ClientRequest/utils/cloneIncomingMessage.test.ts +20 -0
- package/src/interceptors/ClientRequest/utils/cloneIncomingMessage.ts +41 -0
- package/src/interceptors/ClientRequest/utils/concatChunkToBuffer.test.ts +13 -0
- package/src/interceptors/ClientRequest/utils/concatChunkToBuffer.ts +10 -0
- package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.test.ts +44 -0
- package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.ts +38 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.test.ts +336 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestArgs.ts +205 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.test.ts +40 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestEndArgs.ts +51 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.test.ts +35 -0
- package/src/interceptors/ClientRequest/utils/normalizeClientRequestWriteArgs.ts +36 -0
- package/src/interceptors/XMLHttpRequest/XMLHttpRequestOverride.ts +565 -0
- package/src/interceptors/XMLHttpRequest/index.ts +34 -0
- package/src/interceptors/XMLHttpRequest/polyfills/EventPolyfill.ts +51 -0
- package/src/interceptors/XMLHttpRequest/polyfills/ProgressEventPolyfill.ts +17 -0
- package/src/interceptors/XMLHttpRequest/utils/bufferFrom.test.ts +11 -0
- package/src/interceptors/XMLHttpRequest/utils/bufferFrom.ts +16 -0
- package/src/interceptors/XMLHttpRequest/utils/createEvent.test.ts +27 -0
- package/src/interceptors/XMLHttpRequest/utils/createEvent.ts +41 -0
- package/src/interceptors/fetch/index.ts +89 -0
- package/src/presets/browser.ts +8 -0
- package/src/presets/node.ts +8 -0
- package/src/remote.ts +176 -0
- package/src/utils/cloneObject.test.ts +93 -0
- package/src/utils/cloneObject.ts +34 -0
- package/src/utils/getCleanUrl.test.ts +31 -0
- package/src/utils/getCleanUrl.ts +6 -0
- package/src/utils/getRequestOptionsByUrl.ts +29 -0
- package/src/utils/getUrlByRequestOptions.test.ts +140 -0
- package/src/utils/getUrlByRequestOptions.ts +108 -0
- package/src/utils/isObject.test.ts +19 -0
- package/src/utils/isObject.ts +6 -0
- package/src/utils/parseJson.test.ts +9 -0
- package/src/utils/parseJson.ts +12 -0
- package/src/utils/toIsoResponse.test.ts +39 -0
- package/src/utils/toIsoResponse.ts +14 -0
- package/src/utils/uuid.ts +7 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { debug } from 'debug'
|
|
2
|
+
|
|
3
|
+
const log = debug('http normalizeWriteArgs')
|
|
4
|
+
|
|
5
|
+
export type ClientRequestWriteCallback = (error?: Error | null) => void
|
|
6
|
+
export type ClientRequestWriteArgs = [
|
|
7
|
+
chunk: string | Buffer,
|
|
8
|
+
encoding?: BufferEncoding | ClientRequestWriteCallback,
|
|
9
|
+
callback?: ClientRequestWriteCallback
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
export type NormalizedClientRequestWriteArgs = [
|
|
13
|
+
chunk: string | Buffer,
|
|
14
|
+
encoding?: BufferEncoding,
|
|
15
|
+
callback?: ClientRequestWriteCallback
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export function normalizeClientRequestWriteArgs(
|
|
19
|
+
args: ClientRequestWriteArgs
|
|
20
|
+
): NormalizedClientRequestWriteArgs {
|
|
21
|
+
log('normalizing ClientRequest.write arguments...', args)
|
|
22
|
+
|
|
23
|
+
const chunk = args[0]
|
|
24
|
+
const encoding =
|
|
25
|
+
typeof args[1] === 'string' ? (args[1] as BufferEncoding) : undefined
|
|
26
|
+
const callback = typeof args[1] === 'function' ? args[1] : args[2]
|
|
27
|
+
|
|
28
|
+
const writeArgs: NormalizedClientRequestWriteArgs = [
|
|
29
|
+
chunk,
|
|
30
|
+
encoding,
|
|
31
|
+
callback,
|
|
32
|
+
]
|
|
33
|
+
log('successfully normalized ClientRequest.write arguments:', writeArgs)
|
|
34
|
+
|
|
35
|
+
return writeArgs
|
|
36
|
+
}
|
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XMLHttpRequest override class.
|
|
3
|
+
* Inspired by https://github.com/marvinhagemeister/xhr-mocklet.
|
|
4
|
+
*/
|
|
5
|
+
import { until } from '@open-draft/until'
|
|
6
|
+
import {
|
|
7
|
+
Headers,
|
|
8
|
+
stringToHeaders,
|
|
9
|
+
objectToHeaders,
|
|
10
|
+
headersToString,
|
|
11
|
+
} from 'headers-utils'
|
|
12
|
+
import { DOMParser } from '@xmldom/xmldom'
|
|
13
|
+
import { IsomorphicRequest, Observer, Resolver } from '../../createInterceptor'
|
|
14
|
+
import { parseJson } from '../../utils/parseJson'
|
|
15
|
+
import { toIsoResponse } from '../../utils/toIsoResponse'
|
|
16
|
+
import { uuidv4 } from '../../utils/uuid'
|
|
17
|
+
import { bufferFrom } from './utils/bufferFrom'
|
|
18
|
+
import { createEvent } from './utils/createEvent'
|
|
19
|
+
|
|
20
|
+
const createDebug = require('debug')
|
|
21
|
+
|
|
22
|
+
type XMLHttpRequestEventHandler = (
|
|
23
|
+
this: XMLHttpRequest,
|
|
24
|
+
event: Event | ProgressEvent<any>
|
|
25
|
+
) => void
|
|
26
|
+
|
|
27
|
+
interface XMLHttpRequestEvent<EventMap extends any> {
|
|
28
|
+
name: keyof EventMap
|
|
29
|
+
listener: XMLHttpRequestEventHandler
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface CreateXMLHttpRequestOverrideOptions {
|
|
33
|
+
pureXMLHttpRequest: typeof window.XMLHttpRequest
|
|
34
|
+
observer: Observer
|
|
35
|
+
resolver: Resolver
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface InternalXMLHttpRequestEventTargetEventMap
|
|
39
|
+
extends XMLHttpRequestEventTargetEventMap {
|
|
40
|
+
readystatechange: Event
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const createXMLHttpRequestOverride = (
|
|
44
|
+
options: CreateXMLHttpRequestOverrideOptions
|
|
45
|
+
) => {
|
|
46
|
+
const { pureXMLHttpRequest, observer, resolver } = options
|
|
47
|
+
let debug = createDebug('XHR')
|
|
48
|
+
|
|
49
|
+
return class XMLHttpRequestOverride implements XMLHttpRequest {
|
|
50
|
+
_requestHeaders: Headers
|
|
51
|
+
_responseHeaders: Headers
|
|
52
|
+
|
|
53
|
+
// Collection of events modified by `addEventListener`/`removeEventListener` calls.
|
|
54
|
+
_events: XMLHttpRequestEvent<InternalXMLHttpRequestEventTargetEventMap>[] =
|
|
55
|
+
[]
|
|
56
|
+
|
|
57
|
+
/* Request state */
|
|
58
|
+
public static readonly UNSENT = 0
|
|
59
|
+
public static readonly OPENED = 1
|
|
60
|
+
public static readonly HEADERS_RECEIVED = 2
|
|
61
|
+
public static readonly LOADING = 3
|
|
62
|
+
public static readonly DONE = 4
|
|
63
|
+
public readonly UNSENT = 0
|
|
64
|
+
public readonly OPENED = 1
|
|
65
|
+
public readonly HEADERS_RECEIVED = 2
|
|
66
|
+
public readonly LOADING = 3
|
|
67
|
+
public readonly DONE = 4
|
|
68
|
+
|
|
69
|
+
/* Custom public properties */
|
|
70
|
+
public method: string
|
|
71
|
+
public url: string
|
|
72
|
+
|
|
73
|
+
/* XHR public properties */
|
|
74
|
+
public withCredentials: boolean
|
|
75
|
+
public status: number
|
|
76
|
+
public statusText: string
|
|
77
|
+
public user?: string
|
|
78
|
+
public password?: string
|
|
79
|
+
public data: string
|
|
80
|
+
public async?: boolean
|
|
81
|
+
public response: any
|
|
82
|
+
public responseText: string
|
|
83
|
+
public responseType: XMLHttpRequestResponseType
|
|
84
|
+
public responseXML: Document | null
|
|
85
|
+
public responseURL: string
|
|
86
|
+
public upload: XMLHttpRequestUpload
|
|
87
|
+
public readyState: number
|
|
88
|
+
public onreadystatechange: (this: XMLHttpRequest, ev: Event) => any =
|
|
89
|
+
null as any
|
|
90
|
+
public timeout: number
|
|
91
|
+
|
|
92
|
+
/* Events */
|
|
93
|
+
public onabort: (
|
|
94
|
+
this: XMLHttpRequestEventTarget,
|
|
95
|
+
event: ProgressEvent
|
|
96
|
+
) => any = null as any
|
|
97
|
+
public onerror: (this: XMLHttpRequestEventTarget, event: Event) => any =
|
|
98
|
+
null as any
|
|
99
|
+
public onload: (
|
|
100
|
+
this: XMLHttpRequestEventTarget,
|
|
101
|
+
event: ProgressEvent
|
|
102
|
+
) => any = null as any
|
|
103
|
+
public onloadend: (
|
|
104
|
+
this: XMLHttpRequestEventTarget,
|
|
105
|
+
event: ProgressEvent
|
|
106
|
+
) => any = null as any
|
|
107
|
+
public onloadstart: (
|
|
108
|
+
this: XMLHttpRequestEventTarget,
|
|
109
|
+
event: ProgressEvent
|
|
110
|
+
) => any = null as any
|
|
111
|
+
public onprogress: (
|
|
112
|
+
this: XMLHttpRequestEventTarget,
|
|
113
|
+
event: ProgressEvent
|
|
114
|
+
) => any = null as any
|
|
115
|
+
public ontimeout: (
|
|
116
|
+
this: XMLHttpRequestEventTarget,
|
|
117
|
+
event: ProgressEvent
|
|
118
|
+
) => any = null as any
|
|
119
|
+
|
|
120
|
+
constructor() {
|
|
121
|
+
this.url = ''
|
|
122
|
+
this.method = 'GET'
|
|
123
|
+
this.readyState = this.UNSENT
|
|
124
|
+
this.withCredentials = false
|
|
125
|
+
this.status = 200
|
|
126
|
+
this.statusText = 'OK'
|
|
127
|
+
this.data = ''
|
|
128
|
+
this.response = ''
|
|
129
|
+
this.responseType = 'text'
|
|
130
|
+
this.responseText = ''
|
|
131
|
+
this.responseXML = null
|
|
132
|
+
this.responseURL = ''
|
|
133
|
+
this.upload = {} as any
|
|
134
|
+
this.timeout = 0
|
|
135
|
+
|
|
136
|
+
this._requestHeaders = new Headers()
|
|
137
|
+
this._responseHeaders = new Headers()
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setReadyState(nextState: number): void {
|
|
141
|
+
if (nextState === this.readyState) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
debug('readyState change %d -> %d', this.readyState, nextState)
|
|
146
|
+
this.readyState = nextState
|
|
147
|
+
|
|
148
|
+
if (nextState !== this.UNSENT) {
|
|
149
|
+
debug('triggerring readystate change...')
|
|
150
|
+
this.trigger('readystatechange')
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Triggers both direct callback and attached event listeners
|
|
156
|
+
* for the given event.
|
|
157
|
+
*/
|
|
158
|
+
trigger<
|
|
159
|
+
K extends keyof (XMLHttpRequestEventTargetEventMap & {
|
|
160
|
+
readystatechange: ProgressEvent<XMLHttpRequestEventTarget>
|
|
161
|
+
})
|
|
162
|
+
>(eventName: K, options?: ProgressEventInit) {
|
|
163
|
+
debug('trigger "%s" (%d)', eventName, this.readyState)
|
|
164
|
+
debug('resolve listener for event "%s"', eventName)
|
|
165
|
+
|
|
166
|
+
// @ts-expect-error XMLHttpRequest class has no index signature.
|
|
167
|
+
const callback = this[`on${eventName}`] as XMLHttpRequestEventHandler
|
|
168
|
+
callback?.call(this, createEvent(this, eventName, options))
|
|
169
|
+
|
|
170
|
+
for (const event of this._events) {
|
|
171
|
+
if (event.name === eventName) {
|
|
172
|
+
debug(
|
|
173
|
+
'calling mock event listener "%s" (%d)',
|
|
174
|
+
eventName,
|
|
175
|
+
this.readyState
|
|
176
|
+
)
|
|
177
|
+
event.listener.call(this, createEvent(this, eventName, options))
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return this
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
reset() {
|
|
185
|
+
debug('reset')
|
|
186
|
+
|
|
187
|
+
this.setReadyState(this.UNSENT)
|
|
188
|
+
this.status = 200
|
|
189
|
+
this.statusText = 'OK'
|
|
190
|
+
this.data = ''
|
|
191
|
+
this.response = null as any
|
|
192
|
+
this.responseText = null as any
|
|
193
|
+
this.responseXML = null as any
|
|
194
|
+
|
|
195
|
+
this._requestHeaders = new Headers()
|
|
196
|
+
this._responseHeaders = new Headers()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
public async open(
|
|
200
|
+
method: string,
|
|
201
|
+
url: string,
|
|
202
|
+
async: boolean = true,
|
|
203
|
+
user?: string,
|
|
204
|
+
password?: string
|
|
205
|
+
) {
|
|
206
|
+
debug = createDebug(`XHR ${method} ${url}`)
|
|
207
|
+
debug('open', { method, url, async, user, password })
|
|
208
|
+
|
|
209
|
+
this.reset()
|
|
210
|
+
this.setReadyState(this.OPENED)
|
|
211
|
+
|
|
212
|
+
if (typeof url === 'undefined') {
|
|
213
|
+
this.url = method
|
|
214
|
+
this.method = 'GET'
|
|
215
|
+
} else {
|
|
216
|
+
this.url = url
|
|
217
|
+
this.method = method
|
|
218
|
+
this.async = async
|
|
219
|
+
this.user = user
|
|
220
|
+
this.password = password
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
public send(data?: string) {
|
|
225
|
+
debug('send %s %s', this.method, this.url)
|
|
226
|
+
|
|
227
|
+
this.data = data || ''
|
|
228
|
+
|
|
229
|
+
let url: URL
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
url = new URL(this.url)
|
|
233
|
+
} catch (error) {
|
|
234
|
+
// Assume a relative URL, if construction of a new `URL` instance fails.
|
|
235
|
+
// Since `XMLHttpRequest` always executed in a DOM-like environment,
|
|
236
|
+
// resolve the relative request URL against the current window location.
|
|
237
|
+
url = new URL(this.url, window.location.href)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
debug('request headers', this._requestHeaders)
|
|
241
|
+
|
|
242
|
+
// Create an intercepted request instance exposed to the request intercepting middleware.
|
|
243
|
+
const isoRequest: IsomorphicRequest = {
|
|
244
|
+
id: uuidv4(),
|
|
245
|
+
url,
|
|
246
|
+
method: this.method,
|
|
247
|
+
headers: this._requestHeaders,
|
|
248
|
+
credentials: this.withCredentials ? 'include' : 'omit',
|
|
249
|
+
body: this.data,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
observer.emit('request', isoRequest)
|
|
253
|
+
|
|
254
|
+
debug('awaiting mocked response...')
|
|
255
|
+
|
|
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
|
+
)
|
|
265
|
+
|
|
266
|
+
// No way to propagate the actual error message.
|
|
267
|
+
this.trigger('error')
|
|
268
|
+
this.abort()
|
|
269
|
+
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Return a mocked response, if provided in the middleware.
|
|
274
|
+
if (mockedResponse) {
|
|
275
|
+
debug('received mocked response', mockedResponse)
|
|
276
|
+
|
|
277
|
+
// Trigger a loadstart event to indicate the initialization of the fetch.
|
|
278
|
+
this.trigger('loadstart')
|
|
279
|
+
|
|
280
|
+
this.status = mockedResponse.status || 200
|
|
281
|
+
this.statusText = mockedResponse.statusText || 'OK'
|
|
282
|
+
this._responseHeaders = mockedResponse.headers
|
|
283
|
+
? objectToHeaders(mockedResponse.headers)
|
|
284
|
+
: new Headers()
|
|
285
|
+
|
|
286
|
+
debug('set response status', this.status, this.statusText)
|
|
287
|
+
debug('set response headers', this._responseHeaders)
|
|
288
|
+
|
|
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)
|
|
293
|
+
|
|
294
|
+
debug('response type', this.responseType)
|
|
295
|
+
this.response = this.getResponseBody(mockedResponse.body)
|
|
296
|
+
this.responseText = mockedResponse.body || ''
|
|
297
|
+
this.responseXML = this.getResponseXML()
|
|
298
|
+
|
|
299
|
+
debug('set response body', this.response)
|
|
300
|
+
|
|
301
|
+
if (mockedResponse.body && this.response) {
|
|
302
|
+
this.setReadyState(this.LOADING)
|
|
303
|
+
|
|
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)
|
|
307
|
+
|
|
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
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Explicitly mark the request as done so its response never hangs.
|
|
317
|
+
* @see https://github.com/mswjs/interceptors/issues/13
|
|
318
|
+
*/
|
|
319
|
+
this.setReadyState(this.DONE)
|
|
320
|
+
|
|
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
|
|
340
|
+
)
|
|
341
|
+
|
|
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
|
+
})
|
|
386
|
+
|
|
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)
|
|
392
|
+
|
|
393
|
+
if (this.async) {
|
|
394
|
+
originalRequest.timeout = this.timeout
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
debug('send', this.data)
|
|
398
|
+
originalRequest.send(this.data)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
public abort() {
|
|
405
|
+
debug('abort')
|
|
406
|
+
|
|
407
|
+
if (this.readyState > this.UNSENT && this.readyState < this.DONE) {
|
|
408
|
+
this.setReadyState(this.UNSENT)
|
|
409
|
+
this.trigger('abort')
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
dispatchEvent() {
|
|
414
|
+
return false
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
public setRequestHeader(name: string, value: string) {
|
|
418
|
+
debug('set request header "%s" to "%s"', name, value)
|
|
419
|
+
this._requestHeaders.append(name, value)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
public getResponseHeader(name: string): string | null {
|
|
423
|
+
debug('get response header "%s"', name)
|
|
424
|
+
|
|
425
|
+
if (this.readyState < this.HEADERS_RECEIVED) {
|
|
426
|
+
debug(
|
|
427
|
+
'cannot return a header: headers not received (state: %s)',
|
|
428
|
+
this.readyState
|
|
429
|
+
)
|
|
430
|
+
return null
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const headerValue = this._responseHeaders.get(name)
|
|
434
|
+
|
|
435
|
+
debug(
|
|
436
|
+
'resolved response header "%s" to "%s"',
|
|
437
|
+
name,
|
|
438
|
+
headerValue,
|
|
439
|
+
this._responseHeaders
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
return headerValue
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
public getAllResponseHeaders(): string {
|
|
446
|
+
debug('get all response headers')
|
|
447
|
+
|
|
448
|
+
if (this.readyState < this.HEADERS_RECEIVED) {
|
|
449
|
+
debug(
|
|
450
|
+
'cannot return headers: headers not received (state: %s)',
|
|
451
|
+
this.readyState
|
|
452
|
+
)
|
|
453
|
+
return ''
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return headersToString(this._responseHeaders)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
public addEventListener<
|
|
460
|
+
K extends keyof InternalXMLHttpRequestEventTargetEventMap
|
|
461
|
+
>(name: K, listener: XMLHttpRequestEventHandler) {
|
|
462
|
+
debug('addEventListener', name, listener)
|
|
463
|
+
this._events.push({
|
|
464
|
+
name,
|
|
465
|
+
listener,
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
public removeEventListener<K extends keyof XMLHttpRequestEventMap>(
|
|
470
|
+
name: K,
|
|
471
|
+
listener: (event?: XMLHttpRequestEventMap[K]) => void
|
|
472
|
+
): void {
|
|
473
|
+
debug('removeEventListener', name, listener)
|
|
474
|
+
this._events = this._events.filter((storedEvent) => {
|
|
475
|
+
return storedEvent.name !== name && storedEvent.listener !== listener
|
|
476
|
+
})
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
public overrideMimeType() {}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Resolves the response based on the `responseType` value.
|
|
483
|
+
*/
|
|
484
|
+
getResponseBody(body: string | undefined) {
|
|
485
|
+
// Handle an improperly set "null" value of the mocked response body.
|
|
486
|
+
const textBody = body ?? ''
|
|
487
|
+
debug('coerced response body to', textBody)
|
|
488
|
+
|
|
489
|
+
switch (this.responseType) {
|
|
490
|
+
case 'json': {
|
|
491
|
+
debug('resolving response body as JSON')
|
|
492
|
+
return parseJson(textBody)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
case 'blob': {
|
|
496
|
+
const blobType =
|
|
497
|
+
this.getResponseHeader('content-type') || 'text/plain'
|
|
498
|
+
debug('resolving response body as Blob', { type: blobType })
|
|
499
|
+
|
|
500
|
+
return new Blob([textBody], {
|
|
501
|
+
type: blobType,
|
|
502
|
+
})
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
case 'arraybuffer': {
|
|
506
|
+
debug('resolving response body as ArrayBuffer')
|
|
507
|
+
const arrayBuffer = bufferFrom(textBody)
|
|
508
|
+
return arrayBuffer
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
default:
|
|
512
|
+
return textBody
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
getResponseXML() {
|
|
517
|
+
const contentType = this.getResponseHeader('Content-Type')
|
|
518
|
+
if (contentType === 'application/xml' || contentType === 'text/xml') {
|
|
519
|
+
return new DOMParser().parseFromString(this.responseText, contentType)
|
|
520
|
+
}
|
|
521
|
+
return null
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Propagates mock XMLHttpRequest instance callbacks
|
|
526
|
+
* to the given XMLHttpRequest instance.
|
|
527
|
+
*/
|
|
528
|
+
propagateCallbacks(request: XMLHttpRequest) {
|
|
529
|
+
request.onabort = this.abort
|
|
530
|
+
request.onerror = this.onerror
|
|
531
|
+
request.ontimeout = this.ontimeout
|
|
532
|
+
request.onload = this.onload
|
|
533
|
+
request.onloadstart = this.onloadstart
|
|
534
|
+
request.onloadend = this.onloadend
|
|
535
|
+
request.onprogress = this.onprogress
|
|
536
|
+
request.onreadystatechange = this.onreadystatechange
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Propagates the mock XMLHttpRequest instance listeners
|
|
541
|
+
* to the given XMLHttpRequest instance.
|
|
542
|
+
*/
|
|
543
|
+
propagateListeners(request: XMLHttpRequest) {
|
|
544
|
+
debug(
|
|
545
|
+
'propagating request listeners (%d) to the original request',
|
|
546
|
+
this._events.length,
|
|
547
|
+
this._events
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
this._events.forEach(({ name, listener }) => {
|
|
551
|
+
request.addEventListener(name, listener)
|
|
552
|
+
})
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
propagateHeaders(request: XMLHttpRequest, headers: Headers) {
|
|
556
|
+
debug('propagating request headers to the original request', headers)
|
|
557
|
+
|
|
558
|
+
// Preserve the request headers casing.
|
|
559
|
+
Object.entries(headers.raw()).forEach(([name, value]) => {
|
|
560
|
+
debug('setting "%s" (%s) header on the original request', name, value)
|
|
561
|
+
request.setRequestHeader(name, value)
|
|
562
|
+
})
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Interceptor } from '../../createInterceptor'
|
|
2
|
+
import { createXMLHttpRequestOverride } from './XMLHttpRequestOverride'
|
|
3
|
+
|
|
4
|
+
const debug = require('debug')('XHR')
|
|
5
|
+
|
|
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
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Intercepts requests issued via `XMLHttpRequest`.
|
|
13
|
+
*/
|
|
14
|
+
export const interceptXMLHttpRequest: Interceptor = (observer, resolver) => {
|
|
15
|
+
if (pureXMLHttpRequest) {
|
|
16
|
+
debug('patching "XMLHttpRequest" module...')
|
|
17
|
+
|
|
18
|
+
const XMLHttpRequestOverride = createXMLHttpRequestOverride({
|
|
19
|
+
pureXMLHttpRequest,
|
|
20
|
+
observer,
|
|
21
|
+
resolver,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
window.XMLHttpRequest = XMLHttpRequestOverride
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return () => {
|
|
28
|
+
if (pureXMLHttpRequest) {
|
|
29
|
+
debug('restoring modules...')
|
|
30
|
+
|
|
31
|
+
window.XMLHttpRequest = pureXMLHttpRequest
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
|
6
|
+
|
|
7
|
+
public type: string = ''
|
|
8
|
+
public srcElement: EventTarget | null = null
|
|
9
|
+
public target: EventTarget | null
|
|
10
|
+
public currentTarget: EventTarget | null = null
|
|
11
|
+
public eventPhase: number = 0
|
|
12
|
+
public timeStamp: number
|
|
13
|
+
public isTrusted: boolean = true
|
|
14
|
+
public composed: boolean = false
|
|
15
|
+
public cancelable: boolean = true
|
|
16
|
+
public defaultPrevented: boolean = false
|
|
17
|
+
public bubbles: boolean = true
|
|
18
|
+
public lengthComputable: boolean = true
|
|
19
|
+
public loaded: number = 0
|
|
20
|
+
public total: number = 0
|
|
21
|
+
|
|
22
|
+
cancelBubble: boolean = false
|
|
23
|
+
returnValue: boolean = true
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
type: string,
|
|
27
|
+
options?: { target: EventTarget; currentTarget: EventTarget }
|
|
28
|
+
) {
|
|
29
|
+
this.type = type
|
|
30
|
+
this.target = options?.target || null
|
|
31
|
+
this.currentTarget = options?.currentTarget || null
|
|
32
|
+
this.timeStamp = Date.now()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public composedPath(): EventTarget[] {
|
|
36
|
+
return []
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public initEvent(type: string, bubbles?: boolean, cancelable?: boolean) {
|
|
40
|
+
this.type = type
|
|
41
|
+
this.bubbles = !!bubbles
|
|
42
|
+
this.cancelable = !!cancelable
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public preventDefault() {
|
|
46
|
+
this.defaultPrevented = true
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public stopPropagation() {}
|
|
50
|
+
public stopImmediatePropagation() {}
|
|
51
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { EventPolyfill } from './EventPolyfill'
|
|
2
|
+
|
|
3
|
+
export class ProgressEventPolyfill extends EventPolyfill {
|
|
4
|
+
readonly lengthComputable: boolean
|
|
5
|
+
readonly composed: boolean
|
|
6
|
+
readonly loaded: number
|
|
7
|
+
readonly total: number
|
|
8
|
+
|
|
9
|
+
constructor(type: string, init?: ProgressEventInit) {
|
|
10
|
+
super(type)
|
|
11
|
+
|
|
12
|
+
this.lengthComputable = init?.lengthComputable || false
|
|
13
|
+
this.composed = init?.composed || false
|
|
14
|
+
this.loaded = init?.loaded || 0
|
|
15
|
+
this.total = init?.total || 0
|
|
16
|
+
}
|
|
17
|
+
}
|