jquery.mockjax-2.2.1.js 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003
  1. /*! jQuery Mockjax
  2. * A Plugin providing simple and flexible mocking of ajax requests and responses
  3. *
  4. * Version: 2.2.1
  5. * Home: https://github.com/jakerella/jquery-mockjax
  6. * Copyright (c) 2016 Jordan Kasper, formerly appendTo;
  7. * NOTE: This repository was taken over by Jordan Kasper (@jakerella) October, 2014
  8. *
  9. * Dual licensed under the MIT or GPL licenses.
  10. * http://opensource.org/licenses/MIT OR http://www.gnu.org/licenses/gpl-2.0.html
  11. */
  12. (function(root, factory) {
  13. 'use strict';
  14. // AMDJS module definition
  15. if ( typeof define === 'function' && define.amd && define.amd.jQuery ) {
  16. define(['jquery'], function($) {
  17. return factory($, root);
  18. });
  19. // CommonJS module definition
  20. } else if ( typeof exports === 'object') {
  21. // NOTE: To use Mockjax as a Node module you MUST provide the factory with
  22. // a valid version of jQuery and a window object (the global scope):
  23. // var mockjax = require('jquery.mockjax')(jQuery, window);
  24. module.exports = factory;
  25. // Global jQuery in web browsers
  26. } else {
  27. return factory(root.jQuery || root.$, root);
  28. }
  29. }(this, function($, window) {
  30. 'use strict';
  31. var _ajax = $.ajax,
  32. mockHandlers = [],
  33. mockedAjaxCalls = [],
  34. unmockedAjaxCalls = [],
  35. CALLBACK_REGEX = /=\?(&|$)/,
  36. jsc = (new Date()).getTime(),
  37. DEFAULT_RESPONSE_TIME = 500;
  38. // Parse the given XML string.
  39. function parseXML(xml) {
  40. if ( window.DOMParser === undefined && window.ActiveXObject ) {
  41. window.DOMParser = function() { };
  42. DOMParser.prototype.parseFromString = function( xmlString ) {
  43. var doc = new ActiveXObject('Microsoft.XMLDOM');
  44. doc.async = 'false';
  45. doc.loadXML( xmlString );
  46. return doc;
  47. };
  48. }
  49. try {
  50. var xmlDoc = ( new DOMParser() ).parseFromString( xml, 'text/xml' );
  51. if ( $.isXMLDoc( xmlDoc ) ) {
  52. var err = $('parsererror', xmlDoc);
  53. if ( err.length === 1 ) {
  54. throw new Error('Error: ' + $(xmlDoc).text() );
  55. }
  56. } else {
  57. throw new Error('Unable to parse XML');
  58. }
  59. return xmlDoc;
  60. } catch( e ) {
  61. var msg = ( e.name === undefined ? e : e.name + ': ' + e.message );
  62. $(document).trigger('xmlParseError', [ msg ]);
  63. return undefined;
  64. }
  65. }
  66. // Check if the data field on the mock handler and the request match. This
  67. // can be used to restrict a mock handler to being used only when a certain
  68. // set of data is passed to it.
  69. function isMockDataEqual( mock, live ) {
  70. logger.debug( mock, ['Checking mock data against request data', mock, live] );
  71. var identical = true;
  72. if ( $.isFunction(mock) ) {
  73. return !!mock(live);
  74. }
  75. // Test for situations where the data is a querystring (not an object)
  76. if (typeof live === 'string') {
  77. // Querystring may be a regex
  78. if ($.isFunction( mock.test )) {
  79. return mock.test(live);
  80. } else if (typeof mock === 'object') {
  81. live = getQueryParams(live);
  82. } else {
  83. return mock === live;
  84. }
  85. }
  86. $.each(mock, function(k) {
  87. if ( live[k] === undefined ) {
  88. identical = false;
  89. return identical;
  90. } else {
  91. if ( typeof live[k] === 'object' && live[k] !== null ) {
  92. if ( identical && $.isArray( live[k] ) ) {
  93. identical = $.isArray( mock[k] ) && live[k].length === mock[k].length;
  94. }
  95. identical = identical && isMockDataEqual(mock[k], live[k]);
  96. } else {
  97. if ( mock[k] && $.isFunction( mock[k].test ) ) {
  98. identical = identical && mock[k].test(live[k]);
  99. } else {
  100. identical = identical && ( mock[k] === live[k] );
  101. }
  102. }
  103. }
  104. });
  105. return identical;
  106. }
  107. function getQueryParams(queryString) {
  108. var i, l, param, tmp,
  109. paramsObj = {},
  110. params = String(queryString).split(/&/);
  111. for (i=0, l=params.length; i<l; ++i) {
  112. param = params[i];
  113. try {
  114. param = decodeURIComponent(param.replace(/\+/g, ' '));
  115. param = param.split(/=/);
  116. } catch(e) {
  117. // Can't parse this one, so let it go?
  118. continue;
  119. }
  120. if (paramsObj[param[0]]) {
  121. // this is an array query param (more than one entry in query)
  122. if (!paramsObj[param[0]].splice) {
  123. // if not already an array, make it one
  124. tmp = paramsObj[param[0]];
  125. paramsObj[param[0]] = [];
  126. paramsObj[param[0]].push(tmp);
  127. }
  128. paramsObj[param[0]].push(param[1]);
  129. } else {
  130. paramsObj[param[0]] = param[1];
  131. }
  132. }
  133. logger.debug( null, ['Getting query params from string', queryString, paramsObj] );
  134. return paramsObj;
  135. }
  136. // See if a mock handler property matches the default settings
  137. function isDefaultSetting(handler, property) {
  138. return handler[property] === $.mockjaxSettings[property];
  139. }
  140. // Check the given handler should mock the given request
  141. function getMockForRequest( handler, requestSettings ) {
  142. // If the mock was registered with a function, let the function decide if we
  143. // want to mock this request
  144. if ( $.isFunction(handler) ) {
  145. return handler( requestSettings );
  146. }
  147. // Inspect the URL of the request and check if the mock handler's url
  148. // matches the url for this ajax request
  149. if ( $.isFunction(handler.url.test) ) {
  150. // The user provided a regex for the url, test it
  151. if ( !handler.url.test( requestSettings.url ) ) {
  152. return null;
  153. }
  154. } else {
  155. // Apply namespace prefix to the mock handler's url.
  156. var namespace = handler.namespace || $.mockjaxSettings.namespace;
  157. if (!!namespace) {
  158. var namespacedUrl = [namespace, handler.url].join('/');
  159. namespacedUrl = namespacedUrl.replace(/(\/+)/g, '/');
  160. handler.url = namespacedUrl;
  161. }
  162. // Look for a simple wildcard '*' or a direct URL match
  163. var star = handler.url.indexOf('*');
  164. if (handler.url !== requestSettings.url && star === -1 ||
  165. !new RegExp(handler.url.replace(/[-[\]{}()+?.,\\^$|#\s]/g, '\\$&').replace(/\*/g, '.+')).test(requestSettings.url)) {
  166. return null;
  167. }
  168. }
  169. // Inspect the request headers submitted
  170. if ( handler.requestHeaders ) {
  171. //No expectation for headers, do not mock this request
  172. if (requestSettings.headers === undefined) {
  173. return null;
  174. } else {
  175. var headersMismatch = false;
  176. $.each(handler.requestHeaders, function(key, value) {
  177. var v = requestSettings.headers[key];
  178. if(v !== value) {
  179. headersMismatch = true;
  180. return false;
  181. }
  182. });
  183. //Headers do not match, do not mock this request
  184. if (headersMismatch) {
  185. return null;
  186. }
  187. }
  188. }
  189. // Inspect the data submitted in the request (either POST body or GET query string)
  190. if ( handler.data ) {
  191. if ( !requestSettings.data || !isMockDataEqual(handler.data, requestSettings.data) ) {
  192. // They're not identical, do not mock this request
  193. return null;
  194. }
  195. }
  196. // Inspect the request type
  197. if ( handler && handler.type &&
  198. handler.type.toLowerCase() !== requestSettings.type.toLowerCase() ) {
  199. // The request type doesn't match (GET vs. POST)
  200. return null;
  201. }
  202. return handler;
  203. }
  204. function isPosNum(value) {
  205. return typeof value === 'number' && value >= 0;
  206. }
  207. function parseResponseTimeOpt(responseTime) {
  208. if ($.isArray(responseTime) && responseTime.length === 2) {
  209. var min = responseTime[0];
  210. var max = responseTime[1];
  211. if(isPosNum(min) && isPosNum(max)) {
  212. return Math.floor(Math.random() * (max - min)) + min;
  213. }
  214. } else if(isPosNum(responseTime)) {
  215. return responseTime;
  216. }
  217. return DEFAULT_RESPONSE_TIME;
  218. }
  219. // Process the xhr objects send operation
  220. function _xhrSend(mockHandler, requestSettings, origSettings) {
  221. logger.debug( mockHandler, ['Sending fake XHR request', mockHandler, requestSettings, origSettings] );
  222. // This is a substitute for < 1.4 which lacks $.proxy
  223. var process = (function(that) {
  224. return function() {
  225. return (function() {
  226. // The request has returned
  227. this.status = mockHandler.status;
  228. this.statusText = mockHandler.statusText;
  229. this.readyState = 1;
  230. var finishRequest = function () {
  231. this.readyState = 4;
  232. var onReady;
  233. // Copy over our mock to our xhr object before passing control back to
  234. // jQuery's onreadystatechange callback
  235. if ( requestSettings.dataType === 'json' && ( typeof mockHandler.responseText === 'object' ) ) {
  236. this.responseText = JSON.stringify(mockHandler.responseText);
  237. } else if ( requestSettings.dataType === 'xml' ) {
  238. if ( typeof mockHandler.responseXML === 'string' ) {
  239. this.responseXML = parseXML(mockHandler.responseXML);
  240. //in jQuery 1.9.1+, responseXML is processed differently and relies on responseText
  241. this.responseText = mockHandler.responseXML;
  242. } else {
  243. this.responseXML = mockHandler.responseXML;
  244. }
  245. } else if (typeof mockHandler.responseText === 'object' && mockHandler.responseText !== null) {
  246. // since jQuery 1.9 responseText type has to match contentType
  247. mockHandler.contentType = 'application/json';
  248. this.responseText = JSON.stringify(mockHandler.responseText);
  249. } else {
  250. this.responseText = mockHandler.responseText;
  251. }
  252. if( typeof mockHandler.status === 'number' || typeof mockHandler.status === 'string' ) {
  253. this.status = mockHandler.status;
  254. }
  255. if( typeof mockHandler.statusText === 'string') {
  256. this.statusText = mockHandler.statusText;
  257. }
  258. // jQuery 2.0 renamed onreadystatechange to onload
  259. onReady = this.onload || this.onreadystatechange;
  260. // jQuery < 1.4 doesn't have onreadystate change for xhr
  261. if ( $.isFunction( onReady ) ) {
  262. if( mockHandler.isTimeout) {
  263. this.status = -1;
  264. }
  265. onReady.call( this, mockHandler.isTimeout ? 'timeout' : undefined );
  266. } else if ( mockHandler.isTimeout ) {
  267. // Fix for 1.3.2 timeout to keep success from firing.
  268. this.status = -1;
  269. }
  270. };
  271. // We have an executable function, call it to give
  272. // the mock handler a chance to update it's data
  273. if ( $.isFunction(mockHandler.response) ) {
  274. // Wait for it to finish
  275. if ( mockHandler.response.length === 2 ) {
  276. mockHandler.response(origSettings, function () {
  277. finishRequest.call(that);
  278. });
  279. return;
  280. } else {
  281. mockHandler.response(origSettings);
  282. }
  283. }
  284. finishRequest.call(that);
  285. }).apply(that);
  286. };
  287. })(this);
  288. if ( mockHandler.proxy ) {
  289. logger.info( mockHandler, ['Retrieving proxy file: ' + mockHandler.proxy, mockHandler] );
  290. // We're proxying this request and loading in an external file instead
  291. _ajax({
  292. global: false,
  293. url: mockHandler.proxy,
  294. type: mockHandler.proxyType,
  295. data: mockHandler.data,
  296. async: requestSettings.async,
  297. dataType: requestSettings.dataType === 'script' ? 'text/plain' : requestSettings.dataType,
  298. complete: function(xhr) {
  299. // Fix for bug #105
  300. // jQuery will convert the text to XML for us, and if we use the actual responseXML here
  301. // then some other things don't happen, resulting in no data given to the 'success' cb
  302. mockHandler.responseXML = mockHandler.responseText = xhr.responseText;
  303. // Don't override the handler status/statusText if it's specified by the config
  304. if (isDefaultSetting(mockHandler, 'status')) {
  305. mockHandler.status = xhr.status;
  306. }
  307. if (isDefaultSetting(mockHandler, 'statusText')) {
  308. mockHandler.statusText = xhr.statusText;
  309. }
  310. if ( requestSettings.async === false ) {
  311. // TODO: Blocking delay
  312. process();
  313. } else {
  314. this.responseTimer = setTimeout(process, parseResponseTimeOpt(mockHandler.responseTime));
  315. }
  316. }
  317. });
  318. } else {
  319. // type === 'POST' || 'GET' || 'DELETE'
  320. if ( requestSettings.async === false ) {
  321. // TODO: Blocking delay
  322. process();
  323. } else {
  324. this.responseTimer = setTimeout(process, parseResponseTimeOpt(mockHandler.responseTime));
  325. }
  326. }
  327. }
  328. // Construct a mocked XHR Object
  329. function xhr(mockHandler, requestSettings, origSettings, origHandler) {
  330. logger.debug( mockHandler, ['Creating new mock XHR object', mockHandler, requestSettings, origSettings, origHandler] );
  331. // Extend with our default mockjax settings
  332. mockHandler = $.extend(true, {}, $.mockjaxSettings, mockHandler);
  333. if (typeof mockHandler.headers === 'undefined') {
  334. mockHandler.headers = {};
  335. }
  336. if (typeof requestSettings.headers === 'undefined') {
  337. requestSettings.headers = {};
  338. }
  339. if ( mockHandler.contentType ) {
  340. mockHandler.headers['content-type'] = mockHandler.contentType;
  341. }
  342. return {
  343. status: mockHandler.status,
  344. statusText: mockHandler.statusText,
  345. readyState: 1,
  346. open: function() { },
  347. send: function() {
  348. origHandler.fired = true;
  349. _xhrSend.call(this, mockHandler, requestSettings, origSettings);
  350. },
  351. abort: function() {
  352. clearTimeout(this.responseTimer);
  353. },
  354. setRequestHeader: function(header, value) {
  355. requestSettings.headers[header] = value;
  356. },
  357. getResponseHeader: function(header) {
  358. // 'Last-modified', 'Etag', 'content-type' are all checked by jQuery
  359. if ( mockHandler.headers && mockHandler.headers[header] ) {
  360. // Return arbitrary headers
  361. return mockHandler.headers[header];
  362. } else if ( header.toLowerCase() === 'last-modified' ) {
  363. return mockHandler.lastModified || (new Date()).toString();
  364. } else if ( header.toLowerCase() === 'etag' ) {
  365. return mockHandler.etag || '';
  366. } else if ( header.toLowerCase() === 'content-type' ) {
  367. return mockHandler.contentType || 'text/plain';
  368. }
  369. },
  370. getAllResponseHeaders: function() {
  371. var headers = '';
  372. // since jQuery 1.9 responseText type has to match contentType
  373. if (mockHandler.contentType) {
  374. mockHandler.headers['Content-Type'] = mockHandler.contentType;
  375. }
  376. $.each(mockHandler.headers, function(k, v) {
  377. headers += k + ': ' + v + '\n';
  378. });
  379. return headers;
  380. }
  381. };
  382. }
  383. // Process a JSONP mock request.
  384. function processJsonpMock( requestSettings, mockHandler, origSettings ) {
  385. // Handle JSONP Parameter Callbacks, we need to replicate some of the jQuery core here
  386. // because there isn't an easy hook for the cross domain script tag of jsonp
  387. processJsonpUrl( requestSettings );
  388. requestSettings.dataType = 'json';
  389. if(requestSettings.data && CALLBACK_REGEX.test(requestSettings.data) || CALLBACK_REGEX.test(requestSettings.url)) {
  390. createJsonpCallback(requestSettings, mockHandler, origSettings);
  391. // We need to make sure
  392. // that a JSONP style response is executed properly
  393. var rurl = /^(\w+:)?\/\/([^\/?#]+)/,
  394. parts = rurl.exec( requestSettings.url ),
  395. remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host);
  396. requestSettings.dataType = 'script';
  397. if(requestSettings.type.toUpperCase() === 'GET' && remote ) {
  398. var newMockReturn = processJsonpRequest( requestSettings, mockHandler, origSettings );
  399. // Check if we are supposed to return a Deferred back to the mock call, or just
  400. // signal success
  401. if(newMockReturn) {
  402. return newMockReturn;
  403. } else {
  404. return true;
  405. }
  406. }
  407. }
  408. return null;
  409. }
  410. // Append the required callback parameter to the end of the request URL, for a JSONP request
  411. function processJsonpUrl( requestSettings ) {
  412. if ( requestSettings.type.toUpperCase() === 'GET' ) {
  413. if ( !CALLBACK_REGEX.test( requestSettings.url ) ) {
  414. requestSettings.url += (/\?/.test( requestSettings.url ) ? '&' : '?') +
  415. (requestSettings.jsonp || 'callback') + '=?';
  416. }
  417. } else if ( !requestSettings.data || !CALLBACK_REGEX.test(requestSettings.data) ) {
  418. requestSettings.data = (requestSettings.data ? requestSettings.data + '&' : '') + (requestSettings.jsonp || 'callback') + '=?';
  419. }
  420. }
  421. // Process a JSONP request by evaluating the mocked response text
  422. function processJsonpRequest( requestSettings, mockHandler, origSettings ) {
  423. logger.debug( mockHandler, ['Performing JSONP request', mockHandler, requestSettings, origSettings] );
  424. // Synthesize the mock request for adding a script tag
  425. var callbackContext = origSettings && origSettings.context || requestSettings,
  426. // If we are running under jQuery 1.5+, return a deferred object
  427. newMock = ($.Deferred) ? (new $.Deferred()) : null;
  428. // If the response handler on the moock is a function, call it
  429. if ( mockHandler.response && $.isFunction(mockHandler.response) ) {
  430. mockHandler.response(origSettings);
  431. } else if ( typeof mockHandler.responseText === 'object' ) {
  432. // Evaluate the responseText javascript in a global context
  433. $.globalEval( '(' + JSON.stringify( mockHandler.responseText ) + ')');
  434. } else if (mockHandler.proxy) {
  435. logger.info( mockHandler, ['Performing JSONP proxy request: ' + mockHandler.proxy, mockHandler] );
  436. // This handles the unique case where we have a remote URL, but want to proxy the JSONP
  437. // response to another file (not the same URL as the mock matching)
  438. _ajax({
  439. global: false,
  440. url: mockHandler.proxy,
  441. type: mockHandler.proxyType,
  442. data: mockHandler.data,
  443. dataType: requestSettings.dataType === 'script' ? 'text/plain' : requestSettings.dataType,
  444. complete: function(xhr) {
  445. $.globalEval( '(' + xhr.responseText + ')');
  446. completeJsonpCall( requestSettings, mockHandler, callbackContext, newMock );
  447. }
  448. });
  449. return newMock;
  450. } else {
  451. $.globalEval( '(' +
  452. ((typeof mockHandler.responseText === 'string') ?
  453. ('"' + mockHandler.responseText + '"') : mockHandler.responseText) +
  454. ')');
  455. }
  456. completeJsonpCall( requestSettings, mockHandler, callbackContext, newMock );
  457. return newMock;
  458. }
  459. function completeJsonpCall( requestSettings, mockHandler, callbackContext, newMock ) {
  460. var json;
  461. // Successful response
  462. setTimeout(function() {
  463. jsonpSuccess( requestSettings, callbackContext, mockHandler );
  464. jsonpComplete( requestSettings, callbackContext );
  465. if ( newMock ) {
  466. try {
  467. json = $.parseJSON( mockHandler.responseText );
  468. } catch (err) { /* just checking... */ }
  469. newMock.resolveWith( callbackContext, [json || mockHandler.responseText] );
  470. logger.log( mockHandler, ['JSONP mock call complete', mockHandler, newMock] );
  471. }
  472. }, parseResponseTimeOpt( mockHandler.responseTime ));
  473. }
  474. // Create the required JSONP callback function for the request
  475. function createJsonpCallback( requestSettings, mockHandler, origSettings ) {
  476. var callbackContext = origSettings && origSettings.context || requestSettings;
  477. var jsonp = (typeof requestSettings.jsonpCallback === 'string' && requestSettings.jsonpCallback) || ('jsonp' + jsc++);
  478. // Replace the =? sequence both in the query string and the data
  479. if ( requestSettings.data ) {
  480. requestSettings.data = (requestSettings.data + '').replace(CALLBACK_REGEX, '=' + jsonp + '$1');
  481. }
  482. requestSettings.url = requestSettings.url.replace(CALLBACK_REGEX, '=' + jsonp + '$1');
  483. // Handle JSONP-style loading
  484. window[ jsonp ] = window[ jsonp ] || function() {
  485. jsonpSuccess( requestSettings, callbackContext, mockHandler );
  486. jsonpComplete( requestSettings, callbackContext );
  487. // Garbage collect
  488. window[ jsonp ] = undefined;
  489. try {
  490. delete window[ jsonp ];
  491. } catch(e) {}
  492. };
  493. requestSettings.jsonpCallback = jsonp;
  494. }
  495. // The JSONP request was successful
  496. function jsonpSuccess(requestSettings, callbackContext, mockHandler) {
  497. // If a local callback was specified, fire it and pass it the data
  498. if ( requestSettings.success ) {
  499. requestSettings.success.call( callbackContext, mockHandler.responseText || '', 'success', {} );
  500. }
  501. // Fire the global callback
  502. if ( requestSettings.global ) {
  503. (requestSettings.context ? $(requestSettings.context) : $.event).trigger('ajaxSuccess', [{}, requestSettings]);
  504. }
  505. }
  506. // The JSONP request was completed
  507. function jsonpComplete(requestSettings, callbackContext) {
  508. if ( requestSettings.complete ) {
  509. requestSettings.complete.call( callbackContext, {
  510. statusText: 'success',
  511. status: 200
  512. } , 'success' );
  513. }
  514. // The request was completed
  515. if ( requestSettings.global ) {
  516. (requestSettings.context ? $(requestSettings.context) : $.event).trigger('ajaxComplete', [{}, requestSettings]);
  517. }
  518. // Handle the global AJAX counter
  519. if ( requestSettings.global && ! --$.active ) {
  520. $.event.trigger( 'ajaxStop' );
  521. }
  522. }
  523. // The core $.ajax replacement.
  524. function handleAjax( url, origSettings ) {
  525. var mockRequest, requestSettings, mockHandler, overrideCallback;
  526. logger.debug( null, ['Ajax call intercepted', url, origSettings] );
  527. // If url is an object, simulate pre-1.5 signature
  528. if ( typeof url === 'object' ) {
  529. origSettings = url;
  530. url = undefined;
  531. } else {
  532. // work around to support 1.5 signature
  533. origSettings = origSettings || {};
  534. origSettings.url = url || origSettings.url;
  535. }
  536. // Extend the original settings for the request
  537. requestSettings = $.ajaxSetup({}, origSettings);
  538. requestSettings.type = requestSettings.method = requestSettings.method || requestSettings.type;
  539. // Generic function to override callback methods for use with
  540. // callback options (onAfterSuccess, onAfterError, onAfterComplete)
  541. overrideCallback = function(action, mockHandler) {
  542. var origHandler = origSettings[action.toLowerCase()];
  543. return function() {
  544. if ( $.isFunction(origHandler) ) {
  545. origHandler.apply(this, [].slice.call(arguments));
  546. }
  547. mockHandler['onAfter' + action]();
  548. };
  549. };
  550. // Iterate over our mock handlers (in registration order) until we find
  551. // one that is willing to intercept the request
  552. for(var k = 0; k < mockHandlers.length; k++) {
  553. if ( !mockHandlers[k] ) {
  554. continue;
  555. }
  556. mockHandler = getMockForRequest( mockHandlers[k], requestSettings );
  557. if(!mockHandler) {
  558. logger.debug( mockHandlers[k], ['Mock does not match request', url, requestSettings] );
  559. // No valid mock found for this request
  560. continue;
  561. }
  562. if ($.mockjaxSettings.retainAjaxCalls) {
  563. mockedAjaxCalls.push(requestSettings);
  564. }
  565. // If logging is enabled, log the mock to the console
  566. logger.info( mockHandler, [
  567. 'MOCK ' + requestSettings.type.toUpperCase() + ': ' + requestSettings.url,
  568. $.ajaxSetup({}, requestSettings)
  569. ] );
  570. if ( requestSettings.dataType && requestSettings.dataType.toUpperCase() === 'JSONP' ) {
  571. if ((mockRequest = processJsonpMock( requestSettings, mockHandler, origSettings ))) {
  572. // This mock will handle the JSONP request
  573. return mockRequest;
  574. }
  575. }
  576. // We are mocking, so there will be no cross domain request, however, jQuery
  577. // aggressively pursues this if the domains don't match, so we need to
  578. // explicitly disallow it. (See #136)
  579. origSettings.crossDomain = false;
  580. // Removed to fix #54 - keep the mocking data object intact
  581. //mockHandler.data = requestSettings.data;
  582. mockHandler.cache = requestSettings.cache;
  583. mockHandler.timeout = requestSettings.timeout;
  584. mockHandler.global = requestSettings.global;
  585. // In the case of a timeout, we just need to ensure
  586. // an actual jQuery timeout (That is, our reponse won't)
  587. // return faster than the timeout setting.
  588. if ( mockHandler.isTimeout ) {
  589. if ( mockHandler.responseTime > 1 ) {
  590. origSettings.timeout = mockHandler.responseTime - 1;
  591. } else {
  592. mockHandler.responseTime = 2;
  593. origSettings.timeout = 1;
  594. }
  595. }
  596. // Set up onAfter[X] callback functions
  597. if ( $.isFunction( mockHandler.onAfterSuccess ) ) {
  598. origSettings.success = overrideCallback('Success', mockHandler);
  599. }
  600. if ( $.isFunction( mockHandler.onAfterError ) ) {
  601. origSettings.error = overrideCallback('Error', mockHandler);
  602. }
  603. if ( $.isFunction( mockHandler.onAfterComplete ) ) {
  604. origSettings.complete = overrideCallback('Complete', mockHandler);
  605. }
  606. copyUrlParameters(mockHandler, origSettings);
  607. /* jshint loopfunc:true */
  608. (function(mockHandler, requestSettings, origSettings, origHandler) {
  609. mockRequest = _ajax.call($, $.extend(true, {}, origSettings, {
  610. // Mock the XHR object
  611. xhr: function() { return xhr( mockHandler, requestSettings, origSettings, origHandler ); }
  612. }));
  613. })(mockHandler, requestSettings, origSettings, mockHandlers[k]);
  614. /* jshint loopfunc:false */
  615. return mockRequest;
  616. }
  617. // We don't have a mock request
  618. logger.log( null, ['No mock matched to request', url, origSettings] );
  619. if ($.mockjaxSettings.retainAjaxCalls) {
  620. unmockedAjaxCalls.push(origSettings);
  621. }
  622. if($.mockjaxSettings.throwUnmocked === true) {
  623. throw new Error('AJAX not mocked: ' + origSettings.url);
  624. }
  625. else { // trigger a normal request
  626. return _ajax.apply($, [origSettings]);
  627. }
  628. }
  629. /**
  630. * Copies URL parameter values if they were captured by a regular expression
  631. * @param {Object} mockHandler
  632. * @param {Object} origSettings
  633. */
  634. function copyUrlParameters(mockHandler, origSettings) {
  635. //parameters aren't captured if the URL isn't a RegExp
  636. if (!(mockHandler.url instanceof RegExp)) {
  637. return;
  638. }
  639. //if no URL params were defined on the handler, don't attempt a capture
  640. if (!mockHandler.hasOwnProperty('urlParams')) {
  641. return;
  642. }
  643. var captures = mockHandler.url.exec(origSettings.url);
  644. //the whole RegExp match is always the first value in the capture results
  645. if (captures.length === 1) {
  646. return;
  647. }
  648. captures.shift();
  649. //use handler params as keys and capture resuts as values
  650. var i = 0,
  651. capturesLength = captures.length,
  652. paramsLength = mockHandler.urlParams.length,
  653. //in case the number of params specified is less than actual captures
  654. maxIterations = Math.min(capturesLength, paramsLength),
  655. paramValues = {};
  656. for (i; i < maxIterations; i++) {
  657. var key = mockHandler.urlParams[i];
  658. paramValues[key] = captures[i];
  659. }
  660. origSettings.urlParams = paramValues;
  661. }
  662. /**
  663. * Clears handlers that mock given url
  664. * @param url
  665. * @returns {Array}
  666. */
  667. function clearByUrl(url) {
  668. var i, len,
  669. handler,
  670. results = [],
  671. match=url instanceof RegExp ?
  672. function(testUrl) { return url.test(testUrl); } :
  673. function(testUrl) { return url === testUrl; };
  674. for (i=0, len=mockHandlers.length; i<len; i++) {
  675. handler = mockHandlers[i];
  676. if (!match(handler.url)) {
  677. results.push(handler);
  678. } else {
  679. logger.log( handler, [
  680. 'Clearing mock: ' + (handler && handler.url),
  681. handler
  682. ] );
  683. }
  684. }
  685. return results;
  686. }
  687. // Public
  688. $.extend({
  689. ajax: handleAjax
  690. });
  691. var logger = {
  692. _log: function logger( mockHandler, args, level ) {
  693. var loggerLevel = $.mockjaxSettings.logging;
  694. if (mockHandler && typeof mockHandler.logging !== 'undefined') {
  695. loggerLevel = mockHandler.logging;
  696. }
  697. level = ( level === 0 ) ? level : ( level || logLevels.LOG );
  698. args = (args.splice) ? args : [ args ];
  699. // Is logging turned off for this mock or mockjax as a whole?
  700. // Or is this log message above the desired log level?
  701. if ( loggerLevel === false || loggerLevel < level ) {
  702. return;
  703. }
  704. if ( $.mockjaxSettings.log ) {
  705. return $.mockjaxSettings.log( mockHandler, args[1] || args[0] );
  706. } else if ( $.mockjaxSettings.logger && $.mockjaxSettings.logger[$.mockjaxSettings.logLevelMethods[level]] ) {
  707. return $.mockjaxSettings.logger[$.mockjaxSettings.logLevelMethods[level]].apply( $.mockjaxSettings.logger, args );
  708. }
  709. },
  710. /**
  711. * Convenience method for logging a DEBUG level message
  712. * @param {Object} m The mock handler in question
  713. * @param {Array|String|Object} a The items to log
  714. * @return {?} Will return whatever the $.mockjaxSettings.logger method for this level would return (generally 'undefined')
  715. */
  716. debug: function(m,a) { return logger._log(m,a,logLevels.DEBUG); },
  717. /**
  718. * @see logger.debug
  719. */
  720. log: function(m,a) { return logger._log(m,a,logLevels.LOG); },
  721. /**
  722. * @see logger.debug
  723. */
  724. info: function(m,a) { return logger._log(m,a,logLevels.INFO); },
  725. /**
  726. * @see logger.debug
  727. */
  728. warn: function(m,a) { return logger._log(m,a,logLevels.WARN); },
  729. /**
  730. * @see logger.debug
  731. */
  732. error: function(m,a) { return logger._log(m,a,logLevels.ERROR); }
  733. };
  734. var logLevels = {
  735. DEBUG: 4,
  736. LOG: 3,
  737. INFO: 2,
  738. WARN: 1,
  739. ERROR: 0
  740. };
  741. /**
  742. * Default settings for mockjax. Some of these are used for defaults of
  743. * individual mock handlers, and some are for the library as a whole.
  744. * For individual mock handler settings, please see the README on the repo:
  745. * https://github.com/jakerella/jquery-mockjax#api-methods
  746. *
  747. * @type {Object}
  748. */
  749. $.mockjaxSettings = {
  750. log: null, // this is only here for historical purposes... use $.mockjaxSettings.logger
  751. logger: window.console,
  752. logging: 2,
  753. logLevelMethods: ['error', 'warn', 'info', 'log', 'debug'],
  754. namespace: null,
  755. status: 200,
  756. statusText: 'OK',
  757. responseTime: DEFAULT_RESPONSE_TIME,
  758. isTimeout: false,
  759. throwUnmocked: false,
  760. retainAjaxCalls: true,
  761. contentType: 'text/plain',
  762. response: '',
  763. responseText: '',
  764. responseXML: '',
  765. proxy: '',
  766. proxyType: 'GET',
  767. lastModified: null,
  768. etag: '',
  769. headers: {
  770. etag: 'IJF@H#@923uf8023hFO@I#H#',
  771. 'content-type' : 'text/plain'
  772. }
  773. };
  774. /**
  775. * Create a new mock Ajax handler. When a mock handler is matched during a
  776. * $.ajax() call this library will intercept that request and fake a response
  777. * using the data and methods in the mock. You can see all settings in the
  778. * README of the main repository:
  779. * https://github.com/jakerella/jquery-mockjax#api-methods
  780. *
  781. * @param {Object} settings The mock handelr settings: https://github.com/jakerella/jquery-mockjax#api-methods
  782. * @return {Number} The id (index) of the mock handler suitable for clearing (see $.mockjax.clear())
  783. */
  784. $.mockjax = function(settings) {
  785. // Multiple mocks.
  786. if ( $.isArray(settings) ) {
  787. return $.map(settings, function(s) {
  788. return $.mockjax(s);
  789. });
  790. }
  791. var i = mockHandlers.length;
  792. mockHandlers[i] = settings;
  793. logger.log( settings, ['Created new mock handler', settings] );
  794. return i;
  795. };
  796. $.mockjax._logger = logger;
  797. /**
  798. * Remove an Ajax mock from those held in memory. This will prevent any
  799. * future Ajax request mocking for matched requests.
  800. * NOTE: Clearing a mock will not prevent the resolution of in progress requests
  801. *
  802. * @param {Number|String|RegExp} i OPTIONAL The mock to clear. If not provided, all mocks are cleared,
  803. * if a number it is the index in the in-memory cache. If a string or
  804. * RegExp, find a mock that matches that URL and clear it.
  805. * @return {void}
  806. */
  807. $.mockjax.clear = function(i) {
  808. if ( typeof i === 'string' || i instanceof RegExp) {
  809. mockHandlers = clearByUrl(i);
  810. } else if ( i || i === 0 ) {
  811. logger.log( mockHandlers[i], [
  812. 'Clearing mock: ' + (mockHandlers[i] && mockHandlers[i].url),
  813. mockHandlers[i]
  814. ] );
  815. mockHandlers[i] = null;
  816. } else {
  817. logger.log( null, 'Clearing all mocks' );
  818. mockHandlers = [];
  819. }
  820. mockedAjaxCalls = [];
  821. unmockedAjaxCalls = [];
  822. };
  823. /**
  824. * By default all Ajax requests performed after loading Mockjax are recorded
  825. * so that we can see which requests were mocked and which were not. This
  826. * method allows the developer to clear those retained requests.
  827. *
  828. * @return {void}
  829. */
  830. $.mockjax.clearRetainedAjaxCalls = function() {
  831. mockedAjaxCalls = [];
  832. unmockedAjaxCalls = [];
  833. logger.debug( null, 'Cleared retained ajax calls' );
  834. };
  835. /**
  836. * Retrive the mock handler with the given id (index).
  837. *
  838. * @param {Number} i The id (index) to retrieve
  839. * @return {Object} The mock handler settings
  840. */
  841. $.mockjax.handler = function(i) {
  842. if ( arguments.length === 1 ) {
  843. return mockHandlers[i];
  844. }
  845. };
  846. /**
  847. * Retrieve all Ajax calls that have been mocked by this library during the
  848. * current session (in other words, only since you last loaded this file).
  849. *
  850. * @return {Array} The mocked Ajax calls (request settings)
  851. */
  852. $.mockjax.mockedAjaxCalls = function() {
  853. return mockedAjaxCalls;
  854. };
  855. /**
  856. * Return all mock handlers that have NOT been matched against Ajax requests
  857. *
  858. * @return {Array} The mock handlers
  859. */
  860. $.mockjax.unfiredHandlers = function() {
  861. var results = [];
  862. for (var i=0, len=mockHandlers.length; i<len; i++) {
  863. var handler = mockHandlers[i];
  864. if (handler !== null && !handler.fired) {
  865. results.push(handler);
  866. }
  867. }
  868. return results;
  869. };
  870. /**
  871. * Retrieve all Ajax calls that have NOT been mocked by this library during
  872. * the current session (in other words, only since you last loaded this file).
  873. *
  874. * @return {Array} The mocked Ajax calls (request settings)
  875. */
  876. $.mockjax.unmockedAjaxCalls = function() {
  877. return unmockedAjaxCalls;
  878. };
  879. return $.mockjax;
  880. }));