@mswjs/interceptors 0.25.1 → 0.25.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.
- package/lib/browser/Interceptor-8d5fd4c6.d.ts +86 -0
- package/lib/browser/{chunk-ANLPTCZ5.mjs → chunk-AN3YI76R.mjs} +33 -26
- package/lib/browser/{chunk-4CFMDU7Z.js → chunk-CWVY2E3W.js} +20 -134
- package/lib/browser/{chunk-OSIUQA4X.js → chunk-FFBQOFWV.js} +38 -31
- package/lib/browser/{chunk-VMXB5F2J.mjs → chunk-HXJPKJY3.mjs} +25 -15
- package/lib/browser/{chunk-DBFLI5DJ.js → chunk-KITNLK66.js} +30 -20
- package/lib/browser/chunk-KK6APRON.mjs +58 -0
- package/lib/browser/{chunk-GXJLJMOT.mjs → chunk-QPMXOLDO.mjs} +21 -135
- package/lib/browser/chunk-X3NRJIZW.js +58 -0
- package/lib/browser/index.d.ts +7 -3
- package/lib/browser/index.js +24 -5
- package/lib/browser/index.mjs +22 -3
- package/lib/browser/interceptors/XMLHttpRequest/index.d.ts +4 -3
- package/lib/browser/interceptors/XMLHttpRequest/index.js +4 -4
- package/lib/browser/interceptors/XMLHttpRequest/index.mjs +3 -3
- package/lib/browser/interceptors/fetch/index.d.ts +2 -1
- package/lib/browser/interceptors/fetch/index.js +4 -4
- package/lib/browser/interceptors/fetch/index.mjs +3 -3
- package/lib/browser/presets/browser.d.ts +2 -1
- package/lib/browser/presets/browser.js +6 -6
- package/lib/browser/presets/browser.mjs +4 -4
- package/lib/node/{BatchInterceptor-c841b068.d.ts → BatchInterceptor-9785c567.d.ts} +5 -2
- package/lib/node/Interceptor-7a701c1f.d.ts +86 -0
- package/lib/node/RemoteHttpInterceptor.d.ts +3 -2
- package/lib/node/RemoteHttpInterceptor.js +19 -18
- package/lib/node/RemoteHttpInterceptor.mjs +15 -14
- package/lib/node/chunk-3IYIKC3X.mjs +6 -0
- package/lib/node/{chunk-XYZRP5S2.js → chunk-3XFLRXRY.js} +20 -134
- package/lib/node/chunk-5PTPJLB7.js +58 -0
- package/lib/node/{chunk-HSCXCLVT.mjs → chunk-FB53TMYN.mjs} +33 -26
- package/lib/node/{chunk-RGYCLCLK.mjs → chunk-GM3YBSM3.mjs} +21 -135
- package/lib/node/{chunk-E6YC337Q.js → chunk-JCWVLTP7.js} +35 -28
- package/lib/node/{chunk-OL7OR4RL.mjs → chunk-MCO3RLQC.mjs} +48 -26
- package/lib/node/{chunk-3MYUI4B2.js → chunk-NCHFM2TB.js} +50 -28
- package/lib/node/chunk-OGN3ZR35.js +6 -0
- package/lib/node/{chunk-VS3GJPUE.mjs → chunk-UBEFEZXT.mjs} +22 -3
- package/lib/node/{chunk-MVPEJK4V.js → chunk-UF7QIAQ5.js} +23 -4
- package/lib/node/chunk-YQGTMMOZ.mjs +58 -0
- package/lib/node/index.d.ts +3 -2
- package/lib/node/index.js +3 -3
- package/lib/node/index.mjs +2 -2
- package/lib/node/interceptors/ClientRequest/index.d.ts +4 -3
- package/lib/node/interceptors/ClientRequest/index.js +5 -4
- package/lib/node/interceptors/ClientRequest/index.mjs +4 -3
- package/lib/node/interceptors/XMLHttpRequest/index.d.ts +4 -3
- package/lib/node/interceptors/XMLHttpRequest/index.js +5 -4
- package/lib/node/interceptors/XMLHttpRequest/index.mjs +4 -3
- package/lib/node/interceptors/fetch/index.d.ts +2 -1
- package/lib/node/interceptors/fetch/index.js +27 -17
- package/lib/node/interceptors/fetch/index.mjs +25 -15
- package/lib/node/presets/node.d.ts +3 -2
- package/lib/node/presets/node.js +7 -6
- package/lib/node/presets/node.mjs +5 -4
- package/package.json +2 -2
- package/src/BatchInterceptor.test.ts +141 -0
- package/src/BatchInterceptor.ts +38 -4
- package/src/Interceptor.test.ts +46 -0
- package/src/Interceptor.ts +35 -16
- package/src/RemoteHttpInterceptor.ts +11 -9
- package/src/interceptors/ClientRequest/NodeClientRequest.test.ts +10 -10
- package/src/interceptors/ClientRequest/NodeClientRequest.ts +80 -43
- package/src/interceptors/ClientRequest/http.get.ts +3 -1
- package/src/interceptors/ClientRequest/http.request.ts +3 -1
- package/src/interceptors/ClientRequest/index.test.ts +2 -3
- package/src/interceptors/ClientRequest/index.ts +2 -2
- package/src/interceptors/ClientRequest/utils/createRequest.test.ts +2 -2
- package/src/interceptors/ClientRequest/utils/createResponse.test.ts +23 -0
- package/src/interceptors/ClientRequest/utils/createResponse.ts +18 -13
- package/src/interceptors/XMLHttpRequest/XMLHttpRequestProxy.ts +29 -25
- package/src/interceptors/XMLHttpRequest/index.ts +2 -2
- package/src/interceptors/XMLHttpRequest/utils/createResponse.ts +4 -2
- package/src/interceptors/fetch/index.ts +26 -13
- package/src/utils/RequestController.ts +21 -0
- package/src/utils/emitAsync.ts +25 -0
- package/src/utils/responseUtils.ts +5 -0
- package/src/utils/toInteractiveRequest.ts +17 -23
- package/lib/browser/Interceptor-0a020bc4.d.ts +0 -116
- package/lib/browser/chunk-PCFJD76X.js +0 -64
- package/lib/browser/chunk-RT3ATOJH.mjs +0 -64
- package/lib/node/Interceptor-738f79c5.d.ts +0 -116
- package/lib/node/chunk-STA6QBYM.mjs +0 -64
- package/lib/node/chunk-ZJOF5MEZ.js +0 -64
- package/src/utils/AsyncEventEmitter.test.ts +0 -102
- package/src/utils/AsyncEventEmitter.ts +0 -193
- package/src/utils/createLazyCallback.ts +0 -49
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ClientRequest, IncomingMessage } from 'http'
|
|
2
2
|
import type { Logger } from '@open-draft/logger'
|
|
3
3
|
import { until } from '@open-draft/until'
|
|
4
|
+
import { DeferredPromise } from '@open-draft/deferred-promise'
|
|
4
5
|
import type { ClientRequestEmitter } from '.'
|
|
5
6
|
import {
|
|
6
7
|
ClientRequestEndCallback,
|
|
@@ -17,7 +18,7 @@ import { createResponse } from './utils/createResponse'
|
|
|
17
18
|
import { createRequest } from './utils/createRequest'
|
|
18
19
|
import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
|
|
19
20
|
import { uuidv4 } from '../../utils/uuid'
|
|
20
|
-
import {
|
|
21
|
+
import { emitAsync } from '../../utils/emitAsync'
|
|
21
22
|
|
|
22
23
|
export type Protocol = 'http' | 'https'
|
|
23
24
|
|
|
@@ -138,7 +139,17 @@ export class NodeClientRequest extends ClientRequest {
|
|
|
138
139
|
this.writeRequestBodyChunk(chunk, encoding || undefined)
|
|
139
140
|
|
|
140
141
|
const capturedRequest = createRequest(this)
|
|
141
|
-
const interactiveRequest =
|
|
142
|
+
const { interactiveRequest, requestController } =
|
|
143
|
+
toInteractiveRequest(capturedRequest)
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @todo Remove this modification of the original request
|
|
147
|
+
* and expose the controller alongside it in the "request"
|
|
148
|
+
* listener argument.
|
|
149
|
+
*/
|
|
150
|
+
Object.defineProperty(capturedRequest, 'respondWith', {
|
|
151
|
+
value: requestController.respondWith.bind(requestController),
|
|
152
|
+
})
|
|
142
153
|
|
|
143
154
|
// Prevent handling this request if it has already been handled
|
|
144
155
|
// in another (parent) interceptor (like XMLHttpRequest -> ClientRequest).
|
|
@@ -149,34 +160,46 @@ export class NodeClientRequest extends ClientRequest {
|
|
|
149
160
|
return this.passthrough(chunk, encoding, callback)
|
|
150
161
|
}
|
|
151
162
|
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
163
|
+
// Add the last "request" listener that always resolves
|
|
164
|
+
// the pending response Promise. This way if the consumer
|
|
165
|
+
// hasn't handled the request themselves, we will prevent
|
|
166
|
+
// the response Promise from pending indefinitely.
|
|
167
|
+
this.emitter.once('request', ({ requestId: pendingRequestId }) => {
|
|
168
|
+
/**
|
|
169
|
+
* @note Ignore request events emitted by irrelevant
|
|
170
|
+
* requests. This happens when response patching.
|
|
171
|
+
*/
|
|
172
|
+
if (pendingRequestId !== requestId) {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (requestController.responsePromise.state === 'pending') {
|
|
177
|
+
this.logger.info(
|
|
178
|
+
'request has not been handled in listeners, executing fail-safe listener...'
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
requestController.responsePromise.resolve(undefined)
|
|
182
|
+
}
|
|
161
183
|
})
|
|
162
184
|
|
|
163
185
|
// Execute the resolver Promise like a side-effect.
|
|
164
186
|
// Node.js 16 forces "ClientRequest.end" to be synchronous and return "this".
|
|
165
187
|
until(async () => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
* This prevents extraneous parallel request from blocking the resolution
|
|
172
|
-
* of another, unrelated request. For example, during response patching,
|
|
173
|
-
* when request resolution is nested.
|
|
174
|
-
*/
|
|
175
|
-
return pendingRequestId === requestId
|
|
176
|
-
}
|
|
188
|
+
// Notify the interceptor about the request.
|
|
189
|
+
// This will call any "request" listeners the users have.
|
|
190
|
+
this.logger.info(
|
|
191
|
+
'emitting the "request" event for %d listener(s)...',
|
|
192
|
+
this.emitter.listenerCount('request')
|
|
177
193
|
)
|
|
178
194
|
|
|
179
|
-
|
|
195
|
+
await emitAsync(this.emitter, 'request', {
|
|
196
|
+
request: interactiveRequest,
|
|
197
|
+
requestId,
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
this.logger.info('all "request" listeners done!')
|
|
201
|
+
|
|
202
|
+
const mockedResponse = await requestController.responsePromise
|
|
180
203
|
this.logger.info('event.respondWith called with:', mockedResponse)
|
|
181
204
|
|
|
182
205
|
return mockedResponse
|
|
@@ -203,6 +226,8 @@ export class NodeClientRequest extends ClientRequest {
|
|
|
203
226
|
'encountered resolver exception, aborting request...',
|
|
204
227
|
resolverResult.error
|
|
205
228
|
)
|
|
229
|
+
|
|
230
|
+
this.destroyed = true
|
|
206
231
|
this.emit('error', resolverResult.error)
|
|
207
232
|
this.terminate()
|
|
208
233
|
|
|
@@ -212,7 +237,17 @@ export class NodeClientRequest extends ClientRequest {
|
|
|
212
237
|
const mockedResponse = resolverResult.data
|
|
213
238
|
|
|
214
239
|
if (mockedResponse) {
|
|
215
|
-
this.logger.info(
|
|
240
|
+
this.logger.info(
|
|
241
|
+
'received mocked response:',
|
|
242
|
+
mockedResponse.status,
|
|
243
|
+
mockedResponse.statusText
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @note Ignore this request being destroyed by TLS in Node.js
|
|
248
|
+
* due to connection errors.
|
|
249
|
+
*/
|
|
250
|
+
this.destroyed = false
|
|
216
251
|
|
|
217
252
|
// Handle mocked "Response.error" network error responses.
|
|
218
253
|
if (mockedResponse.type === 'error') {
|
|
@@ -431,10 +466,29 @@ export class NodeClientRequest extends ClientRequest {
|
|
|
431
466
|
}
|
|
432
467
|
this.logger.info('mocked response headers ready:', headers)
|
|
433
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Set the internal "res" property to the mocked "OutgoingMessage"
|
|
471
|
+
* to make the "ClientRequest" instance think there's data received
|
|
472
|
+
* from the socket.
|
|
473
|
+
* @see https://github.com/nodejs/node/blob/9c405f2591f5833d0247ed0fafdcd68c5b14ce7a/lib/_http_client.js#L501
|
|
474
|
+
*
|
|
475
|
+
* Set the response immediately so the interceptor could stream data
|
|
476
|
+
* chunks to the request client as they come in.
|
|
477
|
+
*/
|
|
478
|
+
// @ts-ignore
|
|
479
|
+
this.res = this.response
|
|
480
|
+
this.emit('response', this.response)
|
|
481
|
+
|
|
434
482
|
const isResponseStreamFinished = new DeferredPromise<void>()
|
|
435
483
|
|
|
436
484
|
const finishResponseStream = () => {
|
|
437
485
|
this.logger.info('finished response stream!')
|
|
486
|
+
|
|
487
|
+
// Push "null" to indicate that the response body is complete
|
|
488
|
+
// and shouldn't be written to anymore.
|
|
489
|
+
this.response.push(null)
|
|
490
|
+
this.response.complete = true
|
|
491
|
+
|
|
438
492
|
isResponseStreamFinished.resolve()
|
|
439
493
|
}
|
|
440
494
|
|
|
@@ -458,29 +512,12 @@ export class NodeClientRequest extends ClientRequest {
|
|
|
458
512
|
finishResponseStream()
|
|
459
513
|
}
|
|
460
514
|
|
|
461
|
-
/**
|
|
462
|
-
* Set the internal "res" property to the mocked "OutgoingMessage"
|
|
463
|
-
* to make the "ClientRequest" instance think there's data received
|
|
464
|
-
* from the socket.
|
|
465
|
-
* @see https://github.com/nodejs/node/blob/9c405f2591f5833d0247ed0fafdcd68c5b14ce7a/lib/_http_client.js#L501
|
|
466
|
-
*
|
|
467
|
-
* Set the response immediately so the interceptor could stream data
|
|
468
|
-
* chunks to the request client as they come in.
|
|
469
|
-
*/
|
|
470
|
-
// @ts-ignore
|
|
471
|
-
this.res = this.response
|
|
472
|
-
this.emit('response', this.response)
|
|
473
|
-
|
|
474
515
|
isResponseStreamFinished.then(() => {
|
|
475
516
|
this.logger.info('finalizing response...')
|
|
476
|
-
|
|
477
|
-
// Push "null" to indicate that the response body is complete
|
|
478
|
-
// and shouldn't be written to anymore.
|
|
479
|
-
this.response.push(null)
|
|
480
|
-
this.response.complete = true
|
|
481
517
|
this.response.emit('end')
|
|
482
|
-
|
|
483
518
|
this.terminate()
|
|
519
|
+
|
|
520
|
+
this.logger.info('request complete!')
|
|
484
521
|
})
|
|
485
522
|
}
|
|
486
523
|
|
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
} from './utils/normalizeClientRequestArgs'
|
|
11
11
|
|
|
12
12
|
export function get(protocol: Protocol, options: NodeClientOptions) {
|
|
13
|
-
return (
|
|
13
|
+
return function interceptorsHttpGet(
|
|
14
|
+
...args: ClientRequestArgs
|
|
15
|
+
): ClientRequest {
|
|
14
16
|
const clientRequestArgs = normalizeClientRequestArgs(
|
|
15
17
|
`${protocol}:`,
|
|
16
18
|
...args
|
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
const logger = new Logger('http request')
|
|
14
14
|
|
|
15
15
|
export function request(protocol: Protocol, options: NodeClientOptions) {
|
|
16
|
-
return (
|
|
16
|
+
return function interceptorsHttpRequest(
|
|
17
|
+
...args: ClientRequestArgs
|
|
18
|
+
): ClientRequest {
|
|
17
19
|
logger.info('request call (protocol "%s"):', protocol, args)
|
|
18
20
|
|
|
19
21
|
const clientRequestArgs = normalizeClientRequestArgs(
|
|
@@ -57,14 +57,13 @@ it('forbids calling "respondWith" multiple times for the same request', async ()
|
|
|
57
57
|
expect(response.statusMessage).toBe('')
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
-
|
|
61
60
|
it('abort the request if the abort signal is emitted', async () => {
|
|
62
61
|
const requestUrl = httpServer.http.url('/')
|
|
63
62
|
|
|
64
63
|
const requestEmitted = new DeferredPromise<void>()
|
|
65
64
|
interceptor.on('request', async function delayedResponse({ request }) {
|
|
66
65
|
requestEmitted.resolve()
|
|
67
|
-
await sleep(
|
|
66
|
+
await sleep(10_000)
|
|
68
67
|
request.respondWith(new Response())
|
|
69
68
|
})
|
|
70
69
|
|
|
@@ -76,7 +75,7 @@ it('abort the request if the abort signal is emitted', async () => {
|
|
|
76
75
|
abortController.abort()
|
|
77
76
|
|
|
78
77
|
const requestAborted = new DeferredPromise<void>()
|
|
79
|
-
request.on('error', function(err) {
|
|
78
|
+
request.on('error', function (err) {
|
|
80
79
|
expect(err.name).toEqual('AbortError')
|
|
81
80
|
requestAborted.resolve()
|
|
82
81
|
})
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import http from 'http'
|
|
2
2
|
import https from 'https'
|
|
3
|
+
import type { Emitter } from 'strict-event-emitter'
|
|
3
4
|
import { HttpRequestEventMap } from '../../glossary'
|
|
4
5
|
import { Interceptor } from '../../Interceptor'
|
|
5
|
-
import { AsyncEventEmitter } from '../../utils/AsyncEventEmitter'
|
|
6
6
|
import { get } from './http.get'
|
|
7
7
|
import { request } from './http.request'
|
|
8
8
|
import { NodeClientOptions, Protocol } from './NodeClientRequest'
|
|
9
9
|
|
|
10
|
-
export type ClientRequestEmitter =
|
|
10
|
+
export type ClientRequestEmitter = Emitter<HttpRequestEventMap>
|
|
11
11
|
|
|
12
12
|
export type ClientRequestModules = Map<Protocol, typeof http | typeof https>
|
|
13
13
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { it, expect } from 'vitest'
|
|
2
2
|
import { Logger } from '@open-draft/logger'
|
|
3
3
|
import { HttpRequestEventMap } from '../../..'
|
|
4
|
-
import { AsyncEventEmitter } from '../../../utils/AsyncEventEmitter'
|
|
5
4
|
import { NodeClientRequest } from '../NodeClientRequest'
|
|
6
5
|
import { createRequest } from './createRequest'
|
|
6
|
+
import { Emitter } from 'strict-event-emitter'
|
|
7
7
|
|
|
8
|
-
const emitter = new
|
|
8
|
+
const emitter = new Emitter<HttpRequestEventMap>()
|
|
9
9
|
const logger = new Logger('test')
|
|
10
10
|
|
|
11
11
|
it('creates a fetch Request with a JSON body', async () => {
|
|
@@ -2,6 +2,7 @@ import { it, expect } from 'vitest'
|
|
|
2
2
|
import { Socket } from 'net'
|
|
3
3
|
import * as http from 'http'
|
|
4
4
|
import { createResponse } from './createResponse'
|
|
5
|
+
import { responseStatusCodesWithoutBody } from '../../../utils/responseUtils'
|
|
5
6
|
|
|
6
7
|
it('creates a fetch api response from http incoming message', async () => {
|
|
7
8
|
const message = new http.IncomingMessage(new Socket())
|
|
@@ -20,3 +21,25 @@ it('creates a fetch api response from http incoming message', async () => {
|
|
|
20
21
|
expect(response.headers.get('content-type')).toBe('application/json')
|
|
21
22
|
expect(await response.json()).toEqual({ firstName: 'John' })
|
|
22
23
|
})
|
|
24
|
+
|
|
25
|
+
it.each(responseStatusCodesWithoutBody)(
|
|
26
|
+
'ignores message body for %i response status',
|
|
27
|
+
(responseStatus) => {
|
|
28
|
+
const message = new http.IncomingMessage(new Socket())
|
|
29
|
+
message.statusCode = responseStatus
|
|
30
|
+
|
|
31
|
+
const response = createResponse(message)
|
|
32
|
+
|
|
33
|
+
// These chunks will be ignored: this response
|
|
34
|
+
// cannot have body. We don't forward this error to
|
|
35
|
+
// the consumer because it's us who converts the
|
|
36
|
+
// internal stream to a Fetch API Response instance.
|
|
37
|
+
// Consumers will rely on the Response API when constructing
|
|
38
|
+
// mocked responses.
|
|
39
|
+
message.emit('data', Buffer.from('hello'))
|
|
40
|
+
message.emit('end')
|
|
41
|
+
|
|
42
|
+
expect(response.status).toBe(responseStatus)
|
|
43
|
+
expect(response.body).toBe(null)
|
|
44
|
+
}
|
|
45
|
+
)
|
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
import type { IncomingHttpHeaders, IncomingMessage } from 'http'
|
|
2
|
+
import { responseStatusCodesWithoutBody } from '../../../utils/responseUtils'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Creates a Fetch API `Response` instance from the given
|
|
5
6
|
* `http.IncomingMessage` instance.
|
|
6
7
|
*/
|
|
7
8
|
export function createResponse(message: IncomingMessage): Response {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
const responseBodyOrNull = responseStatusCodesWithoutBody.includes(
|
|
10
|
+
message.statusCode || 200
|
|
11
|
+
)
|
|
12
|
+
? null
|
|
13
|
+
: new ReadableStream({
|
|
14
|
+
start(controller) {
|
|
15
|
+
message.on('data', (chunk) => controller.enqueue(chunk))
|
|
16
|
+
message.on('end', () => controller.close())
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @todo Should also listen to the "error" on the message
|
|
20
|
+
* and forward it to the controller. Otherwise the stream
|
|
21
|
+
* will pend indefinitely.
|
|
22
|
+
*/
|
|
23
|
+
},
|
|
24
|
+
})
|
|
20
25
|
|
|
21
|
-
return new Response(
|
|
26
|
+
return new Response(responseBodyOrNull, {
|
|
22
27
|
status: message.statusCode,
|
|
23
28
|
statusText: message.statusMessage,
|
|
24
29
|
headers: createHeadersFromIncomingHttpHeaders(message.headers),
|
|
@@ -2,6 +2,7 @@ import { until } from '@open-draft/until'
|
|
|
2
2
|
import type { Logger } from '@open-draft/logger'
|
|
3
3
|
import { XMLHttpRequestEmitter } from '.'
|
|
4
4
|
import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
|
|
5
|
+
import { emitAsync } from '../../utils/emitAsync'
|
|
5
6
|
import { XMLHttpRequestController } from './XMLHttpRequestController'
|
|
6
7
|
|
|
7
8
|
export interface XMLHttpRequestProxyOptions {
|
|
@@ -42,38 +43,41 @@ export function createXMLHttpRequestProxy({
|
|
|
42
43
|
)
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
const
|
|
46
|
+
const xhrRequestController = new XMLHttpRequestController(
|
|
46
47
|
originalRequest,
|
|
47
48
|
logger
|
|
48
49
|
)
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.logger.info(
|
|
55
|
-
'emitting the "request" event for %s listener(s)...',
|
|
56
|
-
emitter.listenerCount('request')
|
|
57
|
-
)
|
|
58
|
-
emitter.emit('request', {
|
|
59
|
-
request: interactiveRequest,
|
|
60
|
-
requestId,
|
|
61
|
-
})
|
|
51
|
+
xhrRequestController.onRequest = async function ({ request, requestId }) {
|
|
52
|
+
const { interactiveRequest, requestController } =
|
|
53
|
+
toInteractiveRequest(request)
|
|
62
54
|
|
|
63
55
|
this.logger.info('awaiting mocked response...')
|
|
64
56
|
|
|
57
|
+
emitter.once('request', ({ requestId: pendingRequestId }) => {
|
|
58
|
+
if (pendingRequestId !== requestId) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (requestController.responsePromise.state === 'pending') {
|
|
63
|
+
requestController.respondWith(undefined)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
65
67
|
const resolverResult = await until(async () => {
|
|
66
|
-
|
|
67
|
-
'request',
|
|
68
|
-
(
|
|
69
|
-
return pendingRequestId === requestId
|
|
70
|
-
}
|
|
68
|
+
this.logger.info(
|
|
69
|
+
'emitting the "request" event for %s listener(s)...',
|
|
70
|
+
emitter.listenerCount('request')
|
|
71
71
|
)
|
|
72
72
|
|
|
73
|
+
await emitAsync(emitter, 'request', {
|
|
74
|
+
request: interactiveRequest,
|
|
75
|
+
requestId,
|
|
76
|
+
})
|
|
77
|
+
|
|
73
78
|
this.logger.info('all "request" listeners settled!')
|
|
74
79
|
|
|
75
|
-
const
|
|
76
|
-
await interactiveRequest.respondWith.invoked()
|
|
80
|
+
const mockedResponse = await requestController.responsePromise
|
|
77
81
|
|
|
78
82
|
this.logger.info('event.respondWith called with:', mockedResponse)
|
|
79
83
|
|
|
@@ -91,7 +95,7 @@ export function createXMLHttpRequestProxy({
|
|
|
91
95
|
* since not all consumers are expecting to handle errors.
|
|
92
96
|
* If they don't, this error will be swallowed.
|
|
93
97
|
*/
|
|
94
|
-
|
|
98
|
+
xhrRequestController.errorWith(resolverResult.error)
|
|
95
99
|
return
|
|
96
100
|
}
|
|
97
101
|
|
|
@@ -109,11 +113,11 @@ export function createXMLHttpRequestProxy({
|
|
|
109
113
|
'received a network error response, rejecting the request promise...'
|
|
110
114
|
)
|
|
111
115
|
|
|
112
|
-
|
|
116
|
+
xhrRequestController.errorWith(new TypeError('Network error'))
|
|
113
117
|
return
|
|
114
118
|
}
|
|
115
119
|
|
|
116
|
-
return
|
|
120
|
+
return xhrRequestController.respondWith(mockedResponse)
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
this.logger.info(
|
|
@@ -121,7 +125,7 @@ export function createXMLHttpRequestProxy({
|
|
|
121
125
|
)
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
|
|
128
|
+
xhrRequestController.onResponse = async function ({
|
|
125
129
|
response,
|
|
126
130
|
isMockedResponse,
|
|
127
131
|
request,
|
|
@@ -143,7 +147,7 @@ export function createXMLHttpRequestProxy({
|
|
|
143
147
|
// Return the proxied request from the controller
|
|
144
148
|
// so that the controller can react to the consumer's interactions
|
|
145
149
|
// with this request (opening/sending/etc).
|
|
146
|
-
return
|
|
150
|
+
return xhrRequestController.request
|
|
147
151
|
},
|
|
148
152
|
})
|
|
149
153
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { invariant } from 'outvariant'
|
|
2
|
+
import { Emitter } from 'strict-event-emitter'
|
|
2
3
|
import { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'
|
|
3
4
|
import { InteractiveRequest } from '../../utils/toInteractiveRequest'
|
|
4
5
|
import { Interceptor } from '../../Interceptor'
|
|
5
|
-
import { AsyncEventEmitter } from '../../utils/AsyncEventEmitter'
|
|
6
6
|
import { createXMLHttpRequestProxy } from './XMLHttpRequestProxy'
|
|
7
7
|
|
|
8
8
|
export type XMLHttpRequestEventListener = (args: {
|
|
@@ -10,7 +10,7 @@ export type XMLHttpRequestEventListener = (args: {
|
|
|
10
10
|
requestId: string
|
|
11
11
|
}) => Promise<void> | void
|
|
12
12
|
|
|
13
|
-
export type XMLHttpRequestEmitter =
|
|
13
|
+
export type XMLHttpRequestEmitter = Emitter<HttpRequestEventMap>
|
|
14
14
|
|
|
15
15
|
export class XMLHttpRequestInterceptor extends Interceptor<HttpRequestEventMap> {
|
|
16
16
|
static interceptorSymbol = Symbol('xhr')
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { responseStatusCodesWithoutBody } from '../../../utils/responseUtils'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Creates a Fetch API `Response` instance from the given
|
|
@@ -16,7 +16,9 @@ export function createResponse(
|
|
|
16
16
|
* when constructing a Response instance.
|
|
17
17
|
* @see https://github.com/mswjs/interceptors/issues/379
|
|
18
18
|
*/
|
|
19
|
-
const responseBodyOrNull =
|
|
19
|
+
const responseBodyOrNull = responseStatusCodesWithoutBody.includes(
|
|
20
|
+
request.status
|
|
21
|
+
)
|
|
20
22
|
? null
|
|
21
23
|
: body
|
|
22
24
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { DeferredPromise } from '@open-draft/deferred-promise'
|
|
2
1
|
import { invariant } from 'outvariant'
|
|
2
|
+
import { DeferredPromise } from '@open-draft/deferred-promise'
|
|
3
3
|
import { until } from '@open-draft/until'
|
|
4
4
|
import { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'
|
|
5
5
|
import { Interceptor } from '../../Interceptor'
|
|
6
6
|
import { uuidv4 } from '../../utils/uuid'
|
|
7
7
|
import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
|
|
8
|
+
import { emitAsync } from '../../utils/emitAsync'
|
|
8
9
|
|
|
9
10
|
export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
|
|
10
11
|
static symbol = Symbol('fetch')
|
|
@@ -34,15 +35,22 @@ export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
|
|
|
34
35
|
|
|
35
36
|
this.logger.info('[%s] %s', request.method, request.url)
|
|
36
37
|
|
|
37
|
-
const interactiveRequest =
|
|
38
|
+
const { interactiveRequest, requestController } =
|
|
39
|
+
toInteractiveRequest(request)
|
|
38
40
|
|
|
39
41
|
this.logger.info(
|
|
40
42
|
'emitting the "request" event for %d listener(s)...',
|
|
41
43
|
this.emitter.listenerCount('request')
|
|
42
44
|
)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
requestId
|
|
45
|
+
|
|
46
|
+
this.emitter.once('request', ({ requestId: pendingRequestId }) => {
|
|
47
|
+
if (pendingRequestId !== requestId) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (requestController.responsePromise.state === 'pending') {
|
|
52
|
+
requestController.responsePromise.resolve(undefined)
|
|
53
|
+
}
|
|
46
54
|
})
|
|
47
55
|
|
|
48
56
|
this.logger.info('awaiting for the mocked response...')
|
|
@@ -59,18 +67,23 @@ export class FetchInterceptor extends Interceptor<HttpRequestEventMap> {
|
|
|
59
67
|
)
|
|
60
68
|
|
|
61
69
|
const resolverResult = await until(async () => {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
)
|
|
70
|
+
const listenersFinished = emitAsync(this.emitter, 'request', {
|
|
71
|
+
request: interactiveRequest,
|
|
72
|
+
requestId,
|
|
73
|
+
})
|
|
68
74
|
|
|
69
|
-
await Promise.race([
|
|
75
|
+
await Promise.race([
|
|
76
|
+
requestAborted,
|
|
77
|
+
// Put the listeners invocation Promise in the same race condition
|
|
78
|
+
// with the request abort Promise because otherwise awaiting the listeners
|
|
79
|
+
// would always yield some response (or undefined).
|
|
80
|
+
listenersFinished,
|
|
81
|
+
requestController.responsePromise,
|
|
82
|
+
])
|
|
70
83
|
|
|
71
84
|
this.logger.info('all request listeners have been resolved!')
|
|
72
85
|
|
|
73
|
-
const
|
|
86
|
+
const mockedResponse = await requestController.responsePromise
|
|
74
87
|
this.logger.info('event.respondWith called with:', mockedResponse)
|
|
75
88
|
|
|
76
89
|
return mockedResponse
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { invariant } from 'outvariant'
|
|
2
|
+
import { DeferredPromise } from '@open-draft/deferred-promise'
|
|
3
|
+
|
|
4
|
+
export class RequestController {
|
|
5
|
+
public responsePromise: DeferredPromise<Response | undefined>
|
|
6
|
+
|
|
7
|
+
constructor(protected request: Request) {
|
|
8
|
+
this.responsePromise = new DeferredPromise()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
public respondWith(response?: Response): void {
|
|
12
|
+
invariant(
|
|
13
|
+
this.responsePromise.state === 'pending',
|
|
14
|
+
'Failed to respond to "%s %s" request: the "request" event has already been responded to.',
|
|
15
|
+
this.request.method,
|
|
16
|
+
this.request.url
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
this.responsePromise.resolve(response)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Emitter, EventMap } from 'strict-event-emitter'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Emits an event on the given emitter but executes
|
|
5
|
+
* the listeners sequentially. This accounts for asynchronous
|
|
6
|
+
* listeners (e.g. those having "sleep" and handling the request).
|
|
7
|
+
*/
|
|
8
|
+
export async function emitAsync<
|
|
9
|
+
Events extends EventMap,
|
|
10
|
+
EventName extends keyof Events
|
|
11
|
+
>(
|
|
12
|
+
emitter: Emitter<Events>,
|
|
13
|
+
eventName: EventName,
|
|
14
|
+
...data: Events[EventName]
|
|
15
|
+
): Promise<void> {
|
|
16
|
+
const listners = emitter.listeners(eventName)
|
|
17
|
+
|
|
18
|
+
if (listners.length === 0) {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for (const listener of listners) {
|
|
23
|
+
await listener.apply(emitter, data)
|
|
24
|
+
}
|
|
25
|
+
}
|