763 lines
27 KiB
JavaScript
763 lines
27 KiB
JavaScript
const require_chunk = require('./chunk-CbDLau6x.cjs');
|
|
const require_glossary = require('./glossary-BLKRyLBd.cjs');
|
|
const require_fetchUtils = require('./fetchUtils-BaY5iWXw.cjs');
|
|
const require_bufferUtils = require('./bufferUtils-DiCTqG-7.cjs');
|
|
const require_getRawRequest = require('./getRawRequest-BavnMWh_.cjs');
|
|
const require_handleRequest = require('./handleRequest-Bb7Y-XLw.cjs');
|
|
const require_hasConfigurableGlobal = require('./hasConfigurableGlobal-C97fWuaA.cjs');
|
|
let outvariant = require("outvariant");
|
|
let is_node_process = require("is-node-process");
|
|
|
|
//#region src/interceptors/XMLHttpRequest/utils/concatArrayBuffer.ts
|
|
/**
|
|
* Concatenate two `Uint8Array` buffers.
|
|
*/
|
|
function concatArrayBuffer(left, right) {
|
|
const result = new Uint8Array(left.byteLength + right.byteLength);
|
|
result.set(left, 0);
|
|
result.set(right, left.byteLength);
|
|
return result;
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/interceptors/XMLHttpRequest/polyfills/EventPolyfill.ts
|
|
var EventPolyfill = class {
|
|
constructor(type, options) {
|
|
this.NONE = 0;
|
|
this.CAPTURING_PHASE = 1;
|
|
this.AT_TARGET = 2;
|
|
this.BUBBLING_PHASE = 3;
|
|
this.type = "";
|
|
this.srcElement = null;
|
|
this.currentTarget = null;
|
|
this.eventPhase = 0;
|
|
this.isTrusted = true;
|
|
this.composed = false;
|
|
this.cancelable = true;
|
|
this.defaultPrevented = false;
|
|
this.bubbles = true;
|
|
this.lengthComputable = true;
|
|
this.loaded = 0;
|
|
this.total = 0;
|
|
this.cancelBubble = false;
|
|
this.returnValue = true;
|
|
this.type = type;
|
|
this.target = options?.target || null;
|
|
this.currentTarget = options?.currentTarget || null;
|
|
this.timeStamp = Date.now();
|
|
}
|
|
composedPath() {
|
|
return [];
|
|
}
|
|
initEvent(type, bubbles, cancelable) {
|
|
this.type = type;
|
|
this.bubbles = !!bubbles;
|
|
this.cancelable = !!cancelable;
|
|
}
|
|
preventDefault() {
|
|
this.defaultPrevented = true;
|
|
}
|
|
stopPropagation() {}
|
|
stopImmediatePropagation() {}
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/interceptors/XMLHttpRequest/polyfills/ProgressEventPolyfill.ts
|
|
var ProgressEventPolyfill = class extends EventPolyfill {
|
|
constructor(type, init) {
|
|
super(type);
|
|
this.lengthComputable = init?.lengthComputable || false;
|
|
this.composed = init?.composed || false;
|
|
this.loaded = init?.loaded || 0;
|
|
this.total = init?.total || 0;
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
//#region src/interceptors/XMLHttpRequest/utils/createEvent.ts
|
|
const SUPPORTS_PROGRESS_EVENT = typeof ProgressEvent !== "undefined";
|
|
function createEvent(target, type, init) {
|
|
const progressEvents = [
|
|
"error",
|
|
"progress",
|
|
"loadstart",
|
|
"loadend",
|
|
"load",
|
|
"timeout",
|
|
"abort"
|
|
];
|
|
/**
|
|
* `ProgressEvent` is not supported in React Native.
|
|
* @see https://github.com/mswjs/interceptors/issues/40
|
|
*/
|
|
const ProgressEventClass = SUPPORTS_PROGRESS_EVENT ? ProgressEvent : ProgressEventPolyfill;
|
|
return progressEvents.includes(type) ? new ProgressEventClass(type, {
|
|
lengthComputable: true,
|
|
loaded: init?.loaded || 0,
|
|
total: init?.total || 0
|
|
}) : new EventPolyfill(type, {
|
|
target,
|
|
currentTarget: target
|
|
});
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/utils/findPropertySource.ts
|
|
/**
|
|
* Returns the source object of the given property on the target object
|
|
* (the target itself, any parent in its prototype, or null).
|
|
*/
|
|
function findPropertySource(target, propertyName) {
|
|
if (!(propertyName in target)) return null;
|
|
if (Object.prototype.hasOwnProperty.call(target, propertyName)) return target;
|
|
const prototype = Reflect.getPrototypeOf(target);
|
|
return prototype ? findPropertySource(prototype, propertyName) : null;
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/utils/createProxy.ts
|
|
function createProxy(target, options) {
|
|
return new Proxy(target, optionsToProxyHandler(options));
|
|
}
|
|
function optionsToProxyHandler(options) {
|
|
const { constructorCall, methodCall, getProperty, setProperty } = options;
|
|
const handler = {};
|
|
if (typeof constructorCall !== "undefined") handler.construct = function(target, args, newTarget) {
|
|
const next = Reflect.construct.bind(null, target, args, newTarget);
|
|
return constructorCall.call(newTarget, args, next);
|
|
};
|
|
handler.set = function(target, propertyName, nextValue) {
|
|
const next = () => {
|
|
const propertySource = findPropertySource(target, propertyName) || target;
|
|
const ownDescriptors = Reflect.getOwnPropertyDescriptor(propertySource, propertyName);
|
|
if (typeof ownDescriptors?.set !== "undefined") {
|
|
ownDescriptors.set.apply(target, [nextValue]);
|
|
return true;
|
|
}
|
|
return Reflect.defineProperty(propertySource, propertyName, {
|
|
writable: true,
|
|
enumerable: true,
|
|
configurable: true,
|
|
value: nextValue
|
|
});
|
|
};
|
|
if (typeof setProperty !== "undefined") return setProperty.call(target, [propertyName, nextValue], next);
|
|
return next();
|
|
};
|
|
handler.get = function(target, propertyName, receiver) {
|
|
/**
|
|
* @note Using `Reflect.get()` here causes "TypeError: Illegal invocation".
|
|
*/
|
|
const next = () => target[propertyName];
|
|
const value = typeof getProperty !== "undefined" ? getProperty.call(target, [propertyName, receiver], next) : next();
|
|
if (typeof value === "function") return (...args) => {
|
|
const next$1 = value.bind(target, ...args);
|
|
if (typeof methodCall !== "undefined") return methodCall.call(target, [propertyName, args], next$1);
|
|
return next$1();
|
|
};
|
|
return value;
|
|
};
|
|
return handler;
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/interceptors/XMLHttpRequest/utils/isDomParserSupportedType.ts
|
|
function isDomParserSupportedType(type) {
|
|
return [
|
|
"application/xhtml+xml",
|
|
"application/xml",
|
|
"image/svg+xml",
|
|
"text/html",
|
|
"text/xml"
|
|
].some((supportedType) => {
|
|
return type.startsWith(supportedType);
|
|
});
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/utils/parseJson.ts
|
|
/**
|
|
* Parses a given string into JSON.
|
|
* Gracefully handles invalid JSON by returning `null`.
|
|
*/
|
|
function parseJson(data) {
|
|
try {
|
|
return JSON.parse(data);
|
|
} catch (_) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/interceptors/XMLHttpRequest/utils/createResponse.ts
|
|
/**
|
|
* Creates a Fetch API `Response` instance from the given
|
|
* `XMLHttpRequest` instance and a response body.
|
|
*/
|
|
function createResponse(request, body) {
|
|
return new require_fetchUtils.FetchResponse(require_fetchUtils.FetchResponse.isResponseWithBody(request.status) ? body : null, {
|
|
url: request.responseURL,
|
|
status: request.status,
|
|
statusText: request.statusText,
|
|
headers: createHeadersFromXMLHttpRequestHeaders(request.getAllResponseHeaders())
|
|
});
|
|
}
|
|
function createHeadersFromXMLHttpRequestHeaders(headersString) {
|
|
const headers = new Headers();
|
|
const lines = headersString.split(/[\r\n]+/);
|
|
for (const line of lines) {
|
|
if (line.trim() === "") continue;
|
|
const [name, ...parts] = line.split(": ");
|
|
const value = parts.join(": ");
|
|
headers.append(name, value);
|
|
}
|
|
return headers;
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/interceptors/XMLHttpRequest/utils/getBodyByteLength.ts
|
|
/**
|
|
* Return a total byte length of the given request/response body.
|
|
* If the `Content-Length` header is present, it will be used as the byte length.
|
|
*/
|
|
async function getBodyByteLength(input) {
|
|
const explicitContentLength = input.headers.get("content-length");
|
|
if (explicitContentLength != null && explicitContentLength !== "") return Number(explicitContentLength);
|
|
return (await input.arrayBuffer()).byteLength;
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/interceptors/XMLHttpRequest/XMLHttpRequestController.ts
|
|
const kIsRequestHandled = Symbol("kIsRequestHandled");
|
|
const IS_NODE = (0, is_node_process.isNodeProcess)();
|
|
const kFetchRequest = Symbol("kFetchRequest");
|
|
/**
|
|
* An `XMLHttpRequest` instance controller that allows us
|
|
* to handle any given request instance (e.g. responding to it).
|
|
*/
|
|
var XMLHttpRequestController = class {
|
|
constructor(initialRequest, logger) {
|
|
this.initialRequest = initialRequest;
|
|
this.logger = logger;
|
|
this.method = "GET";
|
|
this.url = null;
|
|
this[kIsRequestHandled] = false;
|
|
this.events = /* @__PURE__ */ new Map();
|
|
this.uploadEvents = /* @__PURE__ */ new Map();
|
|
this.requestId = require_fetchUtils.createRequestId();
|
|
this.requestHeaders = new Headers();
|
|
this.responseBuffer = new Uint8Array();
|
|
this.request = createProxy(initialRequest, {
|
|
setProperty: ([propertyName, nextValue], invoke) => {
|
|
switch (propertyName) {
|
|
case "ontimeout": {
|
|
const eventName = propertyName.slice(2);
|
|
/**
|
|
* @note Proxy callbacks to event listeners because JSDOM has trouble
|
|
* translating these properties to callbacks. It seemed to be operating
|
|
* on events exclusively.
|
|
*/
|
|
this.request.addEventListener(eventName, nextValue);
|
|
return invoke();
|
|
}
|
|
default: return invoke();
|
|
}
|
|
},
|
|
methodCall: ([methodName, args], invoke) => {
|
|
switch (methodName) {
|
|
case "open": {
|
|
const [method, url] = args;
|
|
if (typeof url === "undefined") {
|
|
this.method = "GET";
|
|
this.url = toAbsoluteUrl(method);
|
|
} else {
|
|
this.method = method;
|
|
this.url = toAbsoluteUrl(url);
|
|
}
|
|
this.logger = this.logger.extend(`${this.method} ${this.url.href}`);
|
|
this.logger.info("open", this.method, this.url.href);
|
|
return invoke();
|
|
}
|
|
case "addEventListener": {
|
|
const [eventName, listener] = args;
|
|
this.registerEvent(eventName, listener);
|
|
this.logger.info("addEventListener", eventName, listener);
|
|
return invoke();
|
|
}
|
|
case "setRequestHeader": {
|
|
const [name, value] = args;
|
|
this.requestHeaders.set(name, value);
|
|
this.logger.info("setRequestHeader", name, value);
|
|
return invoke();
|
|
}
|
|
case "send": {
|
|
const [body] = args;
|
|
this.request.addEventListener("load", () => {
|
|
if (typeof this.onResponse !== "undefined") {
|
|
const fetchResponse = createResponse(
|
|
this.request,
|
|
/**
|
|
* The `response` property is the right way to read
|
|
* the ambiguous response body, as the request's "responseType" may differ.
|
|
* @see https://xhr.spec.whatwg.org/#the-response-attribute
|
|
*/
|
|
this.request.response
|
|
);
|
|
this.onResponse.call(this, {
|
|
response: fetchResponse,
|
|
isMockedResponse: this[kIsRequestHandled],
|
|
request: fetchRequest,
|
|
requestId: this.requestId
|
|
});
|
|
}
|
|
});
|
|
const requestBody = typeof body === "string" ? require_bufferUtils.encodeBuffer(body) : body;
|
|
const fetchRequest = this.toFetchApiRequest(requestBody);
|
|
this[kFetchRequest] = fetchRequest.clone();
|
|
/**
|
|
* @note Start request handling on the next tick so that the user
|
|
* could add event listeners for "loadend" before the interceptor fires it.
|
|
*/
|
|
queueMicrotask(() => {
|
|
(this.onRequest?.call(this, {
|
|
request: fetchRequest,
|
|
requestId: this.requestId
|
|
}) || Promise.resolve()).finally(() => {
|
|
if (!this[kIsRequestHandled]) {
|
|
this.logger.info("request callback settled but request has not been handled (readystate %d), performing as-is...", this.request.readyState);
|
|
/**
|
|
* @note Set the intercepted request ID on the original request in Node.js
|
|
* so that if it triggers any other interceptors, they don't attempt
|
|
* to process it once again.
|
|
*
|
|
* For instance, XMLHttpRequest is often implemented via "http.ClientRequest"
|
|
* and we don't want for both XHR and ClientRequest interceptors to
|
|
* handle the same request at the same time (e.g. emit the "response" event twice).
|
|
*/
|
|
if (IS_NODE) this.request.setRequestHeader(require_fetchUtils.INTERNAL_REQUEST_ID_HEADER_NAME, this.requestId);
|
|
return invoke();
|
|
}
|
|
});
|
|
});
|
|
break;
|
|
}
|
|
default: return invoke();
|
|
}
|
|
}
|
|
});
|
|
/**
|
|
* Proxy the `.upload` property to gather the event listeners/callbacks.
|
|
*/
|
|
define(this.request, "upload", createProxy(this.request.upload, {
|
|
setProperty: ([propertyName, nextValue], invoke) => {
|
|
switch (propertyName) {
|
|
case "onloadstart":
|
|
case "onprogress":
|
|
case "onaboart":
|
|
case "onerror":
|
|
case "onload":
|
|
case "ontimeout":
|
|
case "onloadend": {
|
|
const eventName = propertyName.slice(2);
|
|
this.registerUploadEvent(eventName, nextValue);
|
|
}
|
|
}
|
|
return invoke();
|
|
},
|
|
methodCall: ([methodName, args], invoke) => {
|
|
switch (methodName) {
|
|
case "addEventListener": {
|
|
const [eventName, listener] = args;
|
|
this.registerUploadEvent(eventName, listener);
|
|
this.logger.info("upload.addEventListener", eventName, listener);
|
|
return invoke();
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
registerEvent(eventName, listener) {
|
|
const nextEvents = (this.events.get(eventName) || []).concat(listener);
|
|
this.events.set(eventName, nextEvents);
|
|
this.logger.info("registered event \"%s\"", eventName, listener);
|
|
}
|
|
registerUploadEvent(eventName, listener) {
|
|
const nextEvents = (this.uploadEvents.get(eventName) || []).concat(listener);
|
|
this.uploadEvents.set(eventName, nextEvents);
|
|
this.logger.info("registered upload event \"%s\"", eventName, listener);
|
|
}
|
|
/**
|
|
* Responds to the current request with the given
|
|
* Fetch API `Response` instance.
|
|
*/
|
|
async respondWith(response) {
|
|
/**
|
|
* @note Since `XMLHttpRequestController` delegates the handling of the responses
|
|
* to the "load" event listener that doesn't distinguish between the mocked and original
|
|
* responses, mark the request that had a mocked response with a corresponding symbol.
|
|
*
|
|
* Mark this request as having a mocked response immediately since
|
|
* calculating request/response total body length is asynchronous.
|
|
*/
|
|
this[kIsRequestHandled] = true;
|
|
/**
|
|
* Dispatch request upload events for requests with a body.
|
|
* @see https://github.com/mswjs/interceptors/issues/573
|
|
*/
|
|
if (this[kFetchRequest]) {
|
|
const totalRequestBodyLength = await getBodyByteLength(this[kFetchRequest]);
|
|
this.trigger("loadstart", this.request.upload, {
|
|
loaded: 0,
|
|
total: totalRequestBodyLength
|
|
});
|
|
this.trigger("progress", this.request.upload, {
|
|
loaded: totalRequestBodyLength,
|
|
total: totalRequestBodyLength
|
|
});
|
|
this.trigger("load", this.request.upload, {
|
|
loaded: totalRequestBodyLength,
|
|
total: totalRequestBodyLength
|
|
});
|
|
this.trigger("loadend", this.request.upload, {
|
|
loaded: totalRequestBodyLength,
|
|
total: totalRequestBodyLength
|
|
});
|
|
}
|
|
this.logger.info("responding with a mocked response: %d %s", response.status, response.statusText);
|
|
define(this.request, "status", response.status);
|
|
define(this.request, "statusText", response.statusText);
|
|
define(this.request, "responseURL", this.url.href);
|
|
this.request.getResponseHeader = new Proxy(this.request.getResponseHeader, { apply: (_, __, args) => {
|
|
this.logger.info("getResponseHeader", args[0]);
|
|
if (this.request.readyState < this.request.HEADERS_RECEIVED) {
|
|
this.logger.info("headers not received yet, returning null");
|
|
return null;
|
|
}
|
|
const headerValue = response.headers.get(args[0]);
|
|
this.logger.info("resolved response header \"%s\" to", args[0], headerValue);
|
|
return headerValue;
|
|
} });
|
|
this.request.getAllResponseHeaders = new Proxy(this.request.getAllResponseHeaders, { apply: () => {
|
|
this.logger.info("getAllResponseHeaders");
|
|
if (this.request.readyState < this.request.HEADERS_RECEIVED) {
|
|
this.logger.info("headers not received yet, returning empty string");
|
|
return "";
|
|
}
|
|
const allHeaders = Array.from(response.headers.entries()).map(([headerName, headerValue]) => {
|
|
return `${headerName}: ${headerValue}`;
|
|
}).join("\r\n");
|
|
this.logger.info("resolved all response headers to", allHeaders);
|
|
return allHeaders;
|
|
} });
|
|
Object.defineProperties(this.request, {
|
|
response: {
|
|
enumerable: true,
|
|
configurable: false,
|
|
get: () => this.response
|
|
},
|
|
responseText: {
|
|
enumerable: true,
|
|
configurable: false,
|
|
get: () => this.responseText
|
|
},
|
|
responseXML: {
|
|
enumerable: true,
|
|
configurable: false,
|
|
get: () => this.responseXML
|
|
}
|
|
});
|
|
const totalResponseBodyLength = await getBodyByteLength(response.clone());
|
|
this.logger.info("calculated response body length", totalResponseBodyLength);
|
|
this.trigger("loadstart", this.request, {
|
|
loaded: 0,
|
|
total: totalResponseBodyLength
|
|
});
|
|
this.setReadyState(this.request.HEADERS_RECEIVED);
|
|
this.setReadyState(this.request.LOADING);
|
|
const finalizeResponse = () => {
|
|
this.logger.info("finalizing the mocked response...");
|
|
this.setReadyState(this.request.DONE);
|
|
this.trigger("load", this.request, {
|
|
loaded: this.responseBuffer.byteLength,
|
|
total: totalResponseBodyLength
|
|
});
|
|
this.trigger("loadend", this.request, {
|
|
loaded: this.responseBuffer.byteLength,
|
|
total: totalResponseBodyLength
|
|
});
|
|
};
|
|
if (response.body) {
|
|
this.logger.info("mocked response has body, streaming...");
|
|
const reader = response.body.getReader();
|
|
const readNextResponseBodyChunk = async () => {
|
|
const { value, done } = await reader.read();
|
|
if (done) {
|
|
this.logger.info("response body stream done!");
|
|
finalizeResponse();
|
|
return;
|
|
}
|
|
if (value) {
|
|
this.logger.info("read response body chunk:", value);
|
|
this.responseBuffer = concatArrayBuffer(this.responseBuffer, value);
|
|
this.trigger("progress", this.request, {
|
|
loaded: this.responseBuffer.byteLength,
|
|
total: totalResponseBodyLength
|
|
});
|
|
}
|
|
readNextResponseBodyChunk();
|
|
};
|
|
readNextResponseBodyChunk();
|
|
} else finalizeResponse();
|
|
}
|
|
responseBufferToText() {
|
|
return require_bufferUtils.decodeBuffer(this.responseBuffer);
|
|
}
|
|
get response() {
|
|
this.logger.info("getResponse (responseType: %s)", this.request.responseType);
|
|
if (this.request.readyState !== this.request.DONE) return null;
|
|
switch (this.request.responseType) {
|
|
case "json": {
|
|
const responseJson = parseJson(this.responseBufferToText());
|
|
this.logger.info("resolved response JSON", responseJson);
|
|
return responseJson;
|
|
}
|
|
case "arraybuffer": {
|
|
const arrayBuffer = require_bufferUtils.toArrayBuffer(this.responseBuffer);
|
|
this.logger.info("resolved response ArrayBuffer", arrayBuffer);
|
|
return arrayBuffer;
|
|
}
|
|
case "blob": {
|
|
const mimeType = this.request.getResponseHeader("Content-Type") || "text/plain";
|
|
const responseBlob = new Blob([this.responseBufferToText()], { type: mimeType });
|
|
this.logger.info("resolved response Blob (mime type: %s)", responseBlob, mimeType);
|
|
return responseBlob;
|
|
}
|
|
default: {
|
|
const responseText = this.responseBufferToText();
|
|
this.logger.info("resolving \"%s\" response type as text", this.request.responseType, responseText);
|
|
return responseText;
|
|
}
|
|
}
|
|
}
|
|
get responseText() {
|
|
/**
|
|
* Throw when trying to read the response body as text when the
|
|
* "responseType" doesn't expect text. This just respects the spec better.
|
|
* @see https://xhr.spec.whatwg.org/#the-responsetext-attribute
|
|
*/
|
|
(0, outvariant.invariant)(this.request.responseType === "" || this.request.responseType === "text", "InvalidStateError: The object is in invalid state.");
|
|
if (this.request.readyState !== this.request.LOADING && this.request.readyState !== this.request.DONE) return "";
|
|
const responseText = this.responseBufferToText();
|
|
this.logger.info("getResponseText: \"%s\"", responseText);
|
|
return responseText;
|
|
}
|
|
get responseXML() {
|
|
(0, outvariant.invariant)(this.request.responseType === "" || this.request.responseType === "document", "InvalidStateError: The object is in invalid state.");
|
|
if (this.request.readyState !== this.request.DONE) return null;
|
|
const contentType = this.request.getResponseHeader("Content-Type") || "";
|
|
if (typeof DOMParser === "undefined") {
|
|
console.warn("Cannot retrieve XMLHttpRequest response body as XML: DOMParser is not defined. You are likely using an environment that is not browser or does not polyfill browser globals correctly.");
|
|
return null;
|
|
}
|
|
if (isDomParserSupportedType(contentType)) return new DOMParser().parseFromString(this.responseBufferToText(), contentType);
|
|
return null;
|
|
}
|
|
errorWith(error) {
|
|
/**
|
|
* @note Mark this request as handled even if it received a mock error.
|
|
* This prevents the controller from trying to perform this request as-is.
|
|
*/
|
|
this[kIsRequestHandled] = true;
|
|
this.logger.info("responding with an error");
|
|
this.setReadyState(this.request.DONE);
|
|
this.trigger("error", this.request);
|
|
this.trigger("loadend", this.request);
|
|
}
|
|
/**
|
|
* Transitions this request's `readyState` to the given one.
|
|
*/
|
|
setReadyState(nextReadyState) {
|
|
this.logger.info("setReadyState: %d -> %d", this.request.readyState, nextReadyState);
|
|
if (this.request.readyState === nextReadyState) {
|
|
this.logger.info("ready state identical, skipping transition...");
|
|
return;
|
|
}
|
|
define(this.request, "readyState", nextReadyState);
|
|
this.logger.info("set readyState to: %d", nextReadyState);
|
|
if (nextReadyState !== this.request.UNSENT) {
|
|
this.logger.info("triggering \"readystatechange\" event...");
|
|
this.trigger("readystatechange", this.request);
|
|
}
|
|
}
|
|
/**
|
|
* Triggers given event on the `XMLHttpRequest` instance.
|
|
*/
|
|
trigger(eventName, target, options) {
|
|
const callback = target[`on${eventName}`];
|
|
const event = createEvent(target, eventName, options);
|
|
this.logger.info("trigger \"%s\"", eventName, options || "");
|
|
if (typeof callback === "function") {
|
|
this.logger.info("found a direct \"%s\" callback, calling...", eventName);
|
|
callback.call(target, event);
|
|
}
|
|
const events = target instanceof XMLHttpRequestUpload ? this.uploadEvents : this.events;
|
|
for (const [registeredEventName, listeners] of events) if (registeredEventName === eventName) {
|
|
this.logger.info("found %d listener(s) for \"%s\" event, calling...", listeners.length, eventName);
|
|
listeners.forEach((listener) => listener.call(target, event));
|
|
}
|
|
}
|
|
/**
|
|
* Converts this `XMLHttpRequest` instance into a Fetch API `Request` instance.
|
|
*/
|
|
toFetchApiRequest(body) {
|
|
this.logger.info("converting request to a Fetch API Request...");
|
|
const resolvedBody = body instanceof Document ? body.documentElement.innerText : body;
|
|
const fetchRequest = new Request(this.url.href, {
|
|
method: this.method,
|
|
headers: this.requestHeaders,
|
|
credentials: this.request.withCredentials ? "include" : "same-origin",
|
|
body: ["GET", "HEAD"].includes(this.method.toUpperCase()) ? null : resolvedBody
|
|
});
|
|
define(fetchRequest, "headers", createProxy(fetchRequest.headers, { methodCall: ([methodName, args], invoke) => {
|
|
switch (methodName) {
|
|
case "append":
|
|
case "set": {
|
|
const [headerName, headerValue] = args;
|
|
this.request.setRequestHeader(headerName, headerValue);
|
|
break;
|
|
}
|
|
case "delete": {
|
|
const [headerName] = args;
|
|
console.warn(`XMLHttpRequest: Cannot remove a "${headerName}" header from the Fetch API representation of the "${fetchRequest.method} ${fetchRequest.url}" request. XMLHttpRequest headers cannot be removed.`);
|
|
break;
|
|
}
|
|
}
|
|
return invoke();
|
|
} }));
|
|
require_getRawRequest.setRawRequest(fetchRequest, this.request);
|
|
this.logger.info("converted request to a Fetch API Request!", fetchRequest);
|
|
return fetchRequest;
|
|
}
|
|
};
|
|
function toAbsoluteUrl(url) {
|
|
/**
|
|
* @note XMLHttpRequest interceptor may run in environments
|
|
* that implement XMLHttpRequest but don't implement "location"
|
|
* (for example, React Native). If that's the case, return the
|
|
* input URL as-is (nothing to be relative to).
|
|
* @see https://github.com/mswjs/msw/issues/1777
|
|
*/
|
|
if (typeof location === "undefined") return new URL(url);
|
|
return new URL(url.toString(), location.href);
|
|
}
|
|
function define(target, property, value) {
|
|
Reflect.defineProperty(target, property, {
|
|
writable: true,
|
|
enumerable: true,
|
|
value
|
|
});
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/interceptors/XMLHttpRequest/XMLHttpRequestProxy.ts
|
|
/**
|
|
* Create a proxied `XMLHttpRequest` class.
|
|
* The proxied class establishes spies on certain methods,
|
|
* allowing us to intercept requests and respond to them.
|
|
*/
|
|
function createXMLHttpRequestProxy({ emitter, logger }) {
|
|
return new Proxy(globalThis.XMLHttpRequest, { construct(target, args, newTarget) {
|
|
logger.info("constructed new XMLHttpRequest");
|
|
const originalRequest = Reflect.construct(target, args, newTarget);
|
|
/**
|
|
* @note Forward prototype descriptors onto the proxied object.
|
|
* XMLHttpRequest is implemented in JSDOM in a way that assigns
|
|
* a bunch of descriptors, like "set responseType()" on the prototype.
|
|
* With this propagation, we make sure that those descriptors trigger
|
|
* when the user operates with the proxied request instance.
|
|
*/
|
|
const prototypeDescriptors = Object.getOwnPropertyDescriptors(target.prototype);
|
|
for (const propertyName in prototypeDescriptors) Reflect.defineProperty(originalRequest, propertyName, prototypeDescriptors[propertyName]);
|
|
const xhrRequestController = new XMLHttpRequestController(originalRequest, logger);
|
|
xhrRequestController.onRequest = async function({ request, requestId }) {
|
|
const controller = new require_fetchUtils.RequestController(request, {
|
|
passthrough: () => {
|
|
this.logger.info("no mocked response received, performing request as-is...");
|
|
},
|
|
respondWith: async (response) => {
|
|
if (require_handleRequest.isResponseError(response)) {
|
|
this.errorWith(/* @__PURE__ */ new TypeError("Network error"));
|
|
return;
|
|
}
|
|
await this.respondWith(response);
|
|
},
|
|
errorWith: (reason) => {
|
|
this.logger.info("request errored!", { error: reason });
|
|
if (reason instanceof Error) this.errorWith(reason);
|
|
}
|
|
});
|
|
this.logger.info("awaiting mocked response...");
|
|
this.logger.info("emitting the \"request\" event for %s listener(s)...", emitter.listenerCount("request"));
|
|
await require_handleRequest.handleRequest({
|
|
request,
|
|
requestId,
|
|
controller,
|
|
emitter
|
|
});
|
|
};
|
|
xhrRequestController.onResponse = async function({ response, isMockedResponse, request, requestId }) {
|
|
this.logger.info("emitting the \"response\" event for %s listener(s)...", emitter.listenerCount("response"));
|
|
emitter.emit("response", {
|
|
response,
|
|
isMockedResponse,
|
|
request,
|
|
requestId
|
|
});
|
|
};
|
|
return xhrRequestController.request;
|
|
} });
|
|
}
|
|
|
|
//#endregion
|
|
//#region src/interceptors/XMLHttpRequest/index.ts
|
|
var XMLHttpRequestInterceptor = class XMLHttpRequestInterceptor extends require_fetchUtils.Interceptor {
|
|
static {
|
|
this.interceptorSymbol = Symbol("xhr");
|
|
}
|
|
constructor() {
|
|
super(XMLHttpRequestInterceptor.interceptorSymbol);
|
|
}
|
|
checkEnvironment() {
|
|
return require_hasConfigurableGlobal.hasConfigurableGlobal("XMLHttpRequest");
|
|
}
|
|
setup() {
|
|
const logger = this.logger.extend("setup");
|
|
logger.info("patching \"XMLHttpRequest\" module...");
|
|
const PureXMLHttpRequest = globalThis.XMLHttpRequest;
|
|
(0, outvariant.invariant)(!PureXMLHttpRequest[require_glossary.IS_PATCHED_MODULE], "Failed to patch the \"XMLHttpRequest\" module: already patched.");
|
|
globalThis.XMLHttpRequest = createXMLHttpRequestProxy({
|
|
emitter: this.emitter,
|
|
logger: this.logger
|
|
});
|
|
logger.info("native \"XMLHttpRequest\" module patched!", globalThis.XMLHttpRequest.name);
|
|
Object.defineProperty(globalThis.XMLHttpRequest, require_glossary.IS_PATCHED_MODULE, {
|
|
enumerable: true,
|
|
configurable: true,
|
|
value: true
|
|
});
|
|
this.subscriptions.push(() => {
|
|
Object.defineProperty(globalThis.XMLHttpRequest, require_glossary.IS_PATCHED_MODULE, { value: void 0 });
|
|
globalThis.XMLHttpRequest = PureXMLHttpRequest;
|
|
logger.info("native \"XMLHttpRequest\" module restored!", globalThis.XMLHttpRequest.name);
|
|
});
|
|
}
|
|
};
|
|
|
|
//#endregion
|
|
Object.defineProperty(exports, 'XMLHttpRequestInterceptor', {
|
|
enumerable: true,
|
|
get: function () {
|
|
return XMLHttpRequestInterceptor;
|
|
}
|
|
});
|
|
//# sourceMappingURL=XMLHttpRequest-B7kJdYYI.cjs.map
|