@mswjs/interceptors 0.25.0 → 0.25.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/lib/browser/Interceptor-8d5fd4c6.d.ts +86 -0
- package/lib/browser/{chunk-4CFMDU7Z.js → chunk-CWVY2E3W.js} +20 -134
- 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-OSIUQA4X.js → chunk-NMG5MQJJ.js} +32 -29
- package/lib/browser/{chunk-GXJLJMOT.mjs → chunk-QPMXOLDO.mjs} +21 -135
- package/lib/browser/{chunk-ANLPTCZ5.mjs → chunk-TYEVJTWH.mjs} +27 -24
- 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 +18 -18
- package/lib/node/RemoteHttpInterceptor.mjs +14 -14
- package/lib/node/{chunk-XYZRP5S2.js → chunk-3XFLRXRY.js} +20 -134
- package/lib/node/chunk-5PTPJLB7.js +58 -0
- package/lib/node/{chunk-E6YC337Q.js → chunk-5YAV7CXX.js} +29 -26
- package/lib/node/{chunk-HSCXCLVT.mjs → chunk-7RGC35CC.mjs} +27 -24
- package/lib/node/{chunk-3MYUI4B2.js → chunk-B2CIOP5B.js} +22 -16
- package/lib/node/{chunk-RGYCLCLK.mjs → chunk-GM3YBSM3.mjs} +21 -135
- package/lib/node/{chunk-OL7OR4RL.mjs → chunk-OMRBBJT7.mjs} +20 -14
- 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 +4 -4
- package/lib/node/interceptors/ClientRequest/index.mjs +3 -3
- package/lib/node/interceptors/XMLHttpRequest/index.d.ts +4 -3
- package/lib/node/interceptors/XMLHttpRequest/index.js +4 -4
- package/lib/node/interceptors/XMLHttpRequest/index.mjs +3 -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 +6 -6
- package/lib/node/presets/node.mjs +4 -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 +35 -18
- 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/XMLHttpRequest/XMLHttpRequestProxy.ts +29 -25
- package/src/interceptors/XMLHttpRequest/index.ts +2 -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/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
package/src/Interceptor.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Logger } from '@open-draft/logger'
|
|
2
|
-
import { Listener } from 'strict-event-emitter'
|
|
3
|
-
import { AsyncEventEmitter } from './utils/AsyncEventEmitter'
|
|
2
|
+
import { Emitter, EventMap, Listener } from 'strict-event-emitter'
|
|
4
3
|
|
|
5
4
|
export type InterceptorEventMap = Record<string, any>
|
|
6
5
|
export type InterceptorSubscription = () => void
|
|
@@ -34,7 +33,7 @@ export type ExtractEventNames<Events extends Record<string, any>> =
|
|
|
34
33
|
Events extends Record<infer EventName, any> ? EventName : never
|
|
35
34
|
|
|
36
35
|
export class Interceptor<Events extends InterceptorEventMap> {
|
|
37
|
-
protected emitter:
|
|
36
|
+
protected emitter: Emitter<Events>
|
|
38
37
|
protected subscriptions: Array<InterceptorSubscription>
|
|
39
38
|
protected logger: Logger
|
|
40
39
|
|
|
@@ -43,7 +42,7 @@ export class Interceptor<Events extends InterceptorEventMap> {
|
|
|
43
42
|
constructor(private readonly symbol: symbol) {
|
|
44
43
|
this.readyState = InterceptorReadyState.INACTIVE
|
|
45
44
|
|
|
46
|
-
this.emitter = new
|
|
45
|
+
this.emitter = new Emitter()
|
|
47
46
|
this.subscriptions = []
|
|
48
47
|
this.logger = new Logger(symbol.description!)
|
|
49
48
|
|
|
@@ -84,12 +83,6 @@ export class Interceptor<Events extends InterceptorEventMap> {
|
|
|
84
83
|
|
|
85
84
|
this.readyState = InterceptorReadyState.APPLYING
|
|
86
85
|
|
|
87
|
-
// Always activate the emitter when applying the interceptor.
|
|
88
|
-
// This will ensure the interceptor can process events after it's
|
|
89
|
-
// been disposed and re-applied again (it may be a singleton).
|
|
90
|
-
this.emitter.activate()
|
|
91
|
-
logger.info('activated the emiter!', this.emitter.readyState)
|
|
92
|
-
|
|
93
86
|
// Whenever applying a new interceptor, check if it hasn't been applied already.
|
|
94
87
|
// This enables to apply the same interceptor multiple times, for example from a different
|
|
95
88
|
// interceptor, only proxying events but keeping the stubs in a single place.
|
|
@@ -112,6 +105,8 @@ export class Interceptor<Events extends InterceptorEventMap> {
|
|
|
112
105
|
runningInstance.emitter.removeListener(event, listener)
|
|
113
106
|
logger.info('removed proxied "%s" listener!', event)
|
|
114
107
|
})
|
|
108
|
+
|
|
109
|
+
return this
|
|
115
110
|
}
|
|
116
111
|
|
|
117
112
|
this.readyState = InterceptorReadyState.APPLIED
|
|
@@ -141,9 +136,9 @@ export class Interceptor<Events extends InterceptorEventMap> {
|
|
|
141
136
|
* Listen to the interceptor's public events.
|
|
142
137
|
*/
|
|
143
138
|
public on<EventName extends ExtractEventNames<Events>>(
|
|
144
|
-
|
|
139
|
+
event: EventName,
|
|
145
140
|
listener: Listener<Events[EventName]>
|
|
146
|
-
):
|
|
141
|
+
): this {
|
|
147
142
|
const logger = this.logger.extend('on')
|
|
148
143
|
|
|
149
144
|
if (
|
|
@@ -151,12 +146,36 @@ export class Interceptor<Events extends InterceptorEventMap> {
|
|
|
151
146
|
this.readyState === InterceptorReadyState.DISPOSED
|
|
152
147
|
) {
|
|
153
148
|
logger.info('cannot listen to events, already disposed!')
|
|
154
|
-
return
|
|
149
|
+
return this
|
|
155
150
|
}
|
|
156
151
|
|
|
157
|
-
logger.info('adding "%s" event listener:',
|
|
152
|
+
logger.info('adding "%s" event listener:', event, listener.name)
|
|
153
|
+
|
|
154
|
+
this.emitter.on(event, listener)
|
|
155
|
+
return this
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public once<EventName extends ExtractEventNames<Events>>(
|
|
159
|
+
event: EventName,
|
|
160
|
+
listener: Listener<Events[EventName]>
|
|
161
|
+
): this {
|
|
162
|
+
this.emitter.once(event, listener)
|
|
163
|
+
return this
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public off<EventName extends ExtractEventNames<Events>>(
|
|
167
|
+
event: EventName,
|
|
168
|
+
listener: Listener<Events[EventName]>
|
|
169
|
+
): this {
|
|
170
|
+
this.emitter.off(event, listener)
|
|
171
|
+
return this
|
|
172
|
+
}
|
|
158
173
|
|
|
159
|
-
|
|
174
|
+
public removeAllListeners<EventName extends ExtractEventNames<Events>>(
|
|
175
|
+
event?: EventName
|
|
176
|
+
): this {
|
|
177
|
+
this.emitter.removeAllListeners(event)
|
|
178
|
+
return this
|
|
160
179
|
}
|
|
161
180
|
|
|
162
181
|
/**
|
|
@@ -196,7 +215,7 @@ export class Interceptor<Events extends InterceptorEventMap> {
|
|
|
196
215
|
logger.info('disposed of all subscriptions!', this.subscriptions.length)
|
|
197
216
|
}
|
|
198
217
|
|
|
199
|
-
this.emitter.
|
|
218
|
+
this.emitter.removeAllListeners()
|
|
200
219
|
logger.info('destroyed the listener!')
|
|
201
220
|
|
|
202
221
|
this.readyState = InterceptorReadyState.DISPOSED
|
|
@@ -5,6 +5,7 @@ import { BatchInterceptor } from './BatchInterceptor'
|
|
|
5
5
|
import { ClientRequestInterceptor } from './interceptors/ClientRequest'
|
|
6
6
|
import { XMLHttpRequestInterceptor } from './interceptors/XMLHttpRequest'
|
|
7
7
|
import { toInteractiveRequest } from './utils/toInteractiveRequest'
|
|
8
|
+
import { emitAsync } from './utils/emitAsync'
|
|
8
9
|
|
|
9
10
|
export interface SerializedRequest {
|
|
10
11
|
id: string
|
|
@@ -166,20 +167,21 @@ export class RemoteHttpResolver extends Interceptor<HttpRequestEventMap> {
|
|
|
166
167
|
body: requestJson.body,
|
|
167
168
|
})
|
|
168
169
|
|
|
169
|
-
const interactiveRequest =
|
|
170
|
+
const { interactiveRequest, requestController } =
|
|
171
|
+
toInteractiveRequest(capturedRequest)
|
|
170
172
|
|
|
171
|
-
this.emitter.
|
|
173
|
+
this.emitter.once('request', () => {
|
|
174
|
+
if (requestController.responsePromise.state === 'pending') {
|
|
175
|
+
requestController.respondWith(undefined)
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
await emitAsync(this.emitter, 'request', {
|
|
172
180
|
request: interactiveRequest,
|
|
173
181
|
requestId: requestJson.id,
|
|
174
182
|
})
|
|
175
183
|
|
|
176
|
-
await
|
|
177
|
-
'request',
|
|
178
|
-
({ args: [{ requestId: pendingRequestId }] }) => {
|
|
179
|
-
return pendingRequestId === requestJson.id
|
|
180
|
-
}
|
|
181
|
-
)
|
|
182
|
-
const [mockedResponse] = await interactiveRequest.respondWith.invoked()
|
|
184
|
+
const mockedResponse = await requestController.responsePromise
|
|
183
185
|
|
|
184
186
|
if (!mockedResponse) {
|
|
185
187
|
return
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { vi, it, expect, beforeAll, afterAll } from 'vitest'
|
|
2
2
|
import express from 'express'
|
|
3
3
|
import { IncomingMessage } from 'http'
|
|
4
|
+
import { Emitter } from 'strict-event-emitter'
|
|
4
5
|
import { Logger } from '@open-draft/logger'
|
|
5
6
|
import { HttpServer } from '@open-draft/test-server/http'
|
|
6
7
|
import { DeferredPromise } from '@open-draft/deferred-promise'
|
|
7
8
|
import { NodeClientRequest } from './NodeClientRequest'
|
|
8
9
|
import { getIncomingMessageBody } from './utils/getIncomingMessageBody'
|
|
9
10
|
import { normalizeClientRequestArgs } from './utils/normalizeClientRequestArgs'
|
|
10
|
-
import { AsyncEventEmitter } from '../../utils/AsyncEventEmitter'
|
|
11
11
|
import { sleep } from '../../../test/helpers'
|
|
12
12
|
import { HttpRequestEventMap } from '../../glossary'
|
|
13
13
|
|
|
@@ -37,7 +37,7 @@ afterAll(async () => {
|
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
it('gracefully finishes the request when it has a mocked response', async () => {
|
|
40
|
-
const emitter = new
|
|
40
|
+
const emitter = new Emitter<HttpRequestEventMap>()
|
|
41
41
|
const request = new NodeClientRequest(
|
|
42
42
|
normalizeClientRequestArgs('http:', 'http://any.thing', {
|
|
43
43
|
method: 'PUT',
|
|
@@ -88,7 +88,7 @@ it('gracefully finishes the request when it has a mocked response', async () =>
|
|
|
88
88
|
})
|
|
89
89
|
|
|
90
90
|
it('responds with a mocked response when requesting an existing hostname', async () => {
|
|
91
|
-
const emitter = new
|
|
91
|
+
const emitter = new Emitter<HttpRequestEventMap>()
|
|
92
92
|
const request = new NodeClientRequest(
|
|
93
93
|
normalizeClientRequestArgs('http:', httpServer.http.url('/comment')),
|
|
94
94
|
{
|
|
@@ -116,7 +116,7 @@ it('responds with a mocked response when requesting an existing hostname', async
|
|
|
116
116
|
})
|
|
117
117
|
|
|
118
118
|
it('performs the request as-is given resolver returned no mocked response', async () => {
|
|
119
|
-
const emitter = new
|
|
119
|
+
const emitter = new Emitter<HttpRequestEventMap>()
|
|
120
120
|
const request = new NodeClientRequest(
|
|
121
121
|
normalizeClientRequestArgs('http:', httpServer.http.url('/comment'), {
|
|
122
122
|
method: 'POST',
|
|
@@ -147,7 +147,7 @@ it('performs the request as-is given resolver returned no mocked response', asyn
|
|
|
147
147
|
})
|
|
148
148
|
|
|
149
149
|
it('emits the ENOTFOUND error connecting to a non-existing hostname given no mocked response', async () => {
|
|
150
|
-
const emitter = new
|
|
150
|
+
const emitter = new Emitter<HttpRequestEventMap>()
|
|
151
151
|
const request = new NodeClientRequest(
|
|
152
152
|
normalizeClientRequestArgs('http:', 'http://non-existing-url.com'),
|
|
153
153
|
{ emitter, logger }
|
|
@@ -165,7 +165,7 @@ it('emits the ENOTFOUND error connecting to a non-existing hostname given no moc
|
|
|
165
165
|
})
|
|
166
166
|
|
|
167
167
|
it('emits the ECONNREFUSED error connecting to an inactive server given no mocked response', async () => {
|
|
168
|
-
const emitter = new
|
|
168
|
+
const emitter = new Emitter<HttpRequestEventMap>()
|
|
169
169
|
const request = new NodeClientRequest(
|
|
170
170
|
normalizeClientRequestArgs('http:', 'http://127.0.0.1:12345'),
|
|
171
171
|
{
|
|
@@ -191,7 +191,7 @@ it('emits the ECONNREFUSED error connecting to an inactive server given no mocke
|
|
|
191
191
|
})
|
|
192
192
|
|
|
193
193
|
it('does not emit ENOTFOUND error connecting to an inactive server given mocked response', async () => {
|
|
194
|
-
const emitter = new
|
|
194
|
+
const emitter = new Emitter<HttpRequestEventMap>()
|
|
195
195
|
const handleError = vi.fn()
|
|
196
196
|
const request = new NodeClientRequest(
|
|
197
197
|
normalizeClientRequestArgs('http:', 'http://non-existing-url.com'),
|
|
@@ -221,7 +221,7 @@ it('does not emit ENOTFOUND error connecting to an inactive server given mocked
|
|
|
221
221
|
})
|
|
222
222
|
|
|
223
223
|
it('does not emit ECONNREFUSED error connecting to an inactive server given mocked response', async () => {
|
|
224
|
-
const emitter = new
|
|
224
|
+
const emitter = new Emitter<HttpRequestEventMap>()
|
|
225
225
|
const handleError = vi.fn()
|
|
226
226
|
const request = new NodeClientRequest(
|
|
227
227
|
normalizeClientRequestArgs('http:', 'http://localhost:9876'),
|
|
@@ -253,7 +253,7 @@ it('does not emit ECONNREFUSED error connecting to an inactive server given mock
|
|
|
253
253
|
})
|
|
254
254
|
|
|
255
255
|
it('sends the request body to the server given no mocked response', async () => {
|
|
256
|
-
const emitter = new
|
|
256
|
+
const emitter = new Emitter<HttpRequestEventMap>()
|
|
257
257
|
const request = new NodeClientRequest(
|
|
258
258
|
normalizeClientRequestArgs('http:', httpServer.http.url('/write'), {
|
|
259
259
|
method: 'POST',
|
|
@@ -284,7 +284,7 @@ it('sends the request body to the server given no mocked response', async () =>
|
|
|
284
284
|
})
|
|
285
285
|
|
|
286
286
|
it('does not send request body to the original server given mocked response', async () => {
|
|
287
|
-
const emitter = new
|
|
287
|
+
const emitter = new Emitter<HttpRequestEventMap>()
|
|
288
288
|
const request = new NodeClientRequest(
|
|
289
289
|
normalizeClientRequestArgs('http:', httpServer.http.url('/write'), {
|
|
290
290
|
method: 'POST',
|
|
@@ -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).
|
|
@@ -155,28 +166,34 @@ export class NodeClientRequest extends ClientRequest {
|
|
|
155
166
|
'emitting the "request" event for %d listener(s)...',
|
|
156
167
|
this.emitter.listenerCount('request')
|
|
157
168
|
)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
169
|
+
|
|
170
|
+
// Add the last "request" listener that always resolves
|
|
171
|
+
// the pending response Promise. This way if the consumer
|
|
172
|
+
// hasn't handled the request themselves, we will prevent
|
|
173
|
+
// the response Promise from pending indefinitely.
|
|
174
|
+
this.emitter.once('request', ({ requestId: pendingRequestId }) => {
|
|
175
|
+
/**
|
|
176
|
+
* @note Ignore request events emitted by irrelevant
|
|
177
|
+
* requests. This happens when response patching.
|
|
178
|
+
*/
|
|
179
|
+
if (pendingRequestId !== requestId) {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (requestController.responsePromise.state === 'pending') {
|
|
184
|
+
requestController.responsePromise.resolve(undefined)
|
|
185
|
+
}
|
|
161
186
|
})
|
|
162
187
|
|
|
163
188
|
// Execute the resolver Promise like a side-effect.
|
|
164
189
|
// Node.js 16 forces "ClientRequest.end" to be synchronous and return "this".
|
|
165
190
|
until(async () => {
|
|
166
|
-
await this.emitter
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
* @note Await only those listeners that are relevant to this request.
|
|
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
|
-
}
|
|
177
|
-
)
|
|
191
|
+
await emitAsync(this.emitter, 'request', {
|
|
192
|
+
request: interactiveRequest,
|
|
193
|
+
requestId,
|
|
194
|
+
})
|
|
178
195
|
|
|
179
|
-
const
|
|
196
|
+
const mockedResponse = await requestController.responsePromise
|
|
180
197
|
this.logger.info('event.respondWith called with:', mockedResponse)
|
|
181
198
|
|
|
182
199
|
return mockedResponse
|
|
@@ -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 { 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,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
|
+
}
|