/*! jquery mockjax * a plugin providing simple and flexible mocking of ajax requests and responses * * version: 2.2.1 * home: https://github.com/jakerella/jquery-mockjax * copyright (c) 2017 jordan kasper, formerly appendto; * note: this repository was taken over by jordan kasper (@jakerella) october, 2014 * * dual licensed under the mit or gpl licenses. * http://opensource.org/licenses/mit or http://www.gnu.org/licenses/gpl-2.0.html */ (function(root, factory) { 'use strict'; // amdjs module definition if ( typeof define === 'function' && define.amd && define.amd.jquery ) { define(['jquery'], function($) { return factory($, root); }); // commonjs module definition } else if ( typeof exports === 'object') { // note: to use mockjax as a node module you must provide the factory with // a valid version of jquery and a window object (the global scope): // var mockjax = require('jquery.mockjax')(jquery, window); module.exports = factory; // global jquery in web browsers } else { return factory(root.jquery || root.$, root); } }(this, function($, window) { 'use strict'; var _ajax = $.ajax, mockhandlers = [], mockedajaxcalls = [], unmockedajaxcalls = [], callback_regex = /=\?(&|$)/, jsc = (new date()).gettime(), default_response_time = 500; // parse the given xml string. function parsexml(xml) { if ( window.domparser === undefined && window.activexobject ) { window.domparser = function() { }; domparser.prototype.parsefromstring = function( xmlstring ) { var doc = new activexobject('microsoft.xmldom'); doc.async = 'false'; doc.loadxml( xmlstring ); return doc; }; } try { var xmldoc = ( new domparser() ).parsefromstring( xml, 'text/xml' ); if ( $.isxmldoc( xmldoc ) ) { var err = $('parsererror', xmldoc); if ( err.length === 1 ) { throw new error('error: ' + $(xmldoc).text() ); } } else { throw new error('unable to parse xml'); } return xmldoc; } catch( e ) { var msg = ( e.name === undefined ? e : e.name + ': ' + e.message ); $(document).trigger('xmlparseerror', [ msg ]); return undefined; } } // check if the data field on the mock handler and the request match. this // can be used to restrict a mock handler to being used only when a certain // set of data is passed to it. function ismockdataequal( mock, live ) { logger.debug( mock, ['checking mock data against request data', mock, live] ); var identical = true; if ( $.isfunction(mock) ) { return !!mock(live); } // test for situations where the data is a querystring (not an object) if (typeof live === 'string') { // querystring may be a regex if ($.isfunction( mock.test )) { return mock.test(live); } else if (typeof mock === 'object') { live = getqueryparams(live); } else { return mock === live; } } $.each(mock, function(k) { if ( live[k] === undefined ) { identical = false; return identical; } else { if ( typeof live[k] === 'object' && live[k] !== null ) { if ( identical && $.isarray( live[k] ) ) { identical = $.isarray( mock[k] ) && live[k].length === mock[k].length; } identical = identical && ismockdataequal(mock[k], live[k]); } else { if ( mock[k] && $.isfunction( mock[k].test ) ) { identical = identical && mock[k].test(live[k]); } else { identical = identical && ( mock[k] === live[k] ); } } } }); return identical; } function getqueryparams(querystring) { var i, l, param, tmp, paramsobj = {}, params = string(querystring).split(/&/); for (i=0, l=params.length; i= 0; } function parseresponsetimeopt(responsetime) { if ($.isarray(responsetime) && responsetime.length === 2) { var min = responsetime[0]; var max = responsetime[1]; if(isposnum(min) && isposnum(max)) { return math.floor(math.random() * (max - min)) + min; } } else if(isposnum(responsetime)) { return responsetime; } return default_response_time; } // process the xhr objects send operation function _xhrsend(mockhandler, requestsettings, origsettings) { logger.debug( mockhandler, ['sending fake xhr request', mockhandler, requestsettings, origsettings] ); // this is a substitute for < 1.4 which lacks $.proxy var process = (function(that) { return function() { return (function() { // the request has returned this.status = mockhandler.status; this.statustext = mockhandler.statustext; this.readystate = 1; var finishrequest = function () { this.readystate = 4; var onready; // copy over our mock to our xhr object before passing control back to // jquery's onreadystatechange callback if ( requestsettings.datatype === 'json' && ( typeof mockhandler.responsetext === 'object' ) ) { this.responsetext = json.stringify(mockhandler.responsetext); } else if ( requestsettings.datatype === 'xml' ) { if ( typeof mockhandler.responsexml === 'string' ) { this.responsexml = parsexml(mockhandler.responsexml); //in jquery 1.9.1+, responsexml is processed differently and relies on responsetext this.responsetext = mockhandler.responsexml; } else { this.responsexml = mockhandler.responsexml; } } else if (typeof mockhandler.responsetext === 'object' && mockhandler.responsetext !== null) { // since jquery 1.9 responsetext type has to match contenttype mockhandler.contenttype = 'application/json'; this.responsetext = json.stringify(mockhandler.responsetext); } else { this.responsetext = mockhandler.responsetext; } if( typeof mockhandler.status === 'number' || typeof mockhandler.status === 'string' ) { this.status = mockhandler.status; } if( typeof mockhandler.statustext === 'string') { this.statustext = mockhandler.statustext; } // jquery 2.0 renamed onreadystatechange to onload onready = this.onload || this.onreadystatechange; // jquery < 1.4 doesn't have onreadystate change for xhr if ( $.isfunction( onready ) ) { if( mockhandler.istimeout) { this.status = -1; } onready.call( this, mockhandler.istimeout ? 'timeout' : undefined ); } else if ( mockhandler.istimeout ) { // fix for 1.3.2 timeout to keep success from firing. this.status = -1; } }; // we have an executable function, call it to give // the mock handler a chance to update it's data if ( $.isfunction(mockhandler.response) ) { // wait for it to finish if ( mockhandler.response.length === 2 ) { mockhandler.response(origsettings, function () { finishrequest.call(that); }); return; } else { mockhandler.response(origsettings); } } finishrequest.call(that); }).apply(that); }; })(this); if ( mockhandler.proxy ) { logger.info( mockhandler, ['retrieving proxy file: ' + mockhandler.proxy, mockhandler] ); // we're proxying this request and loading in an external file instead _ajax({ global: false, url: mockhandler.proxy, type: mockhandler.proxytype, data: mockhandler.data, async: requestsettings.async, datatype: requestsettings.datatype === 'script' ? 'text/plain' : requestsettings.datatype, complete: function(xhr) { // fix for bug #105 // jquery will convert the text to xml for us, and if we use the actual responsexml here // then some other things don't happen, resulting in no data given to the 'success' cb mockhandler.responsexml = mockhandler.responsetext = xhr.responsetext; // don't override the handler status/statustext if it's specified by the config if (isdefaultsetting(mockhandler, 'status')) { mockhandler.status = xhr.status; } if (isdefaultsetting(mockhandler, 'statustext')) { mockhandler.statustext = xhr.statustext; } if ( requestsettings.async === false ) { // todo: blocking delay process(); } else { this.responsetimer = settimeout(process, parseresponsetimeopt(mockhandler.responsetime)); } } }); } else { // type === 'post' || 'get' || 'delete' if ( requestsettings.async === false ) { // todo: blocking delay process(); } else { this.responsetimer = settimeout(process, parseresponsetimeopt(mockhandler.responsetime)); } } } // construct a mocked xhr object function xhr(mockhandler, requestsettings, origsettings, orighandler) { logger.debug( mockhandler, ['creating new mock xhr object', mockhandler, requestsettings, origsettings, orighandler] ); // extend with our default mockjax settings mockhandler = $.extend(true, {}, $.mockjaxsettings, mockhandler); if (typeof mockhandler.headers === 'undefined') { mockhandler.headers = {}; } if (typeof requestsettings.headers === 'undefined') { requestsettings.headers = {}; } if ( mockhandler.contenttype ) { mockhandler.headers['content-type'] = mockhandler.contenttype; } return { status: mockhandler.status, statustext: mockhandler.statustext, readystate: 1, open: function() { }, send: function() { orighandler.fired = true; _xhrsend.call(this, mockhandler, requestsettings, origsettings); }, abort: function() { cleartimeout(this.responsetimer); }, setrequestheader: function(header, value) { requestsettings.headers[header] = value; }, getresponseheader: function(header) { // 'last-modified', 'etag', 'content-type' are all checked by jquery if ( mockhandler.headers && mockhandler.headers[header] ) { // return arbitrary headers return mockhandler.headers[header]; } else if ( header.tolowercase() === 'last-modified' ) { return mockhandler.lastmodified || (new date()).tostring(); } else if ( header.tolowercase() === 'etag' ) { return mockhandler.etag || ''; } else if ( header.tolowercase() === 'content-type' ) { return mockhandler.contenttype || 'text/plain'; } }, getallresponseheaders: function() { var headers = ''; // since jquery 1.9 responsetext type has to match contenttype if (mockhandler.contenttype) { mockhandler.headers['content-type'] = mockhandler.contenttype; } $.each(mockhandler.headers, function(k, v) { headers += k + ': ' + v + '\n'; }); return headers; } }; } // process a jsonp mock request. function processjsonpmock( requestsettings, mockhandler, origsettings ) { // handle jsonp parameter callbacks, we need to replicate some of the jquery core here // because there isn't an easy hook for the cross domain script tag of jsonp processjsonpurl( requestsettings ); requestsettings.datatype = 'json'; if(requestsettings.data && callback_regex.test(requestsettings.data) || callback_regex.test(requestsettings.url)) { createjsonpcallback(requestsettings, mockhandler, origsettings); // we need to make sure // that a jsonp style response is executed properly var rurl = /^(\w+:)?\/\/([^\/?#]+)/, parts = rurl.exec( requestsettings.url ), remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host); requestsettings.datatype = 'script'; if(requestsettings.type.touppercase() === 'get' && remote ) { var newmockreturn = processjsonprequest( requestsettings, mockhandler, origsettings ); // check if we are supposed to return a deferred back to the mock call, or just // signal success if(newmockreturn) { return newmockreturn; } else { return true; } } } return null; } // append the required callback parameter to the end of the request url, for a jsonp request function processjsonpurl( requestsettings ) { if ( requestsettings.type.touppercase() === 'get' ) { if ( !callback_regex.test( requestsettings.url ) ) { requestsettings.url += (/\?/.test( requestsettings.url ) ? '&' : '?') + (requestsettings.jsonp || 'callback') + '=?'; } } else if ( !requestsettings.data || !callback_regex.test(requestsettings.data) ) { requestsettings.data = (requestsettings.data ? requestsettings.data + '&' : '') + (requestsettings.jsonp || 'callback') + '=?'; } } // process a jsonp request by evaluating the mocked response text function processjsonprequest( requestsettings, mockhandler, origsettings ) { logger.debug( mockhandler, ['performing jsonp request', mockhandler, requestsettings, origsettings] ); // synthesize the mock request for adding a script tag var callbackcontext = origsettings && origsettings.context || requestsettings, // if we are running under jquery 1.5+, return a deferred object newmock = ($.deferred) ? (new $.deferred()) : null; // if the response handler on the moock is a function, call it if ( mockhandler.response && $.isfunction(mockhandler.response) ) { mockhandler.response(origsettings); } else if ( typeof mockhandler.responsetext === 'object' ) { // evaluate the responsetext javascript in a global context $.globaleval( '(' + json.stringify( mockhandler.responsetext ) + ')'); } else if (mockhandler.proxy) { logger.info( mockhandler, ['performing jsonp proxy request: ' + mockhandler.proxy, mockhandler] ); // this handles the unique case where we have a remote url, but want to proxy the jsonp // response to another file (not the same url as the mock matching) _ajax({ global: false, url: mockhandler.proxy, type: mockhandler.proxytype, data: mockhandler.data, datatype: requestsettings.datatype === 'script' ? 'text/plain' : requestsettings.datatype, complete: function(xhr) { $.globaleval( '(' + xhr.responsetext + ')'); completejsonpcall( requestsettings, mockhandler, callbackcontext, newmock ); } }); return newmock; } else { $.globaleval( '(' + ((typeof mockhandler.responsetext === 'string') ? ('"' + mockhandler.responsetext + '"') : mockhandler.responsetext) + ')'); } completejsonpcall( requestsettings, mockhandler, callbackcontext, newmock ); return newmock; } function completejsonpcall( requestsettings, mockhandler, callbackcontext, newmock ) { var json; // successful response settimeout(function() { jsonpsuccess( requestsettings, callbackcontext, mockhandler ); jsonpcomplete( requestsettings, callbackcontext ); if ( newmock ) { try { json = $.parsejson( mockhandler.responsetext ); } catch (err) { /* just checking... */ } newmock.resolvewith( callbackcontext, [json || mockhandler.responsetext] ); logger.log( mockhandler, ['jsonp mock call complete', mockhandler, newmock] ); } }, parseresponsetimeopt( mockhandler.responsetime )); } // create the required jsonp callback function for the request function createjsonpcallback( requestsettings, mockhandler, origsettings ) { var callbackcontext = origsettings && origsettings.context || requestsettings; var jsonp = (typeof requestsettings.jsonpcallback === 'string' && requestsettings.jsonpcallback) || ('jsonp' + jsc++); // replace the =? sequence both in the query string and the data if ( requestsettings.data ) { requestsettings.data = (requestsettings.data + '').replace(callback_regex, '=' + jsonp + '$1'); } requestsettings.url = requestsettings.url.replace(callback_regex, '=' + jsonp + '$1'); // handle jsonp-style loading window[ jsonp ] = window[ jsonp ] || function() { jsonpsuccess( requestsettings, callbackcontext, mockhandler ); jsonpcomplete( requestsettings, callbackcontext ); // garbage collect window[ jsonp ] = undefined; try { delete window[ jsonp ]; } catch(e) {} }; requestsettings.jsonpcallback = jsonp; } // the jsonp request was successful function jsonpsuccess(requestsettings, callbackcontext, mockhandler) { // if a local callback was specified, fire it and pass it the data if ( requestsettings.success ) { requestsettings.success.call( callbackcontext, mockhandler.responsetext || '', 'success', {} ); } // fire the global callback if ( requestsettings.global ) { (requestsettings.context ? $(requestsettings.context) : $.event).trigger('ajaxsuccess', [{}, requestsettings]); } } // the jsonp request was completed function jsonpcomplete(requestsettings, callbackcontext) { if ( requestsettings.complete ) { requestsettings.complete.call( callbackcontext, { statustext: 'success', status: 200 } , 'success' ); } // the request was completed if ( requestsettings.global ) { (requestsettings.context ? $(requestsettings.context) : $.event).trigger('ajaxcomplete', [{}, requestsettings]); } // handle the global ajax counter if ( requestsettings.global && ! --$.active ) { $.event.trigger( 'ajaxstop' ); } } // the core $.ajax replacement. function handleajax( url, origsettings ) { var mockrequest, requestsettings, mockhandler, overridecallback; logger.debug( null, ['ajax call intercepted', url, origsettings] ); // if url is an object, simulate pre-1.5 signature if ( typeof url === 'object' ) { origsettings = url; url = undefined; } else { // work around to support 1.5 signature origsettings = origsettings || {}; origsettings.url = url || origsettings.url; } // extend the original settings for the request requestsettings = $.ajaxsetup({}, origsettings); requestsettings.type = requestsettings.method = requestsettings.method || requestsettings.type; // generic function to override callback methods for use with // callback options (onaftersuccess, onaftererror, onaftercomplete) overridecallback = function(action, mockhandler) { var orighandler = origsettings[action.tolowercase()]; return function() { if ( $.isfunction(orighandler) ) { orighandler.apply(this, [].slice.call(arguments)); } mockhandler['onafter' + action](); }; }; // iterate over our mock handlers (in registration order) until we find // one that is willing to intercept the request for(var k = 0; k < mockhandlers.length; k++) { if ( !mockhandlers[k] ) { continue; } mockhandler = getmockforrequest( mockhandlers[k], requestsettings ); if(!mockhandler) { logger.debug( mockhandlers[k], ['mock does not match request', url, requestsettings] ); // no valid mock found for this request continue; } if ($.mockjaxsettings.retainajaxcalls) { mockedajaxcalls.push(requestsettings); } // if logging is enabled, log the mock to the console logger.info( mockhandler, [ 'mock ' + requestsettings.type.touppercase() + ': ' + requestsettings.url, $.ajaxsetup({}, requestsettings) ] ); if ( requestsettings.datatype && requestsettings.datatype.touppercase() === 'jsonp' ) { if ((mockrequest = processjsonpmock( requestsettings, mockhandler, origsettings ))) { // this mock will handle the jsonp request return mockrequest; } } // we are mocking, so there will be no cross domain request, however, jquery // aggressively pursues this if the domains don't match, so we need to // explicitly disallow it. (see #136) origsettings.crossdomain = false; // removed to fix #54 - keep the mocking data object intact //mockhandler.data = requestsettings.data; mockhandler.cache = requestsettings.cache; mockhandler.timeout = requestsettings.timeout; mockhandler.global = requestsettings.global; // in the case of a timeout, we just need to ensure // an actual jquery timeout (that is, our reponse won't) // return faster than the timeout setting. if ( mockhandler.istimeout ) { if ( mockhandler.responsetime > 1 ) { origsettings.timeout = mockhandler.responsetime - 1; } else { mockhandler.responsetime = 2; origsettings.timeout = 1; } } // set up onafter[x] callback functions if ( $.isfunction( mockhandler.onaftersuccess ) ) { origsettings.success = overridecallback('success', mockhandler); } if ( $.isfunction( mockhandler.onaftererror ) ) { origsettings.error = overridecallback('error', mockhandler); } if ( $.isfunction( mockhandler.onaftercomplete ) ) { origsettings.complete = overridecallback('complete', mockhandler); } copyurlparameters(mockhandler, origsettings); /* jshint loopfunc:true */ (function(mockhandler, requestsettings, origsettings, orighandler) { mockrequest = _ajax.call($, $.extend(true, {}, origsettings, { // mock the xhr object xhr: function() { return xhr( mockhandler, requestsettings, origsettings, orighandler ); } })); })(mockhandler, requestsettings, origsettings, mockhandlers[k]); /* jshint loopfunc:false */ return mockrequest; } // we don't have a mock request logger.log( null, ['no mock matched to request', url, origsettings] ); if ($.mockjaxsettings.retainajaxcalls) { unmockedajaxcalls.push(origsettings); } if($.mockjaxsettings.throwunmocked === true) { throw new error('ajax not mocked: ' + origsettings.url); } else { // trigger a normal request return _ajax.apply($, [origsettings]); } } /** * copies url parameter values if they were captured by a regular expression * @param {object} mockhandler * @param {object} origsettings */ function copyurlparameters(mockhandler, origsettings) { //parameters aren't captured if the url isn't a regexp if (!(mockhandler.url instanceof regexp)) { return; } //if no url params were defined on the handler, don't attempt a capture if (!mockhandler.hasownproperty('urlparams')) { return; } var captures = mockhandler.url.exec(origsettings.url); //the whole regexp match is always the first value in the capture results if (captures.length === 1) { return; } captures.shift(); //use handler params as keys and capture resuts as values var i = 0, captureslength = captures.length, paramslength = mockhandler.urlparams.length, //in case the number of params specified is less than actual captures maxiterations = math.min(captureslength, paramslength), paramvalues = {}; for (i; i < maxiterations; i++) { var key = mockhandler.urlparams[i]; paramvalues[key] = captures[i]; } origsettings.urlparams = paramvalues; } /** * clears handlers that mock given url * @param url * @returns {array} */ function clearbyurl(url) { var i, len, handler, results = [], match=url instanceof regexp ? function(testurl) { return url.test(testurl); } : function(testurl) { return url === testurl; }; for (i=0, len=mockhandlers.length; i