mock-xmlhttprequest
Version:
XMLHttpRequest mock for testing
747 lines (742 loc) • 30.3 kB
JavaScript
/**
* mock-xmlhttprequest v8.4.1
* (c) 2025 Bertrand Guay-Paquet
* @license MIT
*/
'use strict';
const HeadersContainer = require('./HeadersContainer.cjs');
const MockXhrRequest = require('./MockXhrRequest.cjs');
const RequestData = require('./RequestData.cjs');
const XhrEvent = require('./XhrEvent.cjs');
const XhrProgressEvent = require('./XhrProgressEvent.cjs');
const Utils = require('./Utils.cjs');
const XhrEventTarget = require('./XhrEventTarget.cjs');
const RESPONSE_TYPES = ['', 'arraybuffer', 'blob', 'document', 'json', 'text'];
/**
* XMLHttpRequest mock for testing.
* Based on https://xhr.spec.whatwg.org version '15 August 2022'.
*
* Supports:
* - Events and states
* - open(), setRequestHeader(), send() and abort()
* - Upload and download progress events
* - Response status, statusText, headers and body
* - The timeout attribute (can be disabled)
* - Simulating a network error (see setNetworkError())
* - Simulating a request timeout (see setRequestTimeout())
*
* Partial support:
* - overrideMimeType(): throws when required, but has no other effect.
* - responseType: '', 'text' and 'json' are fully supported. The responseType values have no
* effect on the response body passed to setResponseBody().
* - responseXml: the response body is not converted to a document response. To get a document
* response, pass it directly as the response body in setResponseBody().
* - responseUrl: the final request URL after redirects isn't automatically set. This can be
* emulated in a request handler.
*
* Not supported:
* - Synchronous requests (i.e. async set to false in open())
* - Parsing the request URL in open() and throwing SyntaxError on failure.
*/
class MockXhr extends XhrEventTarget {
constructor() {
super();
this.UNSENT = MockXhr.UNSENT;
this.OPENED = MockXhr.OPENED;
this.HEADERS_RECEIVED = MockXhr.HEADERS_RECEIVED;
this.LOADING = MockXhr.LOADING;
this.DONE = MockXhr.DONE;
this._authorRequestHeaders = new HeadersContainer();
this._readyState = MockXhr.UNSENT;
this._timeout = 0;
this._crossOriginCredentials = false;
this._uploadObject = new XhrEventTarget(this);
this.responseURL = '';
this._responseType = '';
this._response = makeNetworkErrorResponse();
this._timeoutReference = 0;
this.onreadystatechange = null;
this.timeoutEnabled = true;
MockXhr.onCreate?.(this);
}
get onreadystatechange() {
return this._getEventHandlerProperty('readystatechange');
}
set onreadystatechange(value) {
this._setEventHandlerProperty('readystatechange', value);
}
/**
* @returns The current active request, if any
*/
get currentRequest() { return this._currentRequest; }
/**
* @returns All response headers as an object. The header names are in lower-case.
*/
getResponseHeadersHash() {
return this._response.headers.getHash();
}
//------------------------
// MockXhrResponseReceiver
//------------------------
/**
* Fire a request upload progress event.
*
* @param request Originating request
* @param requestBodyTransmitted Bytes transmitted
* @see {@link https://xhr.spec.whatwg.org/#the-send()-method "processRequestBodyChunkLength" steps}
*/
uploadProgress(request, requestBodyTransmitted) {
// Only act if the originating request is the current active request
if (this._currentRequest?.requestData === request) {
if (!this._sendFlag) {
throw new Error('Mock usage error detected: call send() first (the "send() flag" is not set)');
}
if (this._uploadCompleteFlag) {
throw new Error('Mock usage error detected: upload already completed (the "upload complete flag" is set)');
}
const requestBodyLength = request.getRequestBodySize();
if (requestBodyTransmitted > requestBodyLength) {
throw new Error('Mock usage error detected: upload progress "requestBodyTransmitted" ' +
`(${requestBodyTransmitted}) is greater than "requestBodyLength" (${requestBodyLength})`);
}
// Don't throttle events based on elapsed time because it would make tests much slower and
// harder to write.
if (this._uploadListenerFlag) {
// If no listeners were registered before send(), no upload events should be fired.
this._fireUploadProgressEvent('progress', requestBodyTransmitted, requestBodyLength);
}
}
}
/**
* Set the response headers. Changes the request's readyState to HEADERS_RECEIVED.
*
* @param request Originating request
* @param status Response http status (default 200)
* @param headers Name-value headers (optional)
* @param statusText Response http status text (optional)
*/
setResponseHeaders(request, status, headers, statusText) {
// Only act if the originating request is the current active request
if (this._currentRequest?.requestData === request) {
if (!this._sendFlag) {
throw new Error('Mock usage error detected: call send() first (the "send() flag" is not set)');
}
if (this._readyState !== MockXhr.OPENED) {
throw new Error(`Mock usage error detected: readyState is ${this._readyState}, but it must be OPENED (${MockXhr.OPENED})`);
}
if (request.body) {
this._processRequestEndOfBody(request.getRequestBodySize(), request.getRequestBodySize());
}
status = typeof status === 'number' ? status : 200;
const statusMessage = statusText ?? Utils.getStatusText(status);
this._processResponse({
status,
statusMessage,
headers: new HeadersContainer(headers),
});
}
}
/**
* Fire a response progress event. Changes the request's readyState to LOADING.
*
* @param request Originating request
* @param receivedBytesLength Received bytes' length
* @param length Body length in bytes
* @see {@link https://xhr.spec.whatwg.org/#the-send()-method "processBodyChunk" steps}
*/
downloadProgress(request, receivedBytesLength, length) {
// Only act if the originating request is the current active request
if (this._currentRequest?.requestData === request) {
if (this._readyState !== MockXhr.HEADERS_RECEIVED && this._readyState !== MockXhr.LOADING) {
throw new Error(`Mock usage error detected: readyState is ${this._readyState}, but it must ` +
`be HEADERS_RECEIVED (${MockXhr.HEADERS_RECEIVED}) or LOADING (${MockXhr.LOADING})`);
}
if (this._readyState === MockXhr.HEADERS_RECEIVED) {
this._readyState = MockXhr.LOADING;
}
// As stated in https://xhr.spec.whatwg.org/#the-send()-method
// Web compatibility is the reason readystatechange fires more often than state changes.
this._fireReadyStateChangeEvent();
this._fireProgressEvent('progress', receivedBytesLength, length);
}
}
/**
* Set the response body. Changes the request's readyState to DONE.
*
* @param request Originating request
* @param body Response body
*/
setResponseBody(request, body) {
// Only act if the originating request is the current active request
if (this._currentRequest?.requestData === request) {
if (!this._sendFlag) {
throw new Error('Mock usage error detected: call send() first (the "send() flag" is not set)');
}
if (this._readyState !== MockXhr.OPENED &&
this._readyState !== MockXhr.HEADERS_RECEIVED &&
this._readyState !== MockXhr.LOADING) {
throw new Error(`Mock usage error detected: readyState is ${this._readyState}, but it must be ` +
`OPENED (${MockXhr.OPENED}), HEADERS_RECEIVED (${MockXhr.HEADERS_RECEIVED}) or LOADING (${MockXhr.LOADING})`);
}
if (this._readyState === MockXhr.OPENED) {
// Apply default "200 - OK" response headers with a Content-Length if the user didn't call
// setResponseHeaders() before this point.
const headers = { 'content-length': String(Utils.getBodyByteSize(body)) };
this.setResponseHeaders(request, 200, headers);
}
// As stated in https://xhr.spec.whatwg.org/#the-send()-method
// Web compatibility is the reason readystatechange fires more often than
// state changes.
this._readyState = MockXhr.LOADING;
this._fireReadyStateChangeEvent();
this._response.body = body ?? null;
this._handleResponseEndOfBody();
}
}
/**
* Simulate a network error. Changes the request's readyState to DONE.
*
* @param request Originating request
*/
setNetworkError(request) {
// Only act if the originating request is the current active request
if (this._currentRequest?.requestData === request) {
if (!this._sendFlag) {
throw new Error('Mock usage error detected: call send() first (the "send() flag" is not set)');
}
this._processResponse(makeNetworkErrorResponse());
}
}
/**
* Simulate a request timeout. Changes the request's readyState to DONE.
*
* @param request Originating request
*/
setRequestTimeout(request) {
// Only act if the originating request is the current active request
if (this._currentRequest?.requestData === request) {
if (!this._sendFlag) {
throw new Error('Mock usage error detected: call send() first (the "send() flag" is not set)');
}
if (this.timeout === 0) {
throw new Error('Mock usage error detected: the timeout attribute must be greater than 0 for a timeout to occur');
}
this._timedOutFlag = true;
this._terminateFetchController();
this._processResponse(makeNetworkErrorResponse());
}
}
//-------
// States
//-------
/**
* @returns Client's state
* @see {@link https://xhr.spec.whatwg.org/#dom-xmlhttprequest-readystate}
*/
get readyState() { return this._readyState; }
//--------
// Request
//--------
/**
* Set the request method and url.
*
* @param method Request HTTP method (GET, POST, etc.)
* @param url Request url
* @param async Async request flag (only true or omitted is supported)
* @see {@link https://xhr.spec.whatwg.org/#the-open()-method}
*/
open(method, url, async) {
if (!async && arguments.length > 2) {
throw new Error('async = false is not supported.');
}
if (!Utils.isRequestMethod(method)) {
throwError('SyntaxError', `Method "${method}" is not a method.`);
}
if (Utils.isRequestMethodForbidden(method)) {
throwError('SecurityError', `Method "${method}" forbidden.`);
}
method = Utils.normalizeHTTPMethodName(method);
// Skip parsing the url and setting the username and password
this._terminateFetchController();
// Set variables
this._sendFlag = false;
this._uploadListenerFlag = false;
this._requestMethod = method;
this._requestUrl = url.toString();
this._authorRequestHeaders.reset();
this._response = makeNetworkErrorResponse();
if (this._readyState !== MockXhr.OPENED) {
this._readyState = MockXhr.OPENED;
this._fireReadyStateChangeEvent();
}
}
/**
* Add a request header value.
*
* @param name Header name
* @param value Header value
* @see {@link https://xhr.spec.whatwg.org/#the-setrequestheader()-method}
*/
setRequestHeader(name, value) {
if (this._readyState !== MockXhr.OPENED || this._sendFlag) {
throwError('InvalidStateError');
}
if (typeof name !== 'string' || typeof value !== 'string') {
throw new SyntaxError();
}
// Normalize value
value = value.trim();
if (!Utils.isHeaderName(name)) {
throwError('SyntaxError', `Name "${name}" is not a header name.`);
}
else if (!Utils.isHeaderValue(value)) {
throwError('SyntaxError', `Value "${value}" is not a header value.`);
}
if (Utils.isRequestHeaderForbidden(name)) {
return;
}
this._authorRequestHeaders.addHeader(name, value);
}
/**
* @returns timeout attribute
* @see {@link https://xhr.spec.whatwg.org/#dom-xmlhttprequest-timeout}
*/
get timeout() { return this._timeout; }
/**
* @param value timeout value
* @see {@link https://xhr.spec.whatwg.org/#dom-xmlhttprequest-timeout}
*/
set timeout(value) {
this._timeout = value;
// Use this._getPrototype() to get the value of timeoutEnabled on the most derived class'
// prototype. This allows overriding from a derived class.
if (this._sendFlag && this.timeoutEnabled && this._getPrototype().timeoutEnabled) {
this._scheduleRequestTimeout();
}
}
/**
* @returns withCredentials attribute
* @see {@link https://xhr.spec.whatwg.org/#dom-xmlhttprequest-withcredentials}
*/
get withCredentials() { return this._crossOriginCredentials; }
/**
* @param value withCredentials value
* @see {@link https://xhr.spec.whatwg.org/#dom-xmlhttprequest-withcredentials}
*/
set withCredentials(value) {
if ((this._readyState !== MockXhr.UNSENT && this._readyState !== MockXhr.OPENED) ||
this._sendFlag) {
throwError('InvalidStateError');
}
this._crossOriginCredentials = !!value;
}
/**
* @returns upload attribute
* @see {@link https://xhr.spec.whatwg.org/#the-upload-attribute}
*/
get upload() { return this._uploadObject; }
/**
* Initiate the request.
*
* @param body Request body
* @see {@link https://xhr.spec.whatwg.org/#the-send()-method}
*/
send(body = null) {
if (this._readyState !== MockXhr.OPENED || this._sendFlag) {
throwError('InvalidStateError');
}
if (this._requestMethod === 'GET' || this._requestMethod === 'HEAD') {
body = null;
}
if (body) {
let extractedContentType = null;
// Document body type not supported
// https://fetch.spec.whatwg.org/#bodyinit-safely-extract
{
let contentType = null;
if (typeof body === 'string') {
contentType = 'text/plain;charset=UTF-8';
}
else if (typeof FormData !== 'undefined' && body instanceof FormData) {
contentType = 'multipart/form-data; boundary=-----MochXhr1234';
}
else {
// As specified for Blob, but don't check with instanceof Blob to make mocks easier to do
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
const blobType = body.type;
if (typeof blobType === 'string') {
contentType = blobType;
}
}
extractedContentType = contentType;
}
const originalAuthorContentType = this._authorRequestHeaders.getHeader('Content-Type');
if (originalAuthorContentType !== null) ;
else if (extractedContentType !== null) {
this._authorRequestHeaders.addHeader('Content-Type', extractedContentType);
}
}
this._uploadListenerFlag = this._uploadObject.hasListeners();
const requestData = new RequestData(new HeadersContainer(this._authorRequestHeaders),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this._requestMethod,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
this._requestUrl, body, this._crossOriginCredentials);
const req = new MockXhrRequest(requestData, this);
this._uploadCompleteFlag = false;
this._timedOutFlag = false;
this._uploadCompleteFlag = req.body === null;
this._sendFlag = true;
this._fireProgressEvent('loadstart', 0, 0);
if (!this._uploadCompleteFlag && this._uploadListenerFlag) {
this._fireUploadProgressEvent('loadstart', 0, req.getRequestBodySize());
}
if (this._readyState !== MockXhr.OPENED || !this._sendFlag) {
return;
}
// Other interactions are triggered by the mock's MockXhrResponseReceiver API
this._currentRequest = req;
this._timeoutReference = Date.now();
this._scheduleRequestTimeout();
this._callOnSend(MockXhr.onSend);
const prototype = this._getPrototype();
if (prototype !== MockXhr) {
this._callOnSend(prototype.onSend);
}
this._callOnSend(this.onSend);
}
/**
* Abort the request.
* @see {@link https://xhr.spec.whatwg.org/#the-abort()-method}
*/
abort() {
this._terminateFetchController();
if ((this._readyState === MockXhr.OPENED && this._sendFlag) ||
this._readyState === MockXhr.HEADERS_RECEIVED ||
this._readyState === MockXhr.LOADING) {
this._requestErrorSteps('abort');
}
if (this._readyState === MockXhr.DONE) {
// No readystatechange event is dispatched.
this._readyState = MockXhr.UNSENT;
this._response = makeNetworkErrorResponse();
}
}
//---------
// Response
//---------
/**
* @returns status attribute
* @see {@link https://xhr.spec.whatwg.org/#dom-xmlhttprequest-status}
*/
get status() { return this._response.status; }
/**
* @returns statusText attribute
* @see {@link https://xhr.spec.whatwg.org/#the-statustext-attribute}
*/
get statusText() { return this._response.statusMessage; }
/**
* Get a response header value.
*
* @param name Header name
* @returns Header value
* @see {@link https://xhr.spec.whatwg.org/#dom-xmlhttprequest-getresponseheader}
*/
getResponseHeader(name) {
return this._response.headers.getHeader(name);
}
/**
* Get all response headers as a string.
*
* @returns Concatenated headers
* @see {@link https://xhr.spec.whatwg.org/#dom-xmlhttprequest-getallresponseheaders}
*/
getAllResponseHeaders() {
return this._response.headers.getAll();
}
/**
* Throws when required, but has no other effect.
*
* @param mime MIME type
* @see {@link https://xhr.spec.whatwg.org/#dom-xmlhttprequest-overridemimetype}
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
overrideMimeType(mime) {
if (this._readyState === MockXhr.LOADING || this._readyState === MockXhr.DONE) {
throwError('InvalidStateError');
}
// The other steps are not implemented
}
/**
* @returns responseType attribute
* @see {@link https://xhr.spec.whatwg.org/#dom-xmlhttprequest-responsetype}
*/
get responseType() { return this._responseType; }
/**
* @param value responseType value
* @see {@link https://xhr.spec.whatwg.org/#dom-xmlhttprequest-responsetype}
*/
set responseType(value) {
// Since this library is meant to run on node, skip the steps involving the Window object.
if (this._readyState === MockXhr.LOADING || this._readyState === MockXhr.DONE) {
throwError('InvalidStateError');
}
// The spec doesn't mandate throwing anything on invalid values since values must be of type
// XMLHttpRequestResponseType. Observed browser behavior is to ignore invalid values.
if (RESPONSE_TYPES.includes(value)) {
this._responseType = value;
}
}
/**
* @returns response attribute
* @see {@link https://xhr.spec.whatwg.org/#the-response-attribute}
*/
get response() {
if (this._responseType === '' || this._responseType === 'text') {
if (this._readyState !== MockXhr.LOADING && this._readyState !== MockXhr.DONE) {
return '';
}
return this._getTextResponse();
}
if (this._readyState !== MockXhr.DONE) {
return null;
}
// No specific handling of 'arraybuffer', 'blob', or 'document' response types
if (this._responseType === 'json') {
if (this._response.body === null) {
return null;
}
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return JSON.parse(this._response.body);
}
catch {
return null;
}
}
// Other responseTypes are sent as-is. They can be given directly by setResponseBody() anyway.
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return this._response.body;
}
/**
* @returns responseText attribute
* @see {@link https://xhr.spec.whatwg.org/#the-responsetext-attribute}
*/
get responseText() {
if (this._responseType !== '' && this._responseType !== 'text') {
throwError('InvalidStateError');
}
if (this._readyState !== MockXhr.LOADING && this._readyState !== MockXhr.DONE) {
return '';
}
return this._getTextResponse();
}
/**
* @returns responseXML attribute
* @see {@link https://xhr.spec.whatwg.org/#dom-xmlhttprequest-responsexml}
*/
get responseXML() {
if (this._responseType !== '' && this._responseType !== 'document') {
throwError('InvalidStateError');
}
if (this._readyState !== MockXhr.DONE) {
return null;
}
// The response body is not converted to a document response. To get a document
// response, pass it directly as the response body in setResponseBody().
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return this._response.body ?? '';
}
//------------------------------
// Request and response handling
//------------------------------
/**
* Steps for when the request upload is complete.
*
* @param requestBodyTransmitted Bytes transmitted
* @param requestBodyLength Request body's length
* @see {@link https://xhr.spec.whatwg.org/#the-send()-method "processRequestEndOfBody" steps}
*/
_processRequestEndOfBody(requestBodyTransmitted, requestBodyLength) {
this._uploadCompleteFlag = true;
// There must be at least one Upload listener registered before send() to emit upload progress
// events.
if (!this._uploadListenerFlag) {
return;
}
this._fireUploadProgressEvent('progress', requestBodyTransmitted, requestBodyLength);
this._fireUploadProgressEvent('load', requestBodyTransmitted, requestBodyLength);
this._fireUploadProgressEvent('loadend', requestBodyTransmitted, requestBodyLength);
}
/**
* Steps for when the response headers are received.
*
* @param response Response
* @see {@link https://xhr.spec.whatwg.org/#the-send()-method "processResponse" steps}
*/
_processResponse(response) {
this._response = response;
this._handleErrors();
if (this._response.isNetworkError) {
return;
}
this._readyState = MockXhr.HEADERS_RECEIVED;
this._fireReadyStateChangeEvent();
if (this._readyState !== MockXhr.HEADERS_RECEIVED) {
return;
}
if (this._response.body === null) {
this._handleResponseEndOfBody();
}
// Don't do the step that extract a length from the response's header list. The
// downloadProgress() method of the mock's MockXhrResponseReceiver API has a length argument
// that is used instead.
// Further steps are triggered by the mock's MockXhrResponseReceiver API
}
/**
* Handle response end-of-body for response.
*
* @see {@link https://xhr.spec.whatwg.org/#handle-response-end-of-body}
*/
_handleResponseEndOfBody() {
this._handleErrors();
if (this._response.isNetworkError) {
return;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const length = (this._response.body?.length ?? 0);
this._fireProgressEvent('progress', length, length);
this._readyState = MockXhr.DONE;
this._sendFlag = false;
this._terminateFetchController();
this._fireReadyStateChangeEvent();
this._fireProgressEvent('load', length, length);
this._fireProgressEvent('loadend', length, length);
}
/**
* The "handle errors" steps.
*
* @see {@link https://xhr.spec.whatwg.org/#handle-errors}
*/
_handleErrors() {
if (!this._sendFlag) {
return;
}
if (this._timedOutFlag) {
// Timeout
this._requestErrorSteps('timeout');
// We don't check the aborted flag because it can't be set in the context of this library.
// In a browser, the aborted flag can be set if the user presses Esc, the browser stop button,
// or the document the fetch is associated with is unloaded.
}
else if (this._response.isNetworkError) {
// Network error
this._requestErrorSteps('error');
}
}
/**
* The "request error steps" for event 'event'.
*
* @param event Event name
* @see {@link https://xhr.spec.whatwg.org/#request-error-steps}
*/
_requestErrorSteps(event) {
this._readyState = MockXhr.DONE;
this._sendFlag = false;
this._response = makeNetworkErrorResponse();
this._fireReadyStateChangeEvent();
if (!this._uploadCompleteFlag) {
this._uploadCompleteFlag = true;
if (this._uploadListenerFlag) {
// If no listeners were registered before send(), no upload events should be fired.
this._fireUploadProgressEvent(event, 0, 0);
this._fireUploadProgressEvent('loadend', 0, 0);
}
}
this._fireProgressEvent(event, 0, 0);
this._fireProgressEvent('loadend', 0, 0);
}
_getTextResponse() {
// Skip support for charset decoding as outlined in https://xhr.spec.whatwg.org/#text-response
// Users of this library should instead directly set a string response body as needed.
// The spec allows access to a text response while it's being received (i.e. LOADING state).
// This library current offers no way to simulate this.
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
return (this._response.body?.toString() ?? '');
}
//----------
// Internals
//----------
_callOnSend(onSend) {
// Saves the callback and request data in case they change before then() executes
if (onSend) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const request = this._currentRequest;
void Promise.resolve().then(() => { onSend.call(request, request, this); });
}
}
_terminateFetchController() {
delete this._currentRequest;
this._clearScheduleTimeout();
}
_fireProgressEvent(name, transmitted, length) {
this.dispatchEvent(new XhrProgressEvent(name, transmitted, length));
}
_fireUploadProgressEvent(name, transmitted, length) {
this._uploadObject.dispatchEvent(new XhrProgressEvent(name, transmitted, length));
}
_fireReadyStateChangeEvent() {
const event = new XhrEvent('readystatechange');
this.dispatchEvent(event);
}
_scheduleRequestTimeout() {
// Cancel any previous timeout task
this._clearScheduleTimeout();
if (this._timeout > 0) {
// The timeout delay must be measured relative to the start of fetching
// https://xhr.spec.whatwg.org/#the-timeout-attribute
const delay = Math.max(0, this._timeout - (Date.now() - this._timeoutReference));
this._timeoutTask = setTimeout(() => {
if (this._sendFlag) {
this._currentRequest?.setRequestTimeout();
}
}, delay);
}
}
_clearScheduleTimeout() {
if (this._timeoutTask) {
clearTimeout(this._timeoutTask);
}
delete this._timeoutTask;
}
_getPrototype() {
return this.constructor;
}
}
//-------
// States
//-------
MockXhr.UNSENT = 0;
MockXhr.OPENED = 1;
MockXhr.HEADERS_RECEIVED = 2;
MockXhr.LOADING = 3;
MockXhr.DONE = 4;
/**
* Global flag to enable the effects of the timeout attribute
*/
MockXhr.timeoutEnabled = true;
function throwError(type, text = '') {
const exception = new Error(text);
exception.name = type;
throw exception;
}
function makeNetworkErrorResponse() {
return {
isNetworkError: true,
status: 0,
statusMessage: '',
headers: new HeadersContainer(),
body: null,
};
}
module.exports = MockXhr;