(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 */ var _Event = function Event(type, bubbles, cancelable, target) { this.type = type; this.bubbles = bubbles; this.cancelable = cancelable; this.target = target; }; _Event.prototype = { stopPropagation: function () {}, preventDefault: function () { this.defaultPrevented = true; } }; /* 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); } }); } /* 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 */ 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); }, /* 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); } } 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; } 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)) { 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"; } 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.errorFlag = true; this.requestHeaders = {}; if (this.readyState > FakeXMLHttpRequest.UNSENT && this.sendFlag) { this._readyStateChange(FakeXMLHttpRequest.DONE); this.sendFlag = false; } this.readyState = FakeXMLHttpRequest.UNSENT; this.dispatchEvent(new _Event("abort", false, false, this)); 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]; } } return null; }, /* 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"; } } 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]; } } if (this.async) { 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 = ""; do { if (this.async) { this._readyStateChange(FakeXMLHttpRequest.LOADING); } 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 (typeof this.onload === "function"){ this.onload(); } } }; 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 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; } } 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; } })();