From d8a0d2262c3fc8ef4313a1f05c5755aa94a3d4b6 Mon Sep 17 00:00:00 2001 From: Martin Brennan Date: Thu, 5 Aug 2021 08:23:01 +1000 Subject: [PATCH] DEV: Update pretender and fake-xml-http-request (#13937) We are still on a version of pretender since 2017 https://github.com/pretenderjs/pretender/releases/tag/v1.6.1 Since then many changes have been made, including adding support for xhr.upload. Upgrading will let us write proper acceptance tests for uppy, which uses XmlHTTPRequest internally including xhr.upload. Updates pretender to 3.4.7 and fake-xml-http-request to 2.1.2. Note: There have been no breaking changes in the releases that would affect us, mainly dropping support for old node versions. --- app/assets/javascripts/discourse/package.json | 2 +- app/assets/javascripts/yarn.lock | 12 +- lib/tasks/javascript.rake | 5 +- package.json | 2 +- .../javascripts/fake_xml_http_request.js | 900 ++++--- vendor/assets/javascripts/pretender.js | 2327 +++++++++++++---- yarn.lock | 18 +- 7 files changed, 2341 insertions(+), 925 deletions(-) diff --git a/app/assets/javascripts/discourse/package.json b/app/assets/javascripts/discourse/package.json index 99aaf01935..1f66aa4e7f 100644 --- a/app/assets/javascripts/discourse/package.json +++ b/app/assets/javascripts/discourse/package.json @@ -54,7 +54,7 @@ "message-bus-client": "^3.3.0", "mousetrap": "^1.6.5", "mousetrap-global-bind": "^1.1.0", - "pretender": "^3.4.3", + "pretender": "^3.4.7", "pretty-text": "^1.0.0", "qunit": "^2.14.0", "qunit-dom": "^1.6.0", diff --git a/app/assets/javascripts/yarn.lock b/app/assets/javascripts/yarn.lock index 76e24fe1e2..236a56ce41 100644 --- a/app/assets/javascripts/yarn.lock +++ b/app/assets/javascripts/yarn.lock @@ -5935,7 +5935,7 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fake-xml-http-request@^2.1.1: +fake-xml-http-request@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/fake-xml-http-request/-/fake-xml-http-request-2.1.2.tgz#f1786720cae50bbb46273035a0173414f3e85e74" integrity sha512-HaFMBi7r+oEC9iJNpc3bvcW7Z7iLmM26hPDmlb0mFwyANSsOQAtJxbdWsXITKOzZUyMYK0zYCv3h5yDj9TsiXg== @@ -9347,12 +9347,12 @@ prepend-http@^2.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= -pretender@^3.4.3: - version "3.4.3" - resolved "https://registry.yarnpkg.com/pretender/-/pretender-3.4.3.tgz#a3b4160516007075d29127262f3a0063d19896e9" - integrity sha512-AlbkBly9R8KR+R0sTCJ/ToOeEoUMtt52QVCetui5zoSmeLOU3S8oobFsyPLm1O2txR6t58qDNysqPnA1vVi8Hg== +pretender@^3.4.7: + version "3.4.7" + resolved "https://registry.yarnpkg.com/pretender/-/pretender-3.4.7.tgz#34a2ae2d1fc9db440a990d50e6c0f5481d8755fc" + integrity sha512-jkPAvt1BfRi0RKamweJdEcnjkeu7Es8yix3bJ+KgBC5VpG/Ln4JE3hYN6vJym4qprm8Xo5adhWpm3HCoft1dOw== dependencies: - fake-xml-http-request "^2.1.1" + fake-xml-http-request "^2.1.2" route-recognizer "^0.3.3" printf@^0.6.1: diff --git a/lib/tasks/javascript.rake b/lib/tasks/javascript.rake index 510a1ff0b7..2914940a78 100644 --- a/lib/tasks/javascript.rake +++ b/lib/tasks/javascript.rake @@ -188,7 +188,10 @@ def dependencies source: 'qunit/qunit/qunit.js' }, { - source: 'pretender/pretender.js' + source: 'pretender/dist/pretender.js' + }, + { + source: 'fake-xml-http-request/fake_xml_http_request.js' }, { source: 'sinon/pkg/sinon.js' diff --git a/package.json b/package.json index 39a7c9eeff..abcdfe65f9 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "chrome-launcher": "^0.12.0", "chrome-remote-interface": "^0.25", "lodash-cli": "https://github.com/lodash-archive/lodash-cli.git", - "pretender": "^1.6", + "pretender": "^3.4.7", "puppeteer": "1.20", "qunit": "2.8.0", "route-recognizer": "^0.3.3", diff --git a/vendor/assets/javascripts/fake_xml_http_request.js b/vendor/assets/javascripts/fake_xml_http_request.js index 0284c65fca..3537cc2a6f 100644 --- a/vendor/assets/javascripts/fake_xml_http_request.js +++ b/vendor/assets/javascripts/fake_xml_http_request.js @@ -1,480 +1,530 @@ -(function(undefined){ -/** - * Minimal Event interface implementation - * - * Original implementation by Sven Fuchs: https://gist.github.com/995028 - * Modifications and tests by Christian Johansen. - * - * @author Sven Fuchs (svenfuchs@artweb-design.de) - * @author Christian Johansen (christian@cjohansen.no) - * @license BSD - * - * Copyright (c) 2011 Sven Fuchs, Christian Johansen - */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.FakeXMLHttpRequest = factory()); +}(this, (function () { 'use strict'; -var _Event = function Event(type, bubbles, cancelable, target) { - this.type = type; - this.bubbles = bubbles; - this.cancelable = cancelable; - this.target = target; -}; + /** + * Minimal Event interface implementation + * + * Original implementation by Sven Fuchs: https://gist.github.com/995028 + * Modifications and tests by Christian Johansen. + * + * @author Sven Fuchs (svenfuchs@artweb-design.de) + * @author Christian Johansen (christian@cjohansen.no) + * @license BSD + * + * Copyright (c) 2011 Sven Fuchs, Christian Johansen + */ -_Event.prototype = { - stopPropagation: function () {}, - preventDefault: function () { - this.defaultPrevented = true; - } -}; + var _Event = function Event(type, bubbles, cancelable, target) { + this.type = type; + this.bubbles = bubbles; + this.cancelable = cancelable; + this.target = target; + }; -/* - Used to set the statusText property of an xhr object -*/ -var httpStatusCodes = { - 100: "Continue", - 101: "Switching Protocols", - 200: "OK", - 201: "Created", - 202: "Accepted", - 203: "Non-Authoritative Information", - 204: "No Content", - 205: "Reset Content", - 206: "Partial Content", - 300: "Multiple Choice", - 301: "Moved Permanently", - 302: "Found", - 303: "See Other", - 304: "Not Modified", - 305: "Use Proxy", - 307: "Temporary Redirect", - 400: "Bad Request", - 401: "Unauthorized", - 402: "Payment Required", - 403: "Forbidden", - 404: "Not Found", - 405: "Method Not Allowed", - 406: "Not Acceptable", - 407: "Proxy Authentication Required", - 408: "Request Timeout", - 409: "Conflict", - 410: "Gone", - 411: "Length Required", - 412: "Precondition Failed", - 413: "Request Entity Too Large", - 414: "Request-URI Too Long", - 415: "Unsupported Media Type", - 416: "Requested Range Not Satisfiable", - 417: "Expectation Failed", - 422: "Unprocessable Entity", - 500: "Internal Server Error", - 501: "Not Implemented", - 502: "Bad Gateway", - 503: "Service Unavailable", - 504: "Gateway Timeout", - 505: "HTTP Version Not Supported" -}; - - -/* - Cross-browser XML parsing. Used to turn - XML responses into Document objects - Borrowed from JSpec -*/ -function parseXML(text) { - var xmlDoc; - - if (typeof DOMParser != "undefined") { - var parser = new DOMParser(); - xmlDoc = parser.parseFromString(text, "text/xml"); - } else { - xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); - xmlDoc.async = "false"; - xmlDoc.loadXML(text); - } - - return xmlDoc; -} - -/* - Without mocking, the native XMLHttpRequest object will throw - an error when attempting to set these headers. We match this behavior. -*/ -var unsafeHeaders = { - "Accept-Charset": true, - "Accept-Encoding": true, - "Connection": true, - "Content-Length": true, - "Cookie": true, - "Cookie2": true, - "Content-Transfer-Encoding": true, - "Date": true, - "Expect": true, - "Host": true, - "Keep-Alive": true, - "Referer": true, - "TE": true, - "Trailer": true, - "Transfer-Encoding": true, - "Upgrade": true, - "User-Agent": true, - "Via": true -}; - -/* - Adds an "event" onto the fake xhr object - that just calls the same-named method. This is - in case a library adds callbacks for these events. -*/ -function _addEventListener(eventName, xhr){ - xhr.addEventListener(eventName, function (event) { - var listener = xhr["on" + eventName]; - - if (listener && typeof listener == "function") { - listener(event); + _Event.prototype = { + stopPropagation: function () {}, + preventDefault: function () { + this.defaultPrevented = true; } - }); -} - -/* - Constructor for a fake window.XMLHttpRequest -*/ -function FakeXMLHttpRequest() { - this.readyState = FakeXMLHttpRequest.UNSENT; - this.requestHeaders = {}; - this.requestBody = null; - this.status = 0; - this.statusText = ""; - - this._eventListeners = {}; - var events = ["loadstart", "load", "abort", "loadend"]; - for (var i = events.length - 1; i >= 0; i--) { - _addEventListener(events[i], this); - } -} - - -// These status codes are available on the native XMLHttpRequest -// object, so we match that here in case a library is relying on them. -FakeXMLHttpRequest.UNSENT = 0; -FakeXMLHttpRequest.OPENED = 1; -FakeXMLHttpRequest.HEADERS_RECEIVED = 2; -FakeXMLHttpRequest.LOADING = 3; -FakeXMLHttpRequest.DONE = 4; - -FakeXMLHttpRequest.prototype = { - UNSENT: 0, - OPENED: 1, - HEADERS_RECEIVED: 2, - LOADING: 3, - DONE: 4, - async: true, + }; /* - Duplicates the behavior of native XMLHttpRequest's open function + Used to set the statusText property of an xhr object */ - open: function open(method, url, async, username, password) { - this.method = method; - this.url = url; - this.async = typeof async == "boolean" ? async : true; - this.username = username; - this.password = password; - this.responseText = null; - this.responseXML = null; - this.requestHeaders = {}; - this.sendFlag = false; - this._readyStateChange(FakeXMLHttpRequest.OPENED); - }, + var httpStatusCodes = { + 100: "Continue", + 101: "Switching Protocols", + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 300: "Multiple Choice", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 307: "Temporary Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Request Entity Too Large", + 414: "Request-URI Too Long", + 415: "Unsupported Media Type", + 416: "Requested Range Not Satisfiable", + 417: "Expectation Failed", + 422: "Unprocessable Entity", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported" + }; + /* - Duplicates the behavior of native XMLHttpRequest's addEventListener function + Cross-browser XML parsing. Used to turn + XML responses into Document objects + Borrowed from JSpec */ - addEventListener: function addEventListener(event, listener) { - this._eventListeners[event] = this._eventListeners[event] || []; - this._eventListeners[event].push(listener); - }, + function parseXML(text) { + var xmlDoc; - /* - Duplicates the behavior of native XMLHttpRequest's removeEventListener function - */ - removeEventListener: function removeEventListener(event, listener) { - var listeners = this._eventListeners[event] || []; - - for (var i = 0, l = listeners.length; i < l; ++i) { - if (listeners[i] == listener) { - return listeners.splice(i, 1); - } - } - }, - - /* - Duplicates the behavior of native XMLHttpRequest's dispatchEvent function - */ - dispatchEvent: function dispatchEvent(event) { - var type = event.type; - var listeners = this._eventListeners[type] || []; - - for (var i = 0; i < listeners.length; i++) { - if (typeof listeners[i] == "function") { - listeners[i].call(this, event); - } else { - listeners[i].handleEvent(event); - } - } - - return !!event.defaultPrevented; - }, - - /* - Duplicates the behavior of native XMLHttpRequest's setRequestHeader function - */ - setRequestHeader: function setRequestHeader(header, value) { - verifyState(this); - - if (unsafeHeaders[header] || /^(Sec-|Proxy-)/.test(header)) { - throw new Error("Refused to set unsafe header \"" + header + "\""); - } - - if (this.requestHeaders[header]) { - this.requestHeaders[header] += "," + value; + if (typeof DOMParser != "undefined") { + var parser = new DOMParser(); + xmlDoc = parser.parseFromString(text, "text/xml"); } else { - this.requestHeaders[header] = value; + xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); + xmlDoc.async = "false"; + xmlDoc.loadXML(text); } - }, + + return xmlDoc; + } /* - Duplicates the behavior of native XMLHttpRequest's send function + Without mocking, the native XMLHttpRequest object will throw + an error when attempting to set these headers. We match this behavior. */ - send: function send(data) { - verifyState(this); + var unsafeHeaders = { + "Accept-Charset": true, + "Accept-Encoding": true, + "Connection": true, + "Content-Length": true, + "Cookie": true, + "Cookie2": true, + "Content-Transfer-Encoding": true, + "Date": true, + "Expect": true, + "Host": true, + "Keep-Alive": true, + "Referer": true, + "TE": true, + "Trailer": true, + "Transfer-Encoding": true, + "Upgrade": true, + "User-Agent": true, + "Via": true + }; - if (!/^(get|head)$/i.test(this.method)) { - if (this.requestHeaders["Content-Type"]) { - var value = this.requestHeaders["Content-Type"].split(";"); - this.requestHeaders["Content-Type"] = value[0] + ";charset=utf-8"; - } else { - this.requestHeaders["Content-Type"] = "text/plain;charset=utf-8"; + /* + Adds an "event" onto the fake xhr object + that just calls the same-named method. This is + in case a library adds callbacks for these events. + */ + function _addEventListener(eventName, xhr){ + xhr.addEventListener(eventName, function (event) { + var listener = xhr["on" + eventName]; + + if (listener && typeof listener == "function") { + listener.call(event.target, event); + } + }); + } + + function EventedObject() { + this._eventListeners = {}; + var events = ["loadstart", "progress", "load", "abort", "loadend"]; + for (var i = events.length - 1; i >= 0; i--) { + _addEventListener(events[i], this); + } + } + EventedObject.prototype = { + /* + Duplicates the behavior of native XMLHttpRequest's addEventListener function + */ + addEventListener: function addEventListener(event, listener) { + this._eventListeners[event] = this._eventListeners[event] || []; + this._eventListeners[event].push(listener); + }, + + /* + Duplicates the behavior of native XMLHttpRequest's removeEventListener function + */ + removeEventListener: function removeEventListener(event, listener) { + var listeners = this._eventListeners[event] || []; + + for (var i = 0, l = listeners.length; i < l; ++i) { + if (listeners[i] == listener) { + return listeners.splice(i, 1); + } + } + }, + + /* + Duplicates the behavior of native XMLHttpRequest's dispatchEvent function + */ + dispatchEvent: function dispatchEvent(event) { + var type = event.type; + var listeners = this._eventListeners[type] || []; + + for (var i = 0; i < listeners.length; i++) { + if (typeof listeners[i] == "function") { + listeners[i].call(this, event); + } else { + listeners[i].handleEvent(event); + } } - this.requestBody = data; + return !!event.defaultPrevented; + }, + + /* + Triggers an `onprogress` event with the given parameters. + */ + _progress: function _progress(lengthComputable, loaded, total) { + var event = new _Event('progress'); + event.target = this; + event.lengthComputable = lengthComputable; + event.loaded = loaded; + event.total = total; + this.dispatchEvent(event); } - - this.errorFlag = false; - this.sendFlag = this.async; - this._readyStateChange(FakeXMLHttpRequest.OPENED); - - if (typeof this.onSend == "function") { - this.onSend(this); - } - - this.dispatchEvent(new _Event("loadstart", false, false, this)); - }, + }; /* - Duplicates the behavior of native XMLHttpRequest's abort function + Constructor for a fake window.XMLHttpRequest */ - abort: function abort() { - this.aborted = true; - this.responseText = null; - this.errorFlag = true; - this.requestHeaders = {}; - - if (this.readyState > FakeXMLHttpRequest.UNSENT && this.sendFlag) { - this._readyStateChange(FakeXMLHttpRequest.DONE); - this.sendFlag = false; - } - + function FakeXMLHttpRequest() { + EventedObject.call(this); this.readyState = FakeXMLHttpRequest.UNSENT; + this.requestHeaders = {}; + this.requestBody = null; + this.status = 0; + this.statusText = ""; + this.upload = new EventedObject(); + this.onabort= null; + this.onerror= null; + this.onload= null; + this.onloadend= null; + this.onloadstart= null; + this.onprogress= null; + this.onreadystatechange= null; + this.ontimeout= null; + } - this.dispatchEvent(new _Event("abort", false, false, this)); - if (typeof this.onerror === "function") { + FakeXMLHttpRequest.prototype = new EventedObject(); + + // These status codes are available on the native XMLHttpRequest + // object, so we match that here in case a library is relying on them. + FakeXMLHttpRequest.UNSENT = 0; + FakeXMLHttpRequest.OPENED = 1; + FakeXMLHttpRequest.HEADERS_RECEIVED = 2; + FakeXMLHttpRequest.LOADING = 3; + FakeXMLHttpRequest.DONE = 4; + + var FakeXMLHttpRequestProto = { + UNSENT: 0, + OPENED: 1, + HEADERS_RECEIVED: 2, + LOADING: 3, + DONE: 4, + async: true, + withCredentials: false, + + /* + Duplicates the behavior of native XMLHttpRequest's open function + */ + open: function open(method, url, async, username, password) { + this.method = method; + this.url = url; + this.async = typeof async == "boolean" ? async : true; + this.username = username; + this.password = password; + this.responseText = null; + this.response = this.responseText; + this.responseXML = null; + this.responseURL = url; + this.requestHeaders = {}; + this.sendFlag = false; + this._readyStateChange(FakeXMLHttpRequest.OPENED); + }, + + /* + Duplicates the behavior of native XMLHttpRequest's setRequestHeader function + */ + setRequestHeader: function setRequestHeader(header, value) { + verifyState(this); + + if (unsafeHeaders[header] || /^(Sec-|Proxy-)/.test(header)) { + throw new Error("Refused to set unsafe header \"" + header + "\""); + } + + if (this.requestHeaders[header]) { + this.requestHeaders[header] += "," + value; + } else { + this.requestHeaders[header] = value; + } + }, + + /* + Duplicates the behavior of native XMLHttpRequest's send function + */ + send: function send(data) { + verifyState(this); + + if (!/^(get|head)$/i.test(this.method)) { + var hasContentTypeHeader = false; + + Object.keys(this.requestHeaders).forEach(function (key) { + if (key.toLowerCase() === 'content-type') { + hasContentTypeHeader = true; + } + }); + + if (!hasContentTypeHeader && !(data || '').toString().match('FormData')) { + this.requestHeaders["Content-Type"] = "text/plain;charset=UTF-8"; + } + + this.requestBody = data; + } + + this.errorFlag = false; + this.sendFlag = this.async; + this._readyStateChange(FakeXMLHttpRequest.OPENED); + + if (typeof this.onSend == "function") { + this.onSend(this); + } + + this.dispatchEvent(new _Event("loadstart", false, false, this)); + }, + + /* + Duplicates the behavior of native XMLHttpRequest's abort function + */ + abort: function abort() { + this.aborted = true; + this.responseText = null; + this.response = this.responseText; + this.errorFlag = true; + this.requestHeaders = {}; + + this.dispatchEvent(new _Event("abort", false, false, this)); + + if (this.readyState > FakeXMLHttpRequest.UNSENT && this.sendFlag) { + this._readyStateChange(FakeXMLHttpRequest.UNSENT); + this.sendFlag = false; + } + + if (typeof this.onerror === "function") { this.onerror(); - } - }, + } + }, + + /* + Duplicates the behavior of native XMLHttpRequest's getResponseHeader function + */ + getResponseHeader: function getResponseHeader(header) { + if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { + return null; + } + + if (/^Set-Cookie2?$/i.test(header)) { + return null; + } + + header = header.toLowerCase(); + + for (var h in this.responseHeaders) { + if (h.toLowerCase() == header) { + return this.responseHeaders[h]; + } + } - /* - Duplicates the behavior of native XMLHttpRequest's getResponseHeader function - */ - getResponseHeader: function getResponseHeader(header) { - if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { return null; - } + }, - if (/^Set-Cookie2?$/i.test(header)) { - return null; - } - - header = header.toLowerCase(); - - for (var h in this.responseHeaders) { - if (h.toLowerCase() == header) { - return this.responseHeaders[h]; + /* + Duplicates the behavior of native XMLHttpRequest's getAllResponseHeaders function + */ + getAllResponseHeaders: function getAllResponseHeaders() { + if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { + return ""; } - } - return null; - }, + var headers = ""; - /* - Duplicates the behavior of native XMLHttpRequest's getAllResponseHeaders function - */ - getAllResponseHeaders: function getAllResponseHeaders() { - if (this.readyState < FakeXMLHttpRequest.HEADERS_RECEIVED) { - return ""; - } - - var headers = ""; - - for (var header in this.responseHeaders) { - if (this.responseHeaders.hasOwnProperty(header) && !/^Set-Cookie2?$/i.test(header)) { - headers += header + ": " + this.responseHeaders[header] + "\r\n"; + for (var header in this.responseHeaders) { + if (this.responseHeaders.hasOwnProperty(header) && !/^Set-Cookie2?$/i.test(header)) { + headers += header + ": " + this.responseHeaders[header] + "\r\n"; + } } - } - return headers; - }, + return headers; + }, - /* - Places a FakeXMLHttpRequest object into the passed - state. - */ - _readyStateChange: function _readyStateChange(state) { - this.readyState = state; - - if (typeof this.onreadystatechange == "function") { - this.onreadystatechange(); - } - - this.dispatchEvent(new _Event("readystatechange")); - - if (this.readyState == FakeXMLHttpRequest.DONE) { - this.dispatchEvent(new _Event("load", false, false, this)); - this.dispatchEvent(new _Event("loadend", false, false, this)); - } - }, - - - /* - Sets the FakeXMLHttpRequest object's response headers and - places the object into readyState 2 - */ - _setResponseHeaders: function _setResponseHeaders(headers) { - this.responseHeaders = {}; - - for (var header in headers) { - if (headers.hasOwnProperty(header)) { - this.responseHeaders[header] = headers[header]; + /* + Duplicates the behavior of native XMLHttpRequest's overrideMimeType function + */ + overrideMimeType: function overrideMimeType(mimeType) { + if (typeof mimeType === "string") { + this.forceMimeType = mimeType.toLowerCase(); } - } - - if (this.async) { - this._readyStateChange(FakeXMLHttpRequest.HEADERS_RECEIVED); - } else { - this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED; - } - }, + }, + /* + Places a FakeXMLHttpRequest object into the passed + state. + */ + _readyStateChange: function _readyStateChange(state) { + this.readyState = state; - /* - Sets the FakeXMLHttpRequest object's response body and - if body text is XML, sets responseXML to parsed document - object - */ - _setResponseBody: function _setResponseBody(body) { - verifyRequestSent(this); - verifyHeadersReceived(this); - verifyResponseBodyType(body); + if (typeof this.onreadystatechange == "function") { + this.onreadystatechange(new _Event("readystatechange")); + } - var chunkSize = this.chunkSize || 10; - var index = 0; - this.responseText = ""; + this.dispatchEvent(new _Event("readystatechange")); + + if (this.readyState == FakeXMLHttpRequest.DONE) { + this.dispatchEvent(new _Event("load", false, false, this)); + } + if (this.readyState == FakeXMLHttpRequest.UNSENT || this.readyState == FakeXMLHttpRequest.DONE) { + this.dispatchEvent(new _Event("loadend", false, false, this)); + } + }, + + + /* + Sets the FakeXMLHttpRequest object's response headers and + places the object into readyState 2 + */ + _setResponseHeaders: function _setResponseHeaders(headers) { + this.responseHeaders = {}; + + for (var header in headers) { + if (headers.hasOwnProperty(header)) { + this.responseHeaders[header] = headers[header]; + } + } + + if (this.forceMimeType) { + this.responseHeaders['Content-Type'] = this.forceMimeType; + } - do { if (this.async) { - this._readyStateChange(FakeXMLHttpRequest.LOADING); + this._readyStateChange(FakeXMLHttpRequest.HEADERS_RECEIVED); + } else { + this.readyState = FakeXMLHttpRequest.HEADERS_RECEIVED; + } + }, + + /* + Sets the FakeXMLHttpRequest object's response body and + if body text is XML, sets responseXML to parsed document + object + */ + _setResponseBody: function _setResponseBody(body) { + verifyRequestSent(this); + verifyHeadersReceived(this); + verifyResponseBodyType(body); + + var chunkSize = this.chunkSize || 10; + var index = 0; + this.responseText = ""; + this.response = this.responseText; + + do { + if (this.async) { + this._readyStateChange(FakeXMLHttpRequest.LOADING); + } + + this.responseText += body.substring(index, index + chunkSize); + this.response = this.responseText; + index += chunkSize; + } while (index < body.length); + + var type = this.getResponseHeader("Content-Type"); + + if (this.responseText && (!type || /(text\/xml)|(application\/xml)|(\+xml)/.test(type))) { + try { + this.responseXML = parseXML(this.responseText); + } catch (e) { + // Unable to parse XML - no biggie + } } - this.responseText += body.substring(index, index + chunkSize); - index += chunkSize; - } while (index < body.length); - - var type = this.getResponseHeader("Content-Type"); - - if (this.responseText && (!type || /(text\/xml)|(application\/xml)|(\+xml)/.test(type))) { - try { - this.responseXML = parseXML(this.responseText); - } catch (e) { - // Unable to parse XML - no biggie + if (this.async) { + this._readyStateChange(FakeXMLHttpRequest.DONE); + } else { + this.readyState = FakeXMLHttpRequest.DONE; } + }, + + /* + Forces a response on to the FakeXMLHttpRequest object. + + This is the public API for faking responses. This function + takes a number status, headers object, and string body: + + ``` + xhr.respond(404, {Content-Type: 'text/plain'}, "Sorry. This object was not found.") + + ``` + */ + respond: function respond(status, headers, body) { + this._setResponseHeaders(headers || {}); + this.status = typeof status == "number" ? status : 200; + this.statusText = httpStatusCodes[this.status]; + this._setResponseBody(body || ""); } + }; - if (this.async) { - this._readyStateChange(FakeXMLHttpRequest.DONE); - } else { - this.readyState = FakeXMLHttpRequest.DONE; - } - }, - - /* - Forces a response on to the FakeXMLHttpRequest object. - - This is the public API for faking responses. This function - takes a number status, headers object, and string body: - - ``` - xhr.respond(404, {Content-Type: 'text/plain'}, "Sorry. This object was not found.") - - ``` - */ - respond: function respond(status, headers, body) { - this._setResponseHeaders(headers || {}); - this.status = typeof status == "number" ? status : 200; - this.statusText = httpStatusCodes[this.status]; - this._setResponseBody(body || ""); - if (typeof this.onload === "function"){ - this.onload(); - } - } -}; - -function verifyState(xhr) { - if (xhr.readyState !== FakeXMLHttpRequest.OPENED) { - throw new Error("INVALID_STATE_ERR"); + for (var property in FakeXMLHttpRequestProto) { + FakeXMLHttpRequest.prototype[property] = FakeXMLHttpRequestProto[property]; } - if (xhr.sendFlag) { - throw new Error("INVALID_STATE_ERR"); + function verifyState(xhr) { + if (xhr.readyState !== FakeXMLHttpRequest.OPENED) { + throw new Error("INVALID_STATE_ERR"); + } + + if (xhr.sendFlag) { + throw new Error("INVALID_STATE_ERR"); + } } -} -function verifyRequestSent(xhr) { - if (xhr.readyState == FakeXMLHttpRequest.DONE) { - throw new Error("Request done"); - } -} + function verifyRequestSent(xhr) { + if (xhr.readyState == FakeXMLHttpRequest.DONE) { + throw new Error("Request done"); + } + } -function verifyHeadersReceived(xhr) { - if (xhr.async && xhr.readyState != FakeXMLHttpRequest.HEADERS_RECEIVED) { - throw new Error("No headers received"); - } -} + function verifyHeadersReceived(xhr) { + if (xhr.async && xhr.readyState != FakeXMLHttpRequest.HEADERS_RECEIVED) { + throw new Error("No headers received"); + } + } -function verifyResponseBodyType(body) { - if (typeof body != "string") { - var error = new Error("Attempted to respond to fake XMLHttpRequest with " + - body + ", which is not a string."); - error.name = "InvalidBodyException"; - throw error; - } -} + function verifyResponseBodyType(body) { + if (typeof body != "string") { + var error = new Error("Attempted to respond to fake XMLHttpRequest with " + + body + ", which is not a string."); + error.name = "InvalidBodyException"; + throw error; + } + } -if (typeof module !== 'undefined' && module.exports) { - module.exports = FakeXMLHttpRequest; -} else if (typeof define === 'function' && define.amd) { - define(function() { return FakeXMLHttpRequest; }); -} else if (typeof window !== 'undefined') { - window.FakeXMLHttpRequest = FakeXMLHttpRequest; -} else if (this) { - this.FakeXMLHttpRequest = FakeXMLHttpRequest; -} -})(); + return FakeXMLHttpRequest; + +}))); +//# sourceMappingURL=fake_xml_http_request.js.map diff --git a/vendor/assets/javascripts/pretender.js b/vendor/assets/javascripts/pretender.js index f489fc7ca3..a715b72fd5 100644 --- a/vendor/assets/javascripts/pretender.js +++ b/vendor/assets/javascripts/pretender.js @@ -1,487 +1,1850 @@ -(function(self) { -'use strict'; - -var appearsBrowserified = typeof self !== 'undefined' && - typeof process !== 'undefined' && - Object.prototype.toString.call(process) === '[object Object]'; - -var RouteRecognizer = appearsBrowserified ? require('route-recognizer') : self.RouteRecognizer; -var FakeXMLHttpRequest = appearsBrowserified ? require('fake-xml-http-request') : self.FakeXMLHttpRequest; - -/** - * parseURL - decompose a URL into its parts - * @param {String} url a URL - * @return {Object} parts of the URL, including the following - * - * 'https://www.yahoo.com:1234/mypage?test=yes#abc' - * - * { - * host: 'www.yahoo.com:1234', - * protocol: 'https:', - * search: '?test=yes', - * hash: '#abc', - * href: 'https://www.yahoo.com:1234/mypage?test=yes#abc', - * pathname: '/mypage', - * fullpath: '/mypage?test=yes' - * } - */ -function parseURL(url) { - // TODO: something for when document isn't present... #yolo - var anchor = document.createElement('a'); - anchor.href = url; - - if (!anchor.host) { - anchor.href = anchor.href; // IE: load the host and protocol +var Pretender = (function(self) { + function getModuleDefault(module) { + return module.default || module; } - var pathname = anchor.pathname; - if (pathname.charAt(0) !== '/') { - pathname = '/' + pathname; // IE: prepend leading slash + var appearsBrowserified = + typeof self !== 'undefined' && + typeof process !== 'undefined' && + (Object.prototype.toString.call(process) === '[object Object]' || + Object.prototype.toString.call(process) === '[object process]'); + + var RouteRecognizer = appearsBrowserified + ? getModuleDefault(require('route-recognizer')) + : self.RouteRecognizer; + var FakeXMLHttpRequest = appearsBrowserified + ? getModuleDefault(require('fake-xml-http-request')) + : self.FakeXMLHttpRequest; + + +var Pretender = (function (RouteRecognizer, FakeXMLHttpRequest) { + 'use strict'; + + RouteRecognizer = RouteRecognizer && Object.prototype.hasOwnProperty.call(RouteRecognizer, 'default') ? RouteRecognizer['default'] : RouteRecognizer; + FakeXMLHttpRequest = FakeXMLHttpRequest && Object.prototype.hasOwnProperty.call(FakeXMLHttpRequest, 'default') ? FakeXMLHttpRequest['default'] : FakeXMLHttpRequest; + + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + /** + * Check if we're required to add a port number. + * + * @see https://url.spec.whatwg.org/#default-port + * @param {Number|String} port Port number we need to check + * @param {String} protocol Protocol we need to check against. + * @returns {Boolean} Is it a default port for the given protocol + * @api private + */ + var requiresPort = function required(port, protocol) { + protocol = protocol.split(':')[0]; + port = +port; + + if (!port) return false; + + switch (protocol) { + case 'http': + case 'ws': + return port !== 80; + + case 'https': + case 'wss': + return port !== 443; + + case 'ftp': + return port !== 21; + + case 'gopher': + return port !== 70; + + case 'file': + return false; + } + + return port !== 0; + }; + + var has = Object.prototype.hasOwnProperty + , undef; + + /** + * Decode a URI encoded string. + * + * @param {String} input The URI encoded string. + * @returns {String|Null} The decoded string. + * @api private + */ + function decode(input) { + try { + return decodeURIComponent(input.replace(/\+/g, ' ')); + } catch (e) { + return null; + } + } + + /** + * Simple query string parser. + * + * @param {String} query The query string that needs to be parsed. + * @returns {Object} + * @api public + */ + function querystring(query) { + var parser = /([^=?&]+)=?([^&]*)/g + , result = {} + , part; + + while (part = parser.exec(query)) { + var key = decode(part[1]) + , value = decode(part[2]); + + // + // Prevent overriding of existing properties. This ensures that build-in + // methods like `toString` or __proto__ are not overriden by malicious + // querystrings. + // + // In the case if failed decoding, we want to omit the key/value pairs + // from the result. + // + if (key === null || value === null || key in result) continue; + result[key] = value; + } + + return result; + } + + /** + * Transform a query string to an object. + * + * @param {Object} obj Object that should be transformed. + * @param {String} prefix Optional prefix. + * @returns {String} + * @api public + */ + function querystringify(obj, prefix) { + prefix = prefix || ''; + + var pairs = [] + , value + , key; + + // + // Optionally prefix with a '?' if needed + // + if ('string' !== typeof prefix) prefix = '?'; + + for (key in obj) { + if (has.call(obj, key)) { + value = obj[key]; + + // + // Edge cases where we actually want to encode the value to an empty + // string instead of the stringified value. + // + if (!value && (value === null || value === undef || isNaN(value))) { + value = ''; + } + + key = encodeURIComponent(key); + value = encodeURIComponent(value); + + // + // If we failed to encode the strings, we should bail out as we don't + // want to add invalid strings to the query. + // + if (key === null || value === null) continue; + pairs.push(key +'='+ value); + } + } + + return pairs.length ? prefix + pairs.join('&') : ''; + } + + // + // Expose the module. + // + var stringify = querystringify; + var parse = querystring; + + var querystringify_1 = { + stringify: stringify, + parse: parse + }; + + var slashes = /^[A-Za-z][A-Za-z0-9+-.]*:\/\// + , protocolre = /^([a-z][a-z0-9.+-]*:)?(\/\/)?([\\/]+)?([\S\s]*)/i + , windowsDriveLetter = /^[a-zA-Z]:/ + , whitespace = '[\\x09\\x0A\\x0B\\x0C\\x0D\\x20\\xA0\\u1680\\u180E\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200A\\u202F\\u205F\\u3000\\u2028\\u2029\\uFEFF]' + , left = new RegExp('^'+ whitespace +'+'); + + /** + * Trim a given string. + * + * @param {String} str String to trim. + * @public + */ + function trimLeft(str) { + return (str ? str : '').toString().replace(left, ''); + } + + /** + * These are the parse rules for the URL parser, it informs the parser + * about: + * + * 0. The char it Needs to parse, if it's a string it should be done using + * indexOf, RegExp using exec and NaN means set as current value. + * 1. The property we should set when parsing this value. + * 2. Indication if it's backwards or forward parsing, when set as number it's + * the value of extra chars that should be split off. + * 3. Inherit from location if non existing in the parser. + * 4. `toLowerCase` the resulting value. + */ + var rules = [ + ['#', 'hash'], // Extract from the back. + ['?', 'query'], // Extract from the back. + function sanitize(address, url) { // Sanitize what is left of the address + return isSpecial(url.protocol) ? address.replace(/\\/g, '/') : address; + }, + ['/', 'pathname'], // Extract from the back. + ['@', 'auth', 1], // Extract from the front. + [NaN, 'host', undefined, 1, 1], // Set left over value. + [/:(\d+)$/, 'port', undefined, 1], // RegExp the back. + [NaN, 'hostname', undefined, 1, 1] // Set left over. + ]; + + /** + * These properties should not be copied or inherited from. This is only needed + * for all non blob URL's as a blob URL does not include a hash, only the + * origin. + * + * @type {Object} + * @private + */ + var ignore = { hash: 1, query: 1 }; + + /** + * The location object differs when your code is loaded through a normal page, + * Worker or through a worker using a blob. And with the blobble begins the + * trouble as the location object will contain the URL of the blob, not the + * location of the page where our code is loaded in. The actual origin is + * encoded in the `pathname` so we can thankfully generate a good "default" + * location from it so we can generate proper relative URL's again. + * + * @param {Object|String} loc Optional default location object. + * @returns {Object} lolcation object. + * @public + */ + function lolcation(loc) { + var globalVar; + + if (typeof window !== 'undefined') globalVar = window; + else if (typeof commonjsGlobal !== 'undefined') globalVar = commonjsGlobal; + else if (typeof self !== 'undefined') globalVar = self; + else globalVar = {}; + + var location = globalVar.location || {}; + loc = loc || location; + + var finaldestination = {} + , type = typeof loc + , key; + + if ('blob:' === loc.protocol) { + finaldestination = new Url(unescape(loc.pathname), {}); + } else if ('string' === type) { + finaldestination = new Url(loc, {}); + for (key in ignore) delete finaldestination[key]; + } else if ('object' === type) { + for (key in loc) { + if (key in ignore) continue; + finaldestination[key] = loc[key]; + } + + if (finaldestination.slashes === undefined) { + finaldestination.slashes = slashes.test(loc.href); + } + } + + return finaldestination; + } + + /** + * Check whether a protocol scheme is special. + * + * @param {String} The protocol scheme of the URL + * @return {Boolean} `true` if the protocol scheme is special, else `false` + * @private + */ + function isSpecial(scheme) { + return ( + scheme === 'file:' || + scheme === 'ftp:' || + scheme === 'http:' || + scheme === 'https:' || + scheme === 'ws:' || + scheme === 'wss:' + ); + } + + /** + * @typedef ProtocolExtract + * @type Object + * @property {String} protocol Protocol matched in the URL, in lowercase. + * @property {Boolean} slashes `true` if protocol is followed by "//", else `false`. + * @property {String} rest Rest of the URL that is not part of the protocol. + */ + + /** + * Extract protocol information from a URL with/without double slash ("//"). + * + * @param {String} address URL we want to extract from. + * @param {Object} location + * @return {ProtocolExtract} Extracted information. + * @private + */ + function extractProtocol(address, location) { + address = trimLeft(address); + location = location || {}; + + var match = protocolre.exec(address); + var protocol = match[1] ? match[1].toLowerCase() : ''; + var forwardSlashes = !!match[2]; + var otherSlashes = !!match[3]; + var slashesCount = 0; + var rest; + + if (forwardSlashes) { + if (otherSlashes) { + rest = match[2] + match[3] + match[4]; + slashesCount = match[2].length + match[3].length; + } else { + rest = match[2] + match[4]; + slashesCount = match[2].length; + } + } else { + if (otherSlashes) { + rest = match[3] + match[4]; + slashesCount = match[3].length; + } else { + rest = match[4]; + } + } + + if (protocol === 'file:') { + if (slashesCount >= 2) { + rest = rest.slice(2); + } + } else if (isSpecial(protocol)) { + rest = match[4]; + } else if (protocol) { + if (forwardSlashes) { + rest = rest.slice(2); + } + } else if (slashesCount >= 2 && isSpecial(location.protocol)) { + rest = match[4]; + } + + return { + protocol: protocol, + slashes: forwardSlashes || isSpecial(protocol), + slashesCount: slashesCount, + rest: rest + }; + } + + /** + * Resolve a relative URL pathname against a base URL pathname. + * + * @param {String} relative Pathname of the relative URL. + * @param {String} base Pathname of the base URL. + * @return {String} Resolved pathname. + * @private + */ + function resolve(relative, base) { + if (relative === '') return base; + + var path = (base || '/').split('/').slice(0, -1).concat(relative.split('/')) + , i = path.length + , last = path[i - 1] + , unshift = false + , up = 0; + + while (i--) { + if (path[i] === '.') { + path.splice(i, 1); + } else if (path[i] === '..') { + path.splice(i, 1); + up++; + } else if (up) { + if (i === 0) unshift = true; + path.splice(i, 1); + up--; + } + } + + if (unshift) path.unshift(''); + if (last === '.' || last === '..') path.push(''); + + return path.join('/'); + } + + /** + * The actual URL instance. Instead of returning an object we've opted-in to + * create an actual constructor as it's much more memory efficient and + * faster and it pleases my OCD. + * + * It is worth noting that we should not use `URL` as class name to prevent + * clashes with the global URL instance that got introduced in browsers. + * + * @constructor + * @param {String} address URL we want to parse. + * @param {Object|String} [location] Location defaults for relative paths. + * @param {Boolean|Function} [parser] Parser for the query string. + * @private + */ + function Url(address, location, parser) { + address = trimLeft(address); + + if (!(this instanceof Url)) { + return new Url(address, location, parser); + } + + var relative, extracted, parse, instruction, index, key + , instructions = rules.slice() + , type = typeof location + , url = this + , i = 0; + + // + // The following if statements allows this module two have compatibility with + // 2 different API: + // + // 1. Node.js's `url.parse` api which accepts a URL, boolean as arguments + // where the boolean indicates that the query string should also be parsed. + // + // 2. The `URL` interface of the browser which accepts a URL, object as + // arguments. The supplied object will be used as default values / fall-back + // for relative paths. + // + if ('object' !== type && 'string' !== type) { + parser = location; + location = null; + } + + if (parser && 'function' !== typeof parser) parser = querystringify_1.parse; + + location = lolcation(location); + + // + // Extract protocol information before running the instructions. + // + extracted = extractProtocol(address || '', location); + relative = !extracted.protocol && !extracted.slashes; + url.slashes = extracted.slashes || relative && location.slashes; + url.protocol = extracted.protocol || location.protocol || ''; + address = extracted.rest; + + // + // When the authority component is absent the URL starts with a path + // component. + // + if ( + extracted.protocol === 'file:' && ( + extracted.slashesCount !== 2 || windowsDriveLetter.test(address)) || + (!extracted.slashes && + (extracted.protocol || + extracted.slashesCount < 2 || + !isSpecial(url.protocol))) + ) { + instructions[3] = [/(.*)/, 'pathname']; + } + + for (; i < instructions.length; i++) { + instruction = instructions[i]; + + if (typeof instruction === 'function') { + address = instruction(address, url); + continue; + } + + parse = instruction[0]; + key = instruction[1]; + + if (parse !== parse) { + url[key] = address; + } else if ('string' === typeof parse) { + if (~(index = address.indexOf(parse))) { + if ('number' === typeof instruction[2]) { + url[key] = address.slice(0, index); + address = address.slice(index + instruction[2]); + } else { + url[key] = address.slice(index); + address = address.slice(0, index); + } + } + } else if ((index = parse.exec(address))) { + url[key] = index[1]; + address = address.slice(0, index.index); + } + + url[key] = url[key] || ( + relative && instruction[3] ? location[key] || '' : '' + ); + + // + // Hostname, host and protocol should be lowercased so they can be used to + // create a proper `origin`. + // + if (instruction[4]) url[key] = url[key].toLowerCase(); + } + + // + // Also parse the supplied query string in to an object. If we're supplied + // with a custom parser as function use that instead of the default build-in + // parser. + // + if (parser) url.query = parser(url.query); + + // + // If the URL is relative, resolve the pathname against the base URL. + // + if ( + relative + && location.slashes + && url.pathname.charAt(0) !== '/' + && (url.pathname !== '' || location.pathname !== '') + ) { + url.pathname = resolve(url.pathname, location.pathname); + } + + // + // Default to a / for pathname if none exists. This normalizes the URL + // to always have a / + // + if (url.pathname.charAt(0) !== '/' && isSpecial(url.protocol)) { + url.pathname = '/' + url.pathname; + } + + // + // We should not add port numbers if they are already the default port number + // for a given protocol. As the host also contains the port number we're going + // override it with the hostname which contains no port number. + // + if (!requiresPort(url.port, url.protocol)) { + url.host = url.hostname; + url.port = ''; + } + + // + // Parse down the `auth` for the username and password. + // + url.username = url.password = ''; + if (url.auth) { + instruction = url.auth.split(':'); + url.username = instruction[0] || ''; + url.password = instruction[1] || ''; + } + + url.origin = url.protocol !== 'file:' && isSpecial(url.protocol) && url.host + ? url.protocol +'//'+ url.host + : 'null'; + + // + // The href is just the compiled result. + // + url.href = url.toString(); + } + + /** + * This is convenience method for changing properties in the URL instance to + * insure that they all propagate correctly. + * + * @param {String} part Property we need to adjust. + * @param {Mixed} value The newly assigned value. + * @param {Boolean|Function} fn When setting the query, it will be the function + * used to parse the query. + * When setting the protocol, double slash will be + * removed from the final url if it is true. + * @returns {URL} URL instance for chaining. + * @public + */ + function set(part, value, fn) { + var url = this; + + switch (part) { + case 'query': + if ('string' === typeof value && value.length) { + value = (fn || querystringify_1.parse)(value); + } + + url[part] = value; + break; + + case 'port': + url[part] = value; + + if (!requiresPort(value, url.protocol)) { + url.host = url.hostname; + url[part] = ''; + } else if (value) { + url.host = url.hostname +':'+ value; + } + + break; + + case 'hostname': + url[part] = value; + + if (url.port) value += ':'+ url.port; + url.host = value; + break; + + case 'host': + url[part] = value; + + if (/:\d+$/.test(value)) { + value = value.split(':'); + url.port = value.pop(); + url.hostname = value.join(':'); + } else { + url.hostname = value; + url.port = ''; + } + + break; + + case 'protocol': + url.protocol = value.toLowerCase(); + url.slashes = !fn; + break; + + case 'pathname': + case 'hash': + if (value) { + var char = part === 'pathname' ? '/' : '#'; + url[part] = value.charAt(0) !== char ? char + value : value; + } else { + url[part] = value; + } + break; + + default: + url[part] = value; + } + + for (var i = 0; i < rules.length; i++) { + var ins = rules[i]; + + if (ins[4]) url[ins[1]] = url[ins[1]].toLowerCase(); + } + + url.origin = url.protocol !== 'file:' && isSpecial(url.protocol) && url.host + ? url.protocol +'//'+ url.host + : 'null'; + + url.href = url.toString(); + + return url; + } + + /** + * Transform the properties back in to a valid and full URL string. + * + * @param {Function} stringify Optional query stringify function. + * @returns {String} Compiled version of the URL. + * @public + */ + function toString(stringify) { + if (!stringify || 'function' !== typeof stringify) stringify = querystringify_1.stringify; + + var query + , url = this + , protocol = url.protocol; + + if (protocol && protocol.charAt(protocol.length - 1) !== ':') protocol += ':'; + + var result = protocol + (url.slashes || isSpecial(url.protocol) ? '//' : ''); + + if (url.username) { + result += url.username; + if (url.password) result += ':'+ url.password; + result += '@'; + } + + result += url.host + url.pathname; + + query = 'object' === typeof url.query ? stringify(url.query) : url.query; + if (query) result += '?' !== query.charAt(0) ? '?'+ query : query; + + if (url.hash) result += url.hash; + + return result; + } + + Url.prototype = { set: set, toString: toString }; + + // + // Expose the URL parser and some additional properties that might be useful for + // others or testing. + // + Url.extractProtocol = extractProtocol; + Url.location = lolcation; + Url.trimLeft = trimLeft; + Url.qs = querystringify_1; + + var urlParse = Url; + + /** + * parseURL - decompose a URL into its parts + * @param {String} url a URL + * @return {Object} parts of the URL, including the following + * + * 'https://www.yahoo.com:1234/mypage?test=yes#abc' + * + * { + * host: 'www.yahoo.com:1234', + * protocol: 'https:', + * search: '?test=yes', + * hash: '#abc', + * href: 'https://www.yahoo.com:1234/mypage?test=yes#abc', + * pathname: '/mypage', + * fullpath: '/mypage?test=yes' + * } + */ + function parseURL(url) { + var parsedUrl = new urlParse(url); + if (!parsedUrl.host) { + // eslint-disable-next-line no-self-assign + parsedUrl.href = parsedUrl.href; // IE: load the host and protocol + } + var pathname = parsedUrl.pathname; + if (pathname.charAt(0) !== '/') { + pathname = '/' + pathname; // IE: prepend leading slash + } + var host = parsedUrl.host; + if (parsedUrl.port === '80' || parsedUrl.port === '443') { + host = parsedUrl.hostname; // IE: remove default port + } + return { + host: host, + protocol: parsedUrl.protocol, + search: parsedUrl.query, + hash: parsedUrl.hash, + href: parsedUrl.href, + pathname: pathname, + fullpath: pathname + (parsedUrl.query || '') + (parsedUrl.hash || '') + }; + } + + /** + * Registry + * + * A registry is a map of HTTP verbs to route recognizers. + */ + var Registry = /** @class */ (function () { + function Registry( /* host */) { + // Herein we keep track of RouteRecognizer instances + // keyed by HTTP method. Feel free to add more as needed. + this.verbs = { + GET: new RouteRecognizer(), + PUT: new RouteRecognizer(), + POST: new RouteRecognizer(), + DELETE: new RouteRecognizer(), + PATCH: new RouteRecognizer(), + HEAD: new RouteRecognizer(), + OPTIONS: new RouteRecognizer() + }; + } + return Registry; + }()); + + /** + * Hosts + * + * a map of hosts to Registries, ultimately allowing + * a per-host-and-port, per HTTP verb lookup of RouteRecognizers + */ + var Hosts = /** @class */ (function () { + function Hosts() { + this.registries = {}; + } + /** + * Hosts#forURL - retrieve a map of HTTP verbs to RouteRecognizers + * for a given URL + * + * @param {String} url a URL + * @return {Registry} a map of HTTP verbs to RouteRecognizers + * corresponding to the provided URL's + * hostname and port + */ + Hosts.prototype.forURL = function (url) { + var host = parseURL(url).host; + var registry = this.registries[host]; + if (registry === undefined) { + registry = (this.registries[host] = new Registry( /*host*/)); + } + return registry.verbs; + }; + return Hosts; + }()); + + var global$1 = + (typeof globalThis !== 'undefined' && globalThis) || + (typeof self !== 'undefined' && self) || + (typeof global$1 !== 'undefined' && global$1); + + var support = { + searchParams: 'URLSearchParams' in global$1, + iterable: 'Symbol' in global$1 && 'iterator' in Symbol, + blob: + 'FileReader' in global$1 && + 'Blob' in global$1 && + (function() { + try { + new Blob(); + return true + } catch (e) { + return false + } + })(), + formData: 'FormData' in global$1, + arrayBuffer: 'ArrayBuffer' in global$1 + }; + + function isDataView(obj) { + return obj && DataView.prototype.isPrototypeOf(obj) + } + + if (support.arrayBuffer) { + var viewClasses = [ + '[object Int8Array]', + '[object Uint8Array]', + '[object Uint8ClampedArray]', + '[object Int16Array]', + '[object Uint16Array]', + '[object Int32Array]', + '[object Uint32Array]', + '[object Float32Array]', + '[object Float64Array]' + ]; + + var isArrayBufferView = + ArrayBuffer.isView || + function(obj) { + return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1 + }; + } + + function normalizeName(name) { + if (typeof name !== 'string') { + name = String(name); + } + if (/[^a-z0-9\-#$%&'*+.^_`|~!]/i.test(name) || name === '') { + throw new TypeError('Invalid character in header field name: "' + name + '"') + } + return name.toLowerCase() + } + + function normalizeValue(value) { + if (typeof value !== 'string') { + value = String(value); + } + return value + } + + // Build a destructive iterator for the value list + function iteratorFor(items) { + var iterator = { + next: function() { + var value = items.shift(); + return {done: value === undefined, value: value} + } + }; + + if (support.iterable) { + iterator[Symbol.iterator] = function() { + return iterator + }; + } + + return iterator + } + + function Headers(headers) { + this.map = {}; + + if (headers instanceof Headers) { + headers.forEach(function(value, name) { + this.append(name, value); + }, this); + } else if (Array.isArray(headers)) { + headers.forEach(function(header) { + this.append(header[0], header[1]); + }, this); + } else if (headers) { + Object.getOwnPropertyNames(headers).forEach(function(name) { + this.append(name, headers[name]); + }, this); + } + } + + Headers.prototype.append = function(name, value) { + name = normalizeName(name); + value = normalizeValue(value); + var oldValue = this.map[name]; + this.map[name] = oldValue ? oldValue + ', ' + value : value; + }; + + Headers.prototype['delete'] = function(name) { + delete this.map[normalizeName(name)]; + }; + + Headers.prototype.get = function(name) { + name = normalizeName(name); + return this.has(name) ? this.map[name] : null + }; + + Headers.prototype.has = function(name) { + return this.map.hasOwnProperty(normalizeName(name)) + }; + + Headers.prototype.set = function(name, value) { + this.map[normalizeName(name)] = normalizeValue(value); + }; + + Headers.prototype.forEach = function(callback, thisArg) { + for (var name in this.map) { + if (this.map.hasOwnProperty(name)) { + callback.call(thisArg, this.map[name], name, this); + } + } + }; + + Headers.prototype.keys = function() { + var items = []; + this.forEach(function(value, name) { + items.push(name); + }); + return iteratorFor(items) + }; + + Headers.prototype.values = function() { + var items = []; + this.forEach(function(value) { + items.push(value); + }); + return iteratorFor(items) + }; + + Headers.prototype.entries = function() { + var items = []; + this.forEach(function(value, name) { + items.push([name, value]); + }); + return iteratorFor(items) + }; + + if (support.iterable) { + Headers.prototype[Symbol.iterator] = Headers.prototype.entries; + } + + function consumed(body) { + if (body.bodyUsed) { + return Promise.reject(new TypeError('Already read')) + } + body.bodyUsed = true; + } + + function fileReaderReady(reader) { + return new Promise(function(resolve, reject) { + reader.onload = function() { + resolve(reader.result); + }; + reader.onerror = function() { + reject(reader.error); + }; + }) + } + + function readBlobAsArrayBuffer(blob) { + var reader = new FileReader(); + var promise = fileReaderReady(reader); + reader.readAsArrayBuffer(blob); + return promise + } + + function readBlobAsText(blob) { + var reader = new FileReader(); + var promise = fileReaderReady(reader); + reader.readAsText(blob); + return promise + } + + function readArrayBufferAsText(buf) { + var view = new Uint8Array(buf); + var chars = new Array(view.length); + + for (var i = 0; i < view.length; i++) { + chars[i] = String.fromCharCode(view[i]); + } + return chars.join('') + } + + function bufferClone(buf) { + if (buf.slice) { + return buf.slice(0) + } else { + var view = new Uint8Array(buf.byteLength); + view.set(new Uint8Array(buf)); + return view.buffer + } + } + + function Body() { + this.bodyUsed = false; + + this._initBody = function(body) { + /* + fetch-mock wraps the Response object in an ES6 Proxy to + provide useful test harness features such as flush. However, on + ES5 browsers without fetch or Proxy support pollyfills must be used; + the proxy-pollyfill is unable to proxy an attribute unless it exists + on the object before the Proxy is created. This change ensures + Response.bodyUsed exists on the instance, while maintaining the + semantic of setting Request.bodyUsed in the constructor before + _initBody is called. + */ + this.bodyUsed = this.bodyUsed; + this._bodyInit = body; + if (!body) { + this._bodyText = ''; + } else if (typeof body === 'string') { + this._bodyText = body; + } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { + this._bodyBlob = body; + } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { + this._bodyFormData = body; + } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { + this._bodyText = body.toString(); + } else if (support.arrayBuffer && support.blob && isDataView(body)) { + this._bodyArrayBuffer = bufferClone(body.buffer); + // IE 10-11 can't handle a DataView body. + this._bodyInit = new Blob([this._bodyArrayBuffer]); + } else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) { + this._bodyArrayBuffer = bufferClone(body); + } else { + this._bodyText = body = Object.prototype.toString.call(body); + } + + if (!this.headers.get('content-type')) { + if (typeof body === 'string') { + this.headers.set('content-type', 'text/plain;charset=UTF-8'); + } else if (this._bodyBlob && this._bodyBlob.type) { + this.headers.set('content-type', this._bodyBlob.type); + } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { + this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8'); + } + } + }; + + if (support.blob) { + this.blob = function() { + var rejected = consumed(this); + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return Promise.resolve(this._bodyBlob) + } else if (this._bodyArrayBuffer) { + return Promise.resolve(new Blob([this._bodyArrayBuffer])) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as blob') + } else { + return Promise.resolve(new Blob([this._bodyText])) + } + }; + + this.arrayBuffer = function() { + if (this._bodyArrayBuffer) { + var isConsumed = consumed(this); + if (isConsumed) { + return isConsumed + } + if (ArrayBuffer.isView(this._bodyArrayBuffer)) { + return Promise.resolve( + this._bodyArrayBuffer.buffer.slice( + this._bodyArrayBuffer.byteOffset, + this._bodyArrayBuffer.byteOffset + this._bodyArrayBuffer.byteLength + ) + ) + } else { + return Promise.resolve(this._bodyArrayBuffer) + } + } else { + return this.blob().then(readBlobAsArrayBuffer) + } + }; + } + + this.text = function() { + var rejected = consumed(this); + if (rejected) { + return rejected + } + + if (this._bodyBlob) { + return readBlobAsText(this._bodyBlob) + } else if (this._bodyArrayBuffer) { + return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer)) + } else if (this._bodyFormData) { + throw new Error('could not read FormData body as text') + } else { + return Promise.resolve(this._bodyText) + } + }; + + if (support.formData) { + this.formData = function() { + return this.text().then(decode$1) + }; + } + + this.json = function() { + return this.text().then(JSON.parse) + }; + + return this + } + + // HTTP methods whose capitalization should be normalized + var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']; + + function normalizeMethod(method) { + var upcased = method.toUpperCase(); + return methods.indexOf(upcased) > -1 ? upcased : method + } + + function Request(input, options) { + if (!(this instanceof Request)) { + throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.') + } + + options = options || {}; + var body = options.body; + + if (input instanceof Request) { + if (input.bodyUsed) { + throw new TypeError('Already read') + } + this.url = input.url; + this.credentials = input.credentials; + if (!options.headers) { + this.headers = new Headers(input.headers); + } + this.method = input.method; + this.mode = input.mode; + this.signal = input.signal; + if (!body && input._bodyInit != null) { + body = input._bodyInit; + input.bodyUsed = true; + } + } else { + this.url = String(input); + } + + this.credentials = options.credentials || this.credentials || 'same-origin'; + if (options.headers || !this.headers) { + this.headers = new Headers(options.headers); + } + this.method = normalizeMethod(options.method || this.method || 'GET'); + this.mode = options.mode || this.mode || null; + this.signal = options.signal || this.signal; + this.referrer = null; + + if ((this.method === 'GET' || this.method === 'HEAD') && body) { + throw new TypeError('Body not allowed for GET or HEAD requests') + } + this._initBody(body); + + if (this.method === 'GET' || this.method === 'HEAD') { + if (options.cache === 'no-store' || options.cache === 'no-cache') { + // Search for a '_' parameter in the query string + var reParamSearch = /([?&])_=[^&]*/; + if (reParamSearch.test(this.url)) { + // If it already exists then set the value with the current time + this.url = this.url.replace(reParamSearch, '$1_=' + new Date().getTime()); + } else { + // Otherwise add a new '_' parameter to the end with the current time + var reQueryString = /\?/; + this.url += (reQueryString.test(this.url) ? '&' : '?') + '_=' + new Date().getTime(); + } + } + } + } + + Request.prototype.clone = function() { + return new Request(this, {body: this._bodyInit}) + }; + + function decode$1(body) { + var form = new FormData(); + body + .trim() + .split('&') + .forEach(function(bytes) { + if (bytes) { + var split = bytes.split('='); + var name = split.shift().replace(/\+/g, ' '); + var value = split.join('=').replace(/\+/g, ' '); + form.append(decodeURIComponent(name), decodeURIComponent(value)); + } + }); + return form + } + + function parseHeaders(rawHeaders) { + var headers = new Headers(); + // Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space + // https://tools.ietf.org/html/rfc7230#section-3.2 + var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' '); + // Avoiding split via regex to work around a common IE11 bug with the core-js 3.6.0 regex polyfill + // https://github.com/github/fetch/issues/748 + // https://github.com/zloirock/core-js/issues/751 + preProcessedHeaders + .split('\r') + .map(function(header) { + return header.indexOf('\n') === 0 ? header.substr(1, header.length) : header + }) + .forEach(function(line) { + var parts = line.split(':'); + var key = parts.shift().trim(); + if (key) { + var value = parts.join(':').trim(); + headers.append(key, value); + } + }); + return headers + } + + Body.call(Request.prototype); + + function Response(bodyInit, options) { + if (!(this instanceof Response)) { + throw new TypeError('Please use the "new" operator, this DOM object constructor cannot be called as a function.') + } + if (!options) { + options = {}; + } + + this.type = 'default'; + this.status = options.status === undefined ? 200 : options.status; + this.ok = this.status >= 200 && this.status < 300; + this.statusText = options.statusText === undefined ? '' : '' + options.statusText; + this.headers = new Headers(options.headers); + this.url = options.url || ''; + this._initBody(bodyInit); + } + + Body.call(Response.prototype); + + Response.prototype.clone = function() { + return new Response(this._bodyInit, { + status: this.status, + statusText: this.statusText, + headers: new Headers(this.headers), + url: this.url + }) + }; + + Response.error = function() { + var response = new Response(null, {status: 0, statusText: ''}); + response.type = 'error'; + return response + }; + + var redirectStatuses = [301, 302, 303, 307, 308]; + + Response.redirect = function(url, status) { + if (redirectStatuses.indexOf(status) === -1) { + throw new RangeError('Invalid status code') + } + + return new Response(null, {status: status, headers: {location: url}}) + }; + + var DOMException = global$1.DOMException; + try { + new DOMException(); + } catch (err) { + DOMException = function(message, name) { + this.message = message; + this.name = name; + var error = Error(message); + this.stack = error.stack; + }; + DOMException.prototype = Object.create(Error.prototype); + DOMException.prototype.constructor = DOMException; + } + + function fetch(input, init) { + return new Promise(function(resolve, reject) { + var request = new Request(input, init); + + if (request.signal && request.signal.aborted) { + return reject(new DOMException('Aborted', 'AbortError')) + } + + var xhr = new XMLHttpRequest(); + + function abortXhr() { + xhr.abort(); + } + + xhr.onload = function() { + var options = { + status: xhr.status, + statusText: xhr.statusText, + headers: parseHeaders(xhr.getAllResponseHeaders() || '') + }; + options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL'); + var body = 'response' in xhr ? xhr.response : xhr.responseText; + setTimeout(function() { + resolve(new Response(body, options)); + }, 0); + }; + + xhr.onerror = function() { + setTimeout(function() { + reject(new TypeError('Network request failed')); + }, 0); + }; + + xhr.ontimeout = function() { + setTimeout(function() { + reject(new TypeError('Network request failed')); + }, 0); + }; + + xhr.onabort = function() { + setTimeout(function() { + reject(new DOMException('Aborted', 'AbortError')); + }, 0); + }; + + function fixUrl(url) { + try { + return url === '' && global$1.location.href ? global$1.location.href : url + } catch (e) { + return url + } + } + + xhr.open(request.method, fixUrl(request.url), true); + + if (request.credentials === 'include') { + xhr.withCredentials = true; + } else if (request.credentials === 'omit') { + xhr.withCredentials = false; + } + + if ('responseType' in xhr) { + if (support.blob) { + xhr.responseType = 'blob'; + } else if ( + support.arrayBuffer && + request.headers.get('Content-Type') && + request.headers.get('Content-Type').indexOf('application/octet-stream') !== -1 + ) { + xhr.responseType = 'arraybuffer'; + } + } + + if (init && typeof init.headers === 'object' && !(init.headers instanceof Headers)) { + Object.getOwnPropertyNames(init.headers).forEach(function(name) { + xhr.setRequestHeader(name, normalizeValue(init.headers[name])); + }); + } else { + request.headers.forEach(function(value, name) { + xhr.setRequestHeader(name, value); + }); + } + + if (request.signal) { + request.signal.addEventListener('abort', abortXhr); + + xhr.onreadystatechange = function() { + // DONE (success or failure) + if (xhr.readyState === 4) { + request.signal.removeEventListener('abort', abortXhr); + } + }; + } + + xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit); + }) + } + + fetch.polyfill = true; + + if (!global$1.fetch) { + global$1.fetch = fetch; + global$1.Headers = Headers; + global$1.Request = Request; + global$1.Response = Response; + } + + var FakeFetch = /*#__PURE__*/Object.freeze({ + __proto__: null, + Headers: Headers, + Request: Request, + Response: Response, + get DOMException () { return DOMException; }, + fetch: fetch + }); + + function createPassthrough(fakeXHR, nativeXMLHttpRequest) { + // event types to handle on the xhr + var evts = ['error', 'timeout', 'abort', 'readystatechange']; + // event types to handle on the xhr.upload + var uploadEvents = []; + // properties to copy from the native xhr to fake xhr + var lifecycleProps = [ + 'readyState', + 'responseText', + 'response', + 'responseXML', + 'responseURL', + 'status', + 'statusText', + ]; + var xhr = (fakeXHR._passthroughRequest = new nativeXMLHttpRequest()); + xhr.open(fakeXHR.method, fakeXHR.url, fakeXHR.async, fakeXHR.username, fakeXHR.password); + if (fakeXHR.responseType === 'arraybuffer') { + lifecycleProps = ['readyState', 'response', 'status', 'statusText']; + xhr.responseType = fakeXHR.responseType; + } + // use onload if the browser supports it + if ('onload' in xhr) { + evts.push('load'); + } + // add progress event for async calls + // avoid using progress events for sync calls, they will hang https://bugs.webkit.org/show_bug.cgi?id=40996. + if (fakeXHR.async && fakeXHR.responseType !== 'arraybuffer') { + evts.push('progress'); + uploadEvents.push('progress'); + } + // update `propertyNames` properties from `fromXHR` to `toXHR` + function copyLifecycleProperties(propertyNames, fromXHR, toXHR) { + for (var i = 0; i < propertyNames.length; i++) { + var prop = propertyNames[i]; + if (prop in fromXHR) { + toXHR[prop] = fromXHR[prop]; + } + } + } + // fire fake event on `eventable` + function dispatchEvent(eventable, eventType, event) { + eventable.dispatchEvent(event); + if (eventable['on' + eventType]) { + eventable['on' + eventType](event); + } + } + // set the on- handler on the native xhr for the given eventType + function createHandler(eventType) { + xhr['on' + eventType] = function (event) { + copyLifecycleProperties(lifecycleProps, xhr, fakeXHR); + dispatchEvent(fakeXHR, eventType, event); + }; + } + // set the on- handler on the native xhr's `upload` property for + // the given eventType + function createUploadHandler(eventType) { + if (xhr.upload && fakeXHR.upload && fakeXHR.upload['on' + eventType]) { + xhr.upload['on' + eventType] = function (event) { + dispatchEvent(fakeXHR.upload, eventType, event); + }; + } + } + var i; + for (i = 0; i < evts.length; i++) { + createHandler(evts[i]); + } + for (i = 0; i < uploadEvents.length; i++) { + createUploadHandler(uploadEvents[i]); + } + if (fakeXHR.async) { + xhr.timeout = fakeXHR.timeout; + xhr.withCredentials = fakeXHR.withCredentials; + } + // XMLHttpRequest.timeout default initializes to 0, and is not allowed to be used for + // synchronous XMLHttpRequests requests in a document environment. However, when a XHR + // polyfill does not sets the timeout value, it will throw in React Native environment. + // TODO: + // synchronous XHR is deprecated, make async the default as XMLHttpRequest.open(), + // and throw error if sync XHR has timeout not 0 + if (!xhr.timeout && xhr.timeout !== 0) { + xhr.timeout = 0; // default XMLHttpRequest timeout + } + for (var h in fakeXHR.requestHeaders) { + xhr.setRequestHeader(h, fakeXHR.requestHeaders[h]); + } + return xhr; + } + + function interceptor(ctx) { + function FakeRequest() { + // super() + FakeXMLHttpRequest.call(this); + } + FakeRequest.prototype = Object.create(FakeXMLHttpRequest.prototype); + FakeRequest.prototype.constructor = FakeRequest; + // extend + FakeRequest.prototype.send = function send() { + this.sendArguments = arguments; + if (!ctx.pretender.running) { + throw new Error('You shut down a Pretender instance while there was a pending request. ' + + 'That request just tried to complete. Check to see if you accidentally shut down ' + + 'a pretender earlier than you intended to'); + } + FakeXMLHttpRequest.prototype.send.apply(this, arguments); + if (ctx.pretender.checkPassthrough(this)) { + this.passthrough(); + } + else { + ctx.pretender.handleRequest(this); + } + }; + FakeRequest.prototype.passthrough = function passthrough() { + if (!this.sendArguments) { + throw new Error('You attempted to passthrough a FakeRequest that was never sent. ' + + 'Call `.send()` on the original request first'); + } + var xhr = createPassthrough(this, ctx.pretender._nativeXMLHttpRequest); + xhr.send.apply(xhr, this.sendArguments); + return xhr; + }; + FakeRequest.prototype._passthroughCheck = function (method, args) { + if (this._passthroughRequest) { + return this._passthroughRequest[method].apply(this._passthroughRequest, args); + } + return FakeXMLHttpRequest.prototype[method].apply(this, args); + }; + FakeRequest.prototype.abort = function abort() { + return this._passthroughCheck('abort', arguments); + }; + FakeRequest.prototype.getResponseHeader = function getResponseHeader() { + return this._passthroughCheck('getResponseHeader', arguments); + }; + FakeRequest.prototype.getAllResponseHeaders = function getAllResponseHeaders() { + return this._passthroughCheck('getAllResponseHeaders', arguments); + }; + if (ctx.pretender._nativeXMLHttpRequest.prototype._passthroughCheck) { + // eslint-disable-next-line no-console + console.warn('You created a second Pretender instance while there was already one running. ' + + 'Running two Pretender servers at once will lead to unexpected results and will ' + + 'be removed entirely in a future major version.' + + 'Please call .shutdown() on your instances when you no longer need them to respond.'); + } + return FakeRequest; + } + + var NoopArray = /** @class */ (function () { + function NoopArray() { + this.length = 0; + } + NoopArray.prototype.push = function () { + var _items = []; + for (var _i = 0; _i < arguments.length; _i++) { + _items[_i] = arguments[_i]; + } + return 0; + }; + return NoopArray; + }()); + function scheduleProgressEvent(request, startTime, totalTime) { + var totalSize = 0; + var body = request.requestBody; + if (body) { + if (body instanceof FormData) { + body.forEach(function (value) { + if (value instanceof File) { + totalSize += value.size; + } + else { + totalSize += value.length; + } + }); + } + else { + // Support Blob, BufferSource, USVString, ArrayBufferView + totalSize = body.byteLength || body.size || body.length || 0; + } + } + setTimeout(function () { + if (!request.aborted && !request.status) { + var elapsedTime = new Date().getTime() - startTime.getTime(); + var progressTransmitted = totalTime <= 0 ? 0 : (elapsedTime / totalTime) * totalSize; + // ProgressEvent expects loaded, total + // https://xhr.spec.whatwg.org/#interface-progressevent + request.upload._progress(true, progressTransmitted, totalSize); + request._progress(true, progressTransmitted, totalSize); + scheduleProgressEvent(request, startTime, totalTime); + } + else if (request.status) { + // we're done, send a final progress event with loaded === total + request.upload._progress(true, totalSize, totalSize); + request._progress(true, totalSize, totalSize); + } + }, 50); + } + function isArray(array) { + return Object.prototype.toString.call(array) === '[object Array]'; + } + var PASSTHROUGH = {}; + function verbify(verb) { + return function (path, handler, async) { + return this.register(verb, path, handler, async); + }; + } + var Pretender = /** @class */ (function () { + function Pretender() { + var _this = this; + this.hosts = new Hosts(); + this.handlers = []; + this.get = verbify('GET'); + this.post = verbify('POST'); + this.put = verbify('PUT'); + this.delete = verbify('DELETE'); + this.patch = verbify('PATCH'); + this.head = verbify('HEAD'); + this.options = verbify('OPTIONS'); + this.passthrough = PASSTHROUGH; + var lastArg = arguments[arguments.length - 1]; + var options = typeof lastArg === 'object' ? lastArg : null; + var shouldNotTrack = options && options.trackRequests === false; + this.handledRequests = shouldNotTrack ? new NoopArray() : []; + this.passthroughRequests = shouldNotTrack ? new NoopArray() : []; + this.unhandledRequests = shouldNotTrack ? new NoopArray() : []; + this.requestReferences = []; + this.forcePassthrough = options && options.forcePassthrough === true; + this.disableUnhandled = options && options.disableUnhandled === true; + // reference the native XMLHttpRequest object so + // it can be restored later + this._nativeXMLHttpRequest = self.XMLHttpRequest; + this.running = false; + var ctx = { pretender: this }; + this.ctx = ctx; + // capture xhr requests, channeling them into + // the route map. + self.XMLHttpRequest = interceptor(ctx); + // polyfill fetch when xhr is ready + this._fetchProps = FakeFetch + ? ['fetch', 'Headers', 'Request', 'Response'] + : []; + this._fetchProps.forEach(function (name) { + _this['_native' + name] = self[name]; + self[name] = FakeFetch[name]; + }, this); + // 'start' the server + this.running = true; + // trigger the route map DSL. + var argLength = options ? arguments.length - 1 : arguments.length; + for (var i = 0; i < argLength; i++) { + this.map(arguments[i]); + } + } + Pretender.prototype.map = function (maps) { + maps.call(this); + }; + Pretender.prototype.register = function (verb, url, handler, async) { + if (!handler) { + throw new Error('The function you tried passing to Pretender to handle ' + + verb + + ' ' + + url + + ' is undefined or missing.'); + } + var handlerInstance = handler; + handlerInstance.numberOfCalls = 0; + handlerInstance.async = async; + this.handlers.push(handlerInstance); + var registry = this.hosts.forURL(url)[verb]; + registry.add([ + { + path: parseURL(url).fullpath, + handler: handlerInstance, + }, + ]); + return handlerInstance; + }; + Pretender.prototype.checkPassthrough = function (request) { + var verb = request.method.toUpperCase(); + var path = parseURL(request.url).fullpath; + var recognized = this.hosts.forURL(request.url)[verb].recognize(path); + var match = recognized && recognized[0]; + if ((match && match.handler === PASSTHROUGH) || this.forcePassthrough) { + this.passthroughRequests.push(request); + this.passthroughRequest(verb, path, request); + return true; + } + return false; + }; + Pretender.prototype.handleRequest = function (request) { + var verb = request.method.toUpperCase(); + var path = request.url; + var handler = this._handlerFor(verb, path, request); + if (handler) { + handler.handler.numberOfCalls++; + var async_1 = handler.handler.async; + this.handledRequests.push(request); + var pretender_1 = this; + var _handleRequest_1 = function (statusHeadersAndBody) { + if (!isArray(statusHeadersAndBody)) { + var note = 'Remember to `return [status, headers, body];` in your route handler.'; + throw new Error('Nothing returned by handler for ' + path + '. ' + note); + } + var status = statusHeadersAndBody[0]; + var headers = pretender_1.prepareHeaders(statusHeadersAndBody[1]); + var body = pretender_1.prepareBody(statusHeadersAndBody[2], headers); + pretender_1.handleResponse(request, async_1, function () { + request.respond(status, headers, body); + pretender_1.handledRequest(verb, path, request); + }); + }; + try { + var result = handler.handler(request); + if (result && typeof result.then === 'function') { + // `result` is a promise, resolve it + result.then(function (resolvedResult) { + _handleRequest_1(resolvedResult); + }); + } + else { + _handleRequest_1(result); + } + } + catch (error) { + this.erroredRequest(verb, path, request, error); + this.resolve(request); + } + } + else { + if (!this.disableUnhandled) { + this.unhandledRequests.push(request); + this.unhandledRequest(verb, path, request); + } + } + }; + Pretender.prototype.handleResponse = function (request, strategy, callback) { + var delay = typeof strategy === 'function' ? strategy() : strategy; + delay = typeof delay === 'boolean' || typeof delay === 'number' ? delay : 0; + if (delay === false) { + callback(); + } + else { + var pretender_2 = this; + pretender_2.requestReferences.push({ + request: request, + callback: callback, + }); + if (delay !== true) { + scheduleProgressEvent(request, new Date(), delay); + setTimeout(function () { + pretender_2.resolve(request); + }, delay); + } + } + }; + Pretender.prototype.resolve = function (request) { + for (var i = 0, len = this.requestReferences.length; i < len; i++) { + var res = this.requestReferences[i]; + if (res.request === request) { + res.callback(); + this.requestReferences.splice(i, 1); + break; + } + } + }; + Pretender.prototype.requiresManualResolution = function (verb, path) { + var handler = this._handlerFor(verb.toUpperCase(), path, {}); + if (!handler) { + return false; + } + var async = handler.handler.async; + return typeof async === 'function' ? async() === true : async === true; + }; + Pretender.prototype.prepareBody = function (body, _headers) { + return body; + }; + Pretender.prototype.prepareHeaders = function (headers) { + return headers; + }; + Pretender.prototype.handledRequest = function (_verb, _path, _request) { + /* no-op */ + }; + Pretender.prototype.passthroughRequest = function (_verb, _path, _request) { + /* no-op */ + }; + Pretender.prototype.unhandledRequest = function (verb, path, _request) { + throw new Error('Pretender intercepted ' + + verb + + ' ' + + path + + ' but no handler was defined for this type of request'); + }; + Pretender.prototype.erroredRequest = function (verb, path, _request, error) { + error.message = + 'Pretender intercepted ' + + verb + + ' ' + + path + + ' but encountered an error: ' + + error.message; + throw error; + }; + Pretender.prototype.shutdown = function () { + var _this = this; + self.XMLHttpRequest = this._nativeXMLHttpRequest; + this._fetchProps.forEach(function (name) { + self[name] = _this['_native' + name]; + }, this); + this.ctx.pretender = undefined; + // 'stop' the server + this.running = false; + }; + Pretender.prototype._handlerFor = function (verb, url, request) { + var registry = this.hosts.forURL(url)[verb]; + var matches = registry.recognize(parseURL(url).fullpath); + var match = matches ? matches[0] : null; + if (match) { + request.params = match.params; + request.queryParams = matches.queryParams; + } + return match; + }; + Pretender.parseURL = parseURL; + Pretender.Hosts = Hosts; + Pretender.Registry = Registry; + return Pretender; + }()); + + Pretender.parseURL = parseURL; + Pretender.Hosts = Hosts; + Pretender.Registry = Registry; + + return Pretender; + +}(RouteRecognizer, FakeXMLHttpRequest)); + + + if (typeof module === 'object') { + module.exports = Pretender; + } else if (typeof define !== 'undefined') { + define('pretender', [], function() { + return Pretender; + }); } - var host = anchor.host; - if (anchor.port === '80' || anchor.port === '443') { - host = anchor.hostname; // IE: remove default port - } + self.Pretender = Pretender; - return { - host: host, - protocol: anchor.protocol, - search: anchor.search, - hash: anchor.hash, - href: anchor.href, - pathname: pathname, - fullpath: pathname + (anchor.search || '') + (anchor.hash || '') - }; -} - - -/** - * Registry - * - * A registry is a map of HTTP verbs to route recognizers. - */ - -function Registry(/* host */) { - // Herein we keep track of RouteRecognizer instances - // keyed by HTTP method. Feel free to add more as needed. - this.verbs = { - GET: new RouteRecognizer(), - PUT: new RouteRecognizer(), - POST: new RouteRecognizer(), - DELETE: new RouteRecognizer(), - PATCH: new RouteRecognizer(), - HEAD: new RouteRecognizer(), - OPTIONS: new RouteRecognizer() - }; -} - -/** - * Hosts - * - * a map of hosts to Registries, ultimately allowing - * a per-host-and-port, per HTTP verb lookup of RouteRecognizers - */ -function Hosts() { - this._registries = {}; -} - -/** - * Hosts#forURL - retrieve a map of HTTP verbs to RouteRecognizers - * for a given URL - * - * @param {String} url a URL - * @return {Registry} a map of HTTP verbs to RouteRecognizers - * corresponding to the provided URL's - * hostname and port - */ -Hosts.prototype.forURL = function(url) { - var host = parseURL(url).host; - var registry = this._registries[host]; - - if (registry === undefined) { - registry = (this._registries[host] = new Registry(host)); - } - - return registry.verbs; -}; - - -function Pretender(/* routeMap1, routeMap2, ..., options*/) { - this.hosts = new Hosts(); - - var lastArg = arguments[arguments.length - 1]; - var options = typeof lastArg === 'object' ? lastArg : null; - var shouldNotTrack = options && (options.trackRequests === false); - var noopArray = { push: function() {}, length: 0 }; - - this.handlers = []; - this.handledRequests = shouldNotTrack ? noopArray: []; - this.passthroughRequests = shouldNotTrack ? noopArray: []; - this.unhandledRequests = shouldNotTrack ? noopArray: []; - this.requestReferences = []; - this.forcePassthrough = options && (options.forcePassthrough === true); - this.disableUnhandled = options && (options.disableUnhandled === true); - - // reference the native XMLHttpRequest object so - // it can be restored later - this._nativeXMLHttpRequest = self.XMLHttpRequest; - this.running = false; - var ctx = { pretender: this }; - this.ctx = ctx; - - // capture xhr requests, channeling them into - // the route map. - self.XMLHttpRequest = interceptor(ctx); - - // 'start' the server - this.running = true; - - // trigger the route map DSL. - var argLength = options ? arguments.length - 1 : arguments.length; - for (var i = 0; i < argLength; i++) { - this.map(arguments[i]); - } -} - -function interceptor(ctx) { - function FakeRequest() { - // super() - FakeXMLHttpRequest.call(this); - } - FakeRequest.prototype = Object.create(FakeXMLHttpRequest.prototype); - FakeRequest.prototype.constructor = FakeRequest; - - // extend - FakeRequest.prototype.send = function send() { - if (!ctx.pretender.running) { - throw new Error('You shut down a Pretender instance while there was a pending request. ' + - 'That request just tried to complete. Check to see if you accidentally shut down ' + - 'a pretender earlier than you intended to'); - } - - FakeXMLHttpRequest.prototype.send.apply(this, arguments); - - if (ctx.pretender.checkPassthrough(this)) { - var xhr = createPassthrough(this); - xhr.send.apply(xhr, arguments); - } else { - ctx.pretender.handleRequest(this); - } - }; - - - function createPassthrough(fakeXHR) { - // event types to handle on the xhr - var evts = ['error', 'timeout', 'abort', 'readystatechange']; - - // event types to handle on the xhr.upload - var uploadEvents = []; - - // properties to copy from the native xhr to fake xhr - var lifecycleProps = ['readyState', 'responseText', 'responseXML', 'status', 'statusText']; - - var xhr = fakeXHR._passthroughRequest = new ctx.pretender._nativeXMLHttpRequest(); - xhr.open(fakeXHR.method, fakeXHR.url, fakeXHR.async, fakeXHR.username, fakeXHR.password); - - if (fakeXHR.responseType === 'arraybuffer') { - lifecycleProps = ['readyState', 'response', 'status', 'statusText']; - xhr.responseType = fakeXHR.responseType; - } - - // use onload if the browser supports it - if ('onload' in xhr) { - evts.push('load'); - } - - // add progress event for async calls - // avoid using progress events for sync calls, they will hang https://bugs.webkit.org/show_bug.cgi?id=40996. - if (fakeXHR.async && fakeXHR.responseType !== 'arraybuffer') { - evts.push('progress'); - uploadEvents.push('progress'); - } - - // update `propertyNames` properties from `fromXHR` to `toXHR` - function copyLifecycleProperties(propertyNames, fromXHR, toXHR) { - for (var i = 0; i < propertyNames.length; i++) { - var prop = propertyNames[i]; - if (prop in fromXHR) { - toXHR[prop] = fromXHR[prop]; - } - } - } - - // fire fake event on `eventable` - function dispatchEvent(eventable, eventType, event) { - eventable.dispatchEvent(event); - if (eventable['on' + eventType]) { - eventable['on' + eventType](event); - } - } - - // set the on- handler on the native xhr for the given eventType - function createHandler(eventType) { - xhr['on' + eventType] = function(event) { - copyLifecycleProperties(lifecycleProps, xhr, fakeXHR); - dispatchEvent(fakeXHR, eventType, event); - }; - } - - // set the on- handler on the native xhr's `upload` property for - // the given eventType - function createUploadHandler(eventType) { - if (xhr.upload) { - xhr.upload['on' + eventType] = function(event) { - dispatchEvent(fakeXHR.upload, eventType, event); - }; - } - } - - var i; - for (i = 0; i < evts.length; i++) { - createHandler(evts[i]); - } - for (i = 0; i < uploadEvents.length; i++) { - createUploadHandler(uploadEvents[i]); - } - - if (fakeXHR.async) { - xhr.timeout = fakeXHR.timeout; - xhr.withCredentials = fakeXHR.withCredentials; - } - for (var h in fakeXHR.requestHeaders) { - xhr.setRequestHeader(h, fakeXHR.requestHeaders[h]); - } - return xhr; - } - - FakeRequest.prototype._passthroughCheck = function(method, args) { - if (this._passthroughRequest) { - return this._passthroughRequest[method].apply(this._passthroughRequest, args); - } - return FakeXMLHttpRequest.prototype[method].apply(this, args); - }; - - FakeRequest.prototype.abort = function abort() { - return this._passthroughCheck('abort', arguments); - }; - - FakeRequest.prototype.getResponseHeader = function getResponseHeader() { - return this._passthroughCheck('getResponseHeader', arguments); - }; - - FakeRequest.prototype.getAllResponseHeaders = function getAllResponseHeaders() { - return this._passthroughCheck('getAllResponseHeaders', arguments); - }; - - if (ctx.pretender._nativeXMLHttpRequest.prototype._passthroughCheck) { - console.warn('You created a second Pretender instance while there was already one running. ' + - 'Running two Pretender servers at once will lead to unexpected results and will ' + - 'be removed entirely in a future major version.' + - 'Please call .shutdown() on your instances when you no longer need them to respond.'); - } - return FakeRequest; -} - -function verbify(verb) { - return function(path, handler, async) { - return this.register(verb, path, handler, async); - }; -} - -function scheduleProgressEvent(request, startTime, totalTime) { - setTimeout(function() { - if (!request.aborted && !request.status) { - var ellapsedTime = new Date().getTime() - startTime.getTime(); - request.upload._progress(true, ellapsedTime, totalTime); - request._progress(true, ellapsedTime, totalTime); - scheduleProgressEvent(request, startTime, totalTime); - } - }, 50); -} - -function isArray(array) { - return Object.prototype.toString.call(array) === '[object Array]'; -} - -var PASSTHROUGH = {}; - -Pretender.prototype = { - get: verbify('GET'), - post: verbify('POST'), - put: verbify('PUT'), - 'delete': verbify('DELETE'), - patch: verbify('PATCH'), - head: verbify('HEAD'), - options: verbify('OPTIONS'), - map: function(maps) { - maps.call(this); - }, - register: function register(verb, url, handler, async) { - if (!handler) { - throw new Error('The function you tried passing to Pretender to handle ' + - verb + ' ' + url + ' is undefined or missing.'); - } - - handler.numberOfCalls = 0; - handler.async = async; - this.handlers.push(handler); - - var registry = this.hosts.forURL(url)[verb]; - - registry.add([{ - path: parseURL(url).fullpath, - handler: handler - }]); - - return handler; - }, - passthrough: PASSTHROUGH, - checkPassthrough: function checkPassthrough(request) { - var verb = request.method.toUpperCase(); - var path = parseURL(request.url).fullpath; - var recognized = this.hosts.forURL(request.url)[verb].recognize(path); - var match = recognized && recognized[0]; - - if ((match && match.handler === PASSTHROUGH) || this.forcePassthrough) { - this.passthroughRequests.push(request); - this.passthroughRequest(verb, path, request); - return true; - } - - return false; - }, - handleRequest: function handleRequest(request) { - var verb = request.method.toUpperCase(); - var path = request.url; - - var handler = this._handlerFor(verb, path, request); - - if (handler) { - handler.handler.numberOfCalls++; - var async = handler.handler.async; - this.handledRequests.push(request); - - var pretender = this; - - var _handleRequest = function(statusHeadersAndBody) { - if (!isArray(statusHeadersAndBody)) { - var note = 'Remember to `return [status, headers, body];` in your route handler.'; - throw new Error('Nothing returned by handler for ' + path + '. ' + note); - } - - var status = statusHeadersAndBody[0], - headers = pretender.prepareHeaders(statusHeadersAndBody[1]), - body = pretender.prepareBody(statusHeadersAndBody[2], headers); - - pretender.handleResponse(request, async, function() { - request.respond(status, headers, body); - pretender.handledRequest(verb, path, request); - }); - }; - - try { - var result = handler.handler(request); - if (result && typeof result.then === 'function') { - // `result` is a promise, resolve it - result.then(function(resolvedResult) { - _handleRequest(resolvedResult); - }); - } else { - _handleRequest(result); - } - } catch (error) { - this.erroredRequest(verb, path, request, error); - this.resolve(request); - } - } else { - if (!this.disableUnhandled) { - this.unhandledRequests.push(request); - this.unhandledRequest(verb, path, request); - } - } - }, - handleResponse: function handleResponse(request, strategy, callback) { - var delay = typeof strategy === 'function' ? strategy() : strategy; - delay = typeof delay === 'boolean' || typeof delay === 'number' ? delay : 0; - - if (delay === false) { - callback(); - } else { - var pretender = this; - pretender.requestReferences.push({ - request: request, - callback: callback - }); - - if (delay !== true) { - scheduleProgressEvent(request, new Date(), delay); - setTimeout(function() { - pretender.resolve(request); - }, delay); - } - } - }, - resolve: function resolve(request) { - for (var i = 0, len = this.requestReferences.length; i < len; i++) { - var res = this.requestReferences[i]; - if (res.request === request) { - res.callback(); - this.requestReferences.splice(i, 1); - break; - } - } - }, - requiresManualResolution: function(verb, path) { - var handler = this._handlerFor(verb.toUpperCase(), path, {}); - if (!handler) { return false; } - - var async = handler.handler.async; - return typeof async === 'function' ? async() === true : async === true; - }, - prepareBody: function(body) { return body; }, - prepareHeaders: function(headers) { return headers; }, - handledRequest: function(/* verb, path, request */) { /* no-op */}, - passthroughRequest: function(/* verb, path, request */) { /* no-op */}, - unhandledRequest: function(verb, path/*, request */) { - throw new Error('Pretender intercepted ' + verb + ' ' + - path + ' but no handler was defined for this type of request'); - }, - erroredRequest: function(verb, path, request, error) { - error.message = 'Pretender intercepted ' + verb + ' ' + - path + ' but encountered an error: ' + error.message; - throw error; - }, - _handlerFor: function(verb, url, request) { - var registry = this.hosts.forURL(url)[verb]; - var matches = registry.recognize(parseURL(url).fullpath); - - var match = matches ? matches[0] : null; - if (match) { - request.params = match.params; - request.queryParams = matches.queryParams; - } - - return match; - }, - shutdown: function shutdown() { - self.XMLHttpRequest = this._nativeXMLHttpRequest; - this.ctx.pretender = undefined; - // 'stop' the server - this.running = false; - } -}; - -Pretender.parseURL = parseURL; -Pretender.Hosts = Hosts; -Pretender.Registry = Registry; - -if (typeof module === 'object') { - module.exports = Pretender; -} else if (typeof define !== 'undefined') { - define('pretender', [], function() { - return Pretender; - }); -} -self.Pretender = Pretender; -}(self)); + return Pretender; +})(self); diff --git a/yarn.lock b/yarn.lock index 3c1e5fcca7..e787964b31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1932,10 +1932,10 @@ extract-zip@^1.6.6: mkdirp "^0.5.4" yauzl "^2.10.0" -fake-xml-http-request@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/fake-xml-http-request/-/fake-xml-http-request-1.6.0.tgz#bd0ac79ae3e2660098282048a12c730a6f64d550" - integrity sha512-99XPwwSg89BfzPuv4XCpZxn3EbauMCgAQCxq9MzrvS6DFD73OON6AnUTicL4A0HZtYMBwCZBWVnRqGjZDgQkTg== +fake-xml-http-request@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/fake-xml-http-request/-/fake-xml-http-request-2.1.2.tgz#f1786720cae50bbb46273035a0173414f3e85e74" + integrity sha512-HaFMBi7r+oEC9iJNpc3bvcW7Z7iLmM26hPDmlb0mFwyANSsOQAtJxbdWsXITKOzZUyMYK0zYCv3h5yDj9TsiXg== fast-deep-equal@^3.1.1: version "3.1.3" @@ -3527,12 +3527,12 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -pretender@^1.6: - version "1.6.1" - resolved "https://registry.yarnpkg.com/pretender/-/pretender-1.6.1.tgz#77d1e42ac8c6b298f5cd43534a87645df035db8c" - integrity sha1-d9HkKsjGspj1zUNTSodkXfA124w= +pretender@^3.4.7: + version "3.4.7" + resolved "https://registry.yarnpkg.com/pretender/-/pretender-3.4.7.tgz#34a2ae2d1fc9db440a990d50e6c0f5481d8755fc" + integrity sha512-jkPAvt1BfRi0RKamweJdEcnjkeu7Es8yix3bJ+KgBC5VpG/Ln4JE3hYN6vJym4qprm8Xo5adhWpm3HCoft1dOw== dependencies: - fake-xml-http-request "^1.6.0" + fake-xml-http-request "^2.1.2" route-recognizer "^0.3.3" prettier@2.2.1, prettier@^2.0.4: