@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.
@@ -0,0 +1,111 @@
1
+ import type { Debugger } from 'debug'
2
+ import { until } from '@open-draft/until'
3
+ import { XMLHttpRequestEmitter } from '.'
4
+ import { toInteractiveRequest } from '../../utils/toInteractiveRequest'
5
+ import { XMLHttpRequestController } from './XMLHttpRequestController'
6
+
7
+ export interface XMLHttpRequestProxyOptions {
8
+ emitter: XMLHttpRequestEmitter
9
+ log: Debugger
10
+ }
11
+
12
+ /**
13
+ * Create a proxied `XMLHttpRequest` class.
14
+ * The proxied class establishes spies on certain methods,
15
+ * allowing us to intercept requests and respond to them.
16
+ */
17
+ export function createXMLHttpRequestProxy({
18
+ emitter,
19
+ log,
20
+ }: XMLHttpRequestProxyOptions) {
21
+ const XMLHttpRequestProxy = new Proxy(globalThis.XMLHttpRequest, {
22
+ construct(target, args) {
23
+ log('constructed new XMLHttpRequest')
24
+
25
+ const originalRequest = Reflect.construct(target, args)
26
+
27
+ const requestController = new XMLHttpRequestController(
28
+ originalRequest,
29
+ log
30
+ )
31
+
32
+ requestController.onRequest = async function (request, requestId) {
33
+ // Notify the consumer about a new request.
34
+ const interactiveRequest = toInteractiveRequest(request)
35
+
36
+ this.log(
37
+ 'emitting the "request" event for %s listener(s)...',
38
+ emitter.listenerCount('request')
39
+ )
40
+ emitter.emit('request', interactiveRequest, requestId)
41
+
42
+ this.log('awaiting mocked response...')
43
+
44
+ const [middlewareException, mockedResponse] = await until(async () => {
45
+ await emitter.untilIdle(
46
+ 'request',
47
+ ({ args: [, pendingRequestId] }) => {
48
+ return pendingRequestId === requestId
49
+ }
50
+ )
51
+
52
+ this.log('all "request" listeners settled!')
53
+
54
+ const [mockedResponse] =
55
+ await interactiveRequest.respondWith.invoked()
56
+
57
+ this.log('event.respondWith called with:', mockedResponse)
58
+
59
+ return mockedResponse
60
+ })
61
+
62
+ if (middlewareException) {
63
+ this.log(
64
+ 'request listener threw an exception, aborting request...',
65
+ middlewareException
66
+ )
67
+
68
+ /**
69
+ * @todo Consider forwarding this error to the stderr as well
70
+ * since not all consumers are expecting to handle errors.
71
+ * If they don't, this error will be swallowed.
72
+ */
73
+ requestController.errorWith(middlewareException)
74
+ return
75
+ }
76
+
77
+ if (typeof mockedResponse !== 'undefined') {
78
+ this.log(
79
+ 'received mocked response: %d %s',
80
+ mockedResponse.status,
81
+ mockedResponse.statusText
82
+ )
83
+
84
+ return requestController.respondWith(mockedResponse)
85
+ }
86
+
87
+ this.log('no mocked response received, performing request as-is...')
88
+ }
89
+
90
+ requestController.onResponse = async function (
91
+ response,
92
+ request,
93
+ requestId
94
+ ) {
95
+ this.log(
96
+ 'emitting the "response" event for %s listener(s)...',
97
+ emitter.listenerCount('response')
98
+ )
99
+
100
+ emitter.emit('response', response, request, requestId)
101
+ }
102
+
103
+ // Return the proxied request from the controller
104
+ // so that the controller can react to the consumer's interactions
105
+ // with this request (opening/sending/etc).
106
+ return requestController.request
107
+ },
108
+ })
109
+
110
+ return XMLHttpRequestProxy
111
+ }
@@ -3,7 +3,7 @@ import { HttpRequestEventMap, IS_PATCHED_MODULE } from '../../glossary'
3
3
  import { InteractiveRequest } from '../../utils/toInteractiveRequest'
4
4
  import { Interceptor } from '../../Interceptor'
5
5
  import { AsyncEventEmitter } from '../../utils/AsyncEventEmitter'
6
- import { createXMLHttpRequestOverride } from './XMLHttpRequestOverride'
6
+ import { createXMLHttpRequestProxy } from './XMLHttpRequestProxy'
7
7
 
8
8
  export type XMLHttpRequestEventListener = (
9
9
  request: InteractiveRequest,
@@ -20,10 +20,7 @@ export class XMLHttpRequestInterceptor extends Interceptor<HttpRequestEventMap>
20
20
  }
21
21
 
22
22
  protected checkEnvironment() {
23
- return (
24
- typeof window !== 'undefined' &&
25
- typeof window.XMLHttpRequest !== 'undefined'
26
- )
23
+ return typeof globalThis.XMLHttpRequest !== 'undefined'
27
24
  }
28
25
 
29
26
  protected setup() {
@@ -31,36 +28,38 @@ export class XMLHttpRequestInterceptor extends Interceptor<HttpRequestEventMap>
31
28
 
32
29
  log('patching "XMLHttpRequest" module...')
33
30
 
34
- const PureXMLHttpRequest = window.XMLHttpRequest
31
+ const PureXMLHttpRequest = globalThis.XMLHttpRequest
35
32
 
36
33
  invariant(
37
34
  !(PureXMLHttpRequest as any)[IS_PATCHED_MODULE],
38
35
  'Failed to patch the "XMLHttpRequest" module: already patched.'
39
36
  )
40
37
 
41
- window.XMLHttpRequest = createXMLHttpRequestOverride({
42
- XMLHttpRequest: PureXMLHttpRequest,
38
+ globalThis.XMLHttpRequest = createXMLHttpRequestProxy({
43
39
  emitter: this.emitter,
44
40
  log: this.log,
45
41
  })
46
42
 
47
- log('native "XMLHttpRequest" module patched!', window.XMLHttpRequest.name)
43
+ log(
44
+ 'native "XMLHttpRequest" module patched!',
45
+ globalThis.XMLHttpRequest.name
46
+ )
48
47
 
49
- Object.defineProperty(window.XMLHttpRequest, IS_PATCHED_MODULE, {
48
+ Object.defineProperty(globalThis.XMLHttpRequest, IS_PATCHED_MODULE, {
50
49
  enumerable: true,
51
50
  configurable: true,
52
51
  value: true,
53
52
  })
54
53
 
55
54
  this.subscriptions.push(() => {
56
- Object.defineProperty(window.XMLHttpRequest, IS_PATCHED_MODULE, {
55
+ Object.defineProperty(globalThis.XMLHttpRequest, IS_PATCHED_MODULE, {
57
56
  value: undefined,
58
57
  })
59
58
 
60
- window.XMLHttpRequest = PureXMLHttpRequest
59
+ globalThis.XMLHttpRequest = PureXMLHttpRequest
61
60
  log(
62
61
  'native "XMLHttpRequest" module restored!',
63
- window.XMLHttpRequest.name
62
+ globalThis.XMLHttpRequest.name
64
63
  )
65
64
  })
66
65
  }
@@ -0,0 +1,78 @@
1
+ export interface ProxyOptions<Target extends Record<string, any>> {
2
+ constructorCall?(args: Array<unknown>, next: NextFunction<Target>): Target
3
+
4
+ methodCall?<F extends keyof Target>(
5
+ this: Target,
6
+ data: [methodName: F, args: Array<unknown>],
7
+ next: NextFunction<void>
8
+ ): void
9
+
10
+ setProperty?(
11
+ data: [propertyName: string | symbol, nextValue: unknown],
12
+ next: NextFunction<void>
13
+ ): void
14
+
15
+ getProperty?(
16
+ data: [propertyName: string | symbol, receiver: Target],
17
+ next: NextFunction<void>
18
+ ): void
19
+ }
20
+
21
+ export type NextFunction<ReturnType> = () => ReturnType
22
+
23
+ export function createProxy<Target extends object>(
24
+ target: Target,
25
+ options: ProxyOptions<Target>
26
+ ): Target {
27
+ const proxy = new Proxy(target, optionsToProxyHandler(options))
28
+ return proxy
29
+ }
30
+
31
+ function optionsToProxyHandler<T extends Record<string, any>>(
32
+ options: ProxyOptions<T>
33
+ ): ProxyHandler<T> {
34
+ const { constructorCall, methodCall, getProperty, setProperty } = options
35
+ const handler: ProxyHandler<T> = {}
36
+
37
+ if (typeof constructorCall !== 'undefined') {
38
+ handler.construct = function (target, args, newTarget) {
39
+ const next = Reflect.construct.bind(null, target as any, args, newTarget)
40
+ return constructorCall.call(newTarget, args, next)
41
+ }
42
+ }
43
+
44
+ if (typeof setProperty !== 'undefined') {
45
+ handler.set = function (target, propertyName, nextValue, receiver) {
46
+ const next = () => Reflect.set(target, propertyName, nextValue, receiver)
47
+ return setProperty.call(target, [propertyName, nextValue], next) as any
48
+ }
49
+ }
50
+
51
+ handler.get = function (target, propertyName, receiver) {
52
+ /**
53
+ * @note Using `Reflect.get()` here causes "TypeError: Illegal invocation".
54
+ */
55
+ const next = () => target[propertyName as any]
56
+
57
+ const value =
58
+ typeof getProperty !== 'undefined'
59
+ ? getProperty.call(target, [propertyName, receiver], next)
60
+ : next()
61
+
62
+ if (typeof value === 'function') {
63
+ return (...args: Array<any>) => {
64
+ const next = value.bind(target, ...args)
65
+
66
+ if (typeof methodCall !== 'undefined') {
67
+ return methodCall.call(target, [propertyName as any, args], next)
68
+ }
69
+
70
+ return next()
71
+ }
72
+ }
73
+
74
+ return value
75
+ }
76
+
77
+ return handler
78
+ }
@@ -1,93 +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 { Headers } from 'headers-polyfill';
7
- import type { XMLHttpRequestEmitter } from '.';
8
- declare type XMLHttpRequestEventHandler = (this: XMLHttpRequest, event: Event | ProgressEvent<any>) => void;
9
- interface XMLHttpRequestEvent<EventMap extends any> {
10
- name: keyof EventMap;
11
- listener: XMLHttpRequestEventHandler;
12
- }
13
- interface CreateXMLHttpRequestOverrideOptions {
14
- XMLHttpRequest: typeof window.XMLHttpRequest;
15
- emitter: XMLHttpRequestEmitter;
16
- log: Debugger;
17
- }
18
- interface InternalXMLHttpRequestEventTargetEventMap extends XMLHttpRequestEventTargetEventMap {
19
- readystatechange: Event;
20
- }
21
- export declare type ExtractCallbacks<Key extends string> = Key extends 'abort' | `on${infer _CallbackName}` ? Key : never;
22
- export declare const createXMLHttpRequestOverride: (options: CreateXMLHttpRequestOverrideOptions) => {
23
- new (): {
24
- _requestHeaders: Headers;
25
- _responseHeaders: Headers;
26
- _responseBuffer: Uint8Array;
27
- _events: XMLHttpRequestEvent<InternalXMLHttpRequestEventTargetEventMap>[];
28
- log: Debugger;
29
- readonly UNSENT: 0;
30
- readonly OPENED: 1;
31
- readonly HEADERS_RECEIVED: 2;
32
- readonly LOADING: 3;
33
- readonly DONE: 4;
34
- method: string;
35
- url: string;
36
- withCredentials: boolean;
37
- status: number;
38
- statusText: string;
39
- user?: string | undefined;
40
- password?: string | undefined;
41
- async?: boolean | undefined;
42
- responseType: XMLHttpRequestResponseType;
43
- responseURL: string;
44
- upload: XMLHttpRequestUpload;
45
- readyState: number;
46
- onreadystatechange: (this: XMLHttpRequest, ev: Event) => any;
47
- timeout: number;
48
- onabort: (this: XMLHttpRequestEventTarget, event: ProgressEvent) => any;
49
- onerror: (this: XMLHttpRequestEventTarget, event: Event) => any;
50
- onload: (this: XMLHttpRequestEventTarget, event: ProgressEvent) => any;
51
- onloadend: (this: XMLHttpRequestEventTarget, event: ProgressEvent) => any;
52
- onloadstart: (this: XMLHttpRequestEventTarget, event: ProgressEvent) => any;
53
- onprogress: (this: XMLHttpRequestEventTarget, event: ProgressEvent) => any;
54
- ontimeout: (this: XMLHttpRequestEventTarget, event: ProgressEvent) => any;
55
- setReadyState(nextState: number): void;
56
- /**
57
- * Triggers both direct callback and attached event listeners
58
- * for the given event.
59
- */
60
- trigger<K extends "readystatechange" | keyof XMLHttpRequestEventTargetEventMap>(eventName: K, options?: ProgressEventInit | undefined): any;
61
- reset(): void;
62
- open(method: string, url: string, async?: boolean, user?: string | undefined, password?: string | undefined): Promise<void>;
63
- send(data?: string | ArrayBuffer | undefined): void;
64
- readonly responseText: string;
65
- readonly response: unknown;
66
- readonly responseXML: Document | null;
67
- abort(): void;
68
- dispatchEvent(): boolean;
69
- setRequestHeader(name: string, value: string): void;
70
- getResponseHeader(name: string): string | null;
71
- getAllResponseHeaders(): string;
72
- addEventListener<Event_1 extends keyof InternalXMLHttpRequestEventTargetEventMap>(event: Event_1, listener: XMLHttpRequestEventHandler): void;
73
- removeEventListener<Event_2 extends keyof XMLHttpRequestEventMap>(event: Event_2, listener: (event?: XMLHttpRequestEventMap[Event_2] | undefined) => void): void;
74
- overrideMimeType(): void;
75
- /**
76
- * Propagates mock XMLHttpRequest instance callbacks
77
- * to the given XMLHttpRequest instance.
78
- */
79
- propagateCallbacks(request: XMLHttpRequest): void;
80
- /**
81
- * Propagates the mock XMLHttpRequest instance listeners
82
- * to the given XMLHttpRequest instance.
83
- */
84
- propagateListeners(request: XMLHttpRequest): void;
85
- propagateHeaders(request: XMLHttpRequest, headers: Headers): void;
86
- };
87
- readonly UNSENT: 0;
88
- readonly OPENED: 1;
89
- readonly HEADERS_RECEIVED: 2;
90
- readonly LOADING: 3;
91
- readonly DONE: 4;
92
- };
93
- export {};