You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

521 lines
16 KiB

  1. /*!
  2. * MockJax - jQuery Plugin to Mock Ajax requests
  3. *
  4. * Version: 1.5.0pre
  5. * Released:
  6. * Home: http://github.com/appendto/jquery-mockjax
  7. * Author: Jonathan Sharp (http://jdsharp.com)
  8. * License: MIT,GPL
  9. *
  10. * Copyright (c) 2011 appendTo LLC.
  11. * Dual licensed under the MIT or GPL licenses.
  12. * http://appendto.com/open-source-licenses
  13. */
  14. (function($) {
  15. var _ajax = $.ajax,
  16. mockHandlers = [],
  17. CALLBACK_REGEX = /=\?(&|$)/,
  18. jsc = (new Date()).getTime();
  19. // Parse the given XML string.
  20. function parseXML(xml) {
  21. if ( window['DOMParser'] == undefined && window.ActiveXObject ) {
  22. DOMParser = function() { };
  23. DOMParser.prototype.parseFromString = function( xmlString ) {
  24. var doc = new ActiveXObject('Microsoft.XMLDOM');
  25. doc.async = 'false';
  26. doc.loadXML( xmlString );
  27. return doc;
  28. };
  29. }
  30. try {
  31. var xmlDoc = ( new DOMParser() ).parseFromString( xml, 'text/xml' );
  32. if ( $.isXMLDoc( xmlDoc ) ) {
  33. var err = $('parsererror', xmlDoc);
  34. if ( err.length == 1 ) {
  35. throw('Error: ' + $(xmlDoc).text() );
  36. }
  37. } else {
  38. throw('Unable to parse XML');
  39. }
  40. } catch( e ) {
  41. var msg = ( e.name == undefined ? e : e.name + ': ' + e.message );
  42. $(document).trigger('xmlParseError', [ msg ]);
  43. return undefined;
  44. }
  45. return xmlDoc;
  46. }
  47. // Trigger a jQuery event
  48. function trigger(s, type, args) {
  49. (s.context ? jQuery(s.context) : jQuery.event).trigger(type, args);
  50. }
  51. // Check if the data field on the mock handler and the request match. This
  52. // can be used to restrict a mock handler to being used only when a certain
  53. // set of data is passed to it.
  54. function isMockDataEqual( mock, live ) {
  55. var identical = false;
  56. // Test for situations where the data is a querystring (not an object)
  57. if (typeof live === 'string') {
  58. // Querystring may be a regex
  59. return $.isFunction( mock.test ) ? mock.test(live) : mock == live;
  60. }
  61. $.each(mock, function(k, v) {
  62. if ( live[k] === undefined ) {
  63. identical = false;
  64. return identical;
  65. } else {
  66. identical = true;
  67. if ( typeof live[k] == 'object' ) {
  68. return isMockDataEqual(mock[k], live[k]);
  69. } else {
  70. if ( $.isFunction( mock[k].test ) ) {
  71. identical = mock[k].test(live[k]);
  72. } else {
  73. identical = ( mock[k] == live[k] );
  74. }
  75. return identical;
  76. }
  77. }
  78. });
  79. return identical;
  80. }
  81. // Check the given handler should mock the given request
  82. function getMockForRequest( handler, requestSettings ) {
  83. // If the mock was registered with a function, let the function decide if we
  84. // want to mock this request
  85. if ( $.isFunction(handler) ) {
  86. return handler( requestSettings );
  87. }
  88. // Inspect the URL of the request and check if the mock handler's url
  89. // matches the url for this ajax request
  90. if ( $.isFunction(handler.url.test) ) {
  91. // The user provided a regex for the url, test it
  92. if ( !handler.url.test( requestSettings.url ) ) {
  93. return null;
  94. }
  95. } else {
  96. // Look for a simple wildcard '*' or a direct URL match
  97. var star = handler.url.indexOf('*');
  98. if (handler.url !== requestSettings.url && star === -1 ||
  99. !new RegExp(handler.url.replace(/[-[\]{}()+?.,\\^$|#\s]/g, "\\$&").replace('*', '.+')).test(requestSettings.url)) {
  100. return null;
  101. }
  102. }
  103. // Inspect the data submitted in the request (either POST body or GET query string)
  104. if ( handler.data && requestSettings.data ) {
  105. if ( !isMockDataEqual(handler.data, requestSettings.data) ) {
  106. // They're not identical, do not mock this request
  107. return null;
  108. }
  109. }
  110. // Inspect the request type
  111. if ( handler && handler.type &&
  112. handler.type.toLowerCase() != requestSettings.type.toLowerCase() ) {
  113. // The request type doesn't match (GET vs. POST)
  114. return null;
  115. }
  116. return handler;
  117. }
  118. // If logging is enabled, log the mock to the console
  119. function logMock( mockHandler, requestSettings ) {
  120. var c = $.extend({}, $.mockjaxSettings, mockHandler);
  121. if ( c.log && $.isFunction(c.log) ) {
  122. c.log('MOCK ' + requestSettings.type.toUpperCase() + ': ' + requestSettings.url, $.extend({}, requestSettings));
  123. }
  124. }
  125. // Process the xhr objects send operation
  126. function _xhrSend(mockHandler, requestSettings, origSettings) {
  127. // This is a substitute for < 1.4 which lacks $.proxy
  128. var process = (function(that) {
  129. return function() {
  130. return (function() {
  131. // The request has returned
  132. this.status = mockHandler.status;
  133. this.statusText = mockHandler.statusText;
  134. this.readyState = 4;
  135. // We have an executable function, call it to give
  136. // the mock handler a chance to update it's data
  137. if ( $.isFunction(mockHandler.response) ) {
  138. mockHandler.response(origSettings);
  139. }
  140. // Copy over our mock to our xhr object before passing control back to
  141. // jQuery's onreadystatechange callback
  142. if ( requestSettings.dataType == 'json' && ( typeof mockHandler.responseText == 'object' ) ) {
  143. this.responseText = JSON.stringify(mockHandler.responseText);
  144. } else if ( requestSettings.dataType == 'xml' ) {
  145. if ( typeof mockHandler.responseXML == 'string' ) {
  146. this.responseXML = parseXML(mockHandler.responseXML);
  147. } else {
  148. this.responseXML = mockHandler.responseXML;
  149. }
  150. } else {
  151. this.responseText = mockHandler.responseText;
  152. }
  153. if( typeof mockHandler.status == 'number' || typeof mockHandler.status == 'string' ) {
  154. this.status = mockHandler.status;
  155. }
  156. if( typeof mockHandler.statusText === "string") {
  157. this.statusText = mockHandler.statusText;
  158. }
  159. // jQuery < 1.4 doesn't have onreadystate change for xhr
  160. if ( $.isFunction(this.onreadystatechange) ) {
  161. if( mockHandler.isTimeout) {
  162. this.status = -1;
  163. }
  164. this.onreadystatechange( mockHandler.isTimeout ? 'timeout' : undefined );
  165. } else if ( mockHandler.isTimeout ) {
  166. // Fix for 1.3.2 timeout to keep success from firing.
  167. this.status = -1;
  168. }
  169. }).apply(that);
  170. };
  171. })(this);
  172. if ( mockHandler.proxy ) {
  173. // We're proxying this request and loading in an external file instead
  174. _ajax({
  175. global: false,
  176. url: mockHandler.proxy,
  177. type: mockHandler.proxyType,
  178. data: mockHandler.data,
  179. dataType: requestSettings.dataType === "script" ? "text/plain" : requestSettings.dataType,
  180. complete: function(xhr, txt) {
  181. mockHandler.responseXML = xhr.responseXML;
  182. mockHandler.responseText = xhr.responseText;
  183. mockHandler.status = xhr.status;
  184. mockHandler.statusText = xhr.statusText;
  185. this.responseTimer = setTimeout(process, mockHandler.responseTime || 0);
  186. }
  187. });
  188. } else {
  189. // type == 'POST' || 'GET' || 'DELETE'
  190. if ( requestSettings.async === false ) {
  191. // TODO: Blocking delay
  192. process();
  193. } else {
  194. this.responseTimer = setTimeout(process, mockHandler.responseTime || 50);
  195. }
  196. }
  197. }
  198. // Construct a mocked XHR Object
  199. function xhr(mockHandler, requestSettings, origSettings, origHandler) {
  200. // Extend with our default mockjax settings
  201. mockHandler = $.extend({}, $.mockjaxSettings, mockHandler);
  202. if (typeof mockHandler.headers === 'undefined') {
  203. mockHandler.headers = {};
  204. }
  205. if ( mockHandler.contentType ) {
  206. mockHandler.headers['content-type'] = mockHandler.contentType;
  207. }
  208. return {
  209. status: mockHandler.status,
  210. statusText: mockHandler.statusText,
  211. readyState: 1,
  212. open: function() { },
  213. send: function() {
  214. origHandler.fired = true;
  215. _xhrSend.call(this, mockHandler, requestSettings, origSettings);
  216. },
  217. abort: function() {
  218. clearTimeout(this.responseTimer);
  219. },
  220. setRequestHeader: function(header, value) {
  221. mockHandler.headers[header] = value;
  222. },
  223. getResponseHeader: function(header) {
  224. // 'Last-modified', 'Etag', 'content-type' are all checked by jQuery
  225. if ( mockHandler.headers && mockHandler.headers[header] ) {
  226. // Return arbitrary headers
  227. return mockHandler.headers[header];
  228. } else if ( header.toLowerCase() == 'last-modified' ) {
  229. return mockHandler.lastModified || (new Date()).toString();
  230. } else if ( header.toLowerCase() == 'etag' ) {
  231. return mockHandler.etag || '';
  232. } else if ( header.toLowerCase() == 'content-type' ) {
  233. return mockHandler.contentType || 'text/plain';
  234. }
  235. },
  236. getAllResponseHeaders: function() {
  237. var headers = '';
  238. $.each(mockHandler.headers, function(k, v) {
  239. headers += k + ': ' + v + "\n";
  240. });
  241. return headers;
  242. }
  243. };
  244. }
  245. // Process a JSONP mock request.
  246. function processJsonpMock( requestSettings, mockHandler, origSettings ) {
  247. // Handle JSONP Parameter Callbacks, we need to replicate some of the jQuery core here
  248. // because there isn't an easy hook for the cross domain script tag of jsonp
  249. processJsonpUrl( requestSettings );
  250. requestSettings.dataType = "json";
  251. if(requestSettings.data && CALLBACK_REGEX.test(requestSettings.data) || CALLBACK_REGEX.test(requestSettings.url)) {
  252. createJsonpCallback(requestSettings, mockHandler);
  253. // We need to make sure
  254. // that a JSONP style response is executed properly
  255. var rurl = /^(\w+:)?\/\/([^\/?#]+)/,
  256. parts = rurl.exec( requestSettings.url ),
  257. remote = parts && (parts[1] && parts[1] !== location.protocol || parts[2] !== location.host);
  258. requestSettings.dataType = "script";
  259. if(requestSettings.type.toUpperCase() === "GET" && remote ) {
  260. var newMockReturn = processJsonpRequest( requestSettings, mockHandler, origSettings );
  261. // Check if we are supposed to return a Deferred back to the mock call, or just
  262. // signal success
  263. if(newMockReturn) {
  264. return newMockReturn;
  265. } else {
  266. return true;
  267. }
  268. }
  269. }
  270. return null;
  271. }
  272. // Append the required callback parameter to the end of the request URL, for a JSONP request
  273. function processJsonpUrl( requestSettings ) {
  274. if ( requestSettings.type.toUpperCase() === "GET" ) {
  275. if ( !CALLBACK_REGEX.test( requestSettings.url ) ) {
  276. requestSettings.url += (/\?/.test( requestSettings.url ) ? "&" : "?") +
  277. (requestSettings.jsonp || "callback") + "=?";
  278. }
  279. } else if ( !requestSettings.data || !CALLBACK_REGEX.test(requestSettings.data) ) {
  280. requestSettings.data = (requestSettings.data ? requestSettings.data + "&" : "") + (requestSettings.jsonp || "callback") + "=?";
  281. }
  282. }
  283. // Process a JSONP request by evaluating the mocked response text
  284. function processJsonpRequest( requestSettings, mockHandler, origSettings ) {
  285. // Synthesize the mock request for adding a script tag
  286. var callbackContext = origSettings && origSettings.context || requestSettings,
  287. newMock = null;
  288. // If the response handler on the moock is a function, call it
  289. if ( mockHandler.response && $.isFunction(mockHandler.response) ) {
  290. mockHandler.response(origSettings);
  291. } else {
  292. // Evaluate the responseText javascript in a global context
  293. if( typeof mockHandler.responseText === 'object' ) {
  294. $.globalEval( '(' + JSON.stringify( mockHandler.responseText ) + ')');
  295. } else {
  296. $.globalEval( '(' + mockHandler.responseText + ')');
  297. }
  298. }
  299. // Successful response
  300. jsonpSuccess( requestSettings, mockHandler );
  301. jsonpComplete( requestSettings, mockHandler );
  302. // If we are running under jQuery 1.5+, return a deferred object
  303. if(jQuery.Deferred){
  304. newMock = new jQuery.Deferred();
  305. if(typeof mockHandler.responseText == "object"){
  306. newMock.resolve( mockHandler.responseText );
  307. }
  308. else{
  309. newMock.resolve( jQuery.parseJSON( mockHandler.responseText ) );
  310. }
  311. }
  312. return newMock;
  313. }
  314. // Create the required JSONP callback function for the request
  315. function createJsonpCallback( requestSettings, mockHandler ) {
  316. jsonp = requestSettings.jsonpCallback || ("jsonp" + jsc++);
  317. // Replace the =? sequence both in the query string and the data
  318. if ( requestSettings.data ) {
  319. requestSettings.data = (requestSettings.data + "").replace(CALLBACK_REGEX, "=" + jsonp + "$1");
  320. }
  321. requestSettings.url = requestSettings.url.replace(CALLBACK_REGEX, "=" + jsonp + "$1");
  322. // Handle JSONP-style loading
  323. window[ jsonp ] = window[ jsonp ] || function( tmp ) {
  324. data = tmp;
  325. jsonpSuccess( requestSettings, mockHandler );
  326. jsonpComplete( requestSettings, mockHandler );
  327. // Garbage collect
  328. window[ jsonp ] = undefined;
  329. try {
  330. delete window[ jsonp ];
  331. } catch(e) {}
  332. if ( head ) {
  333. head.removeChild( script );
  334. }
  335. };
  336. }
  337. // The JSONP request was successful
  338. function jsonpSuccess(requestSettings, mockHandler) {
  339. // If a local callback was specified, fire it and pass it the data
  340. if ( requestSettings.success ) {
  341. requestSettings.success.call( callbackContext, ( mockHandler.response ? mockHandler.response.toString() : mockHandler.responseText || ''), status, {} );
  342. }
  343. // Fire the global callback
  344. if ( requestSettings.global ) {
  345. trigger(requestSettings, "ajaxSuccess", [{}, requestSettings] );
  346. }
  347. }
  348. // The JSONP request was completed
  349. function jsonpComplete(requestSettings, mockHandler) {
  350. // Process result
  351. if ( requestSettings.complete ) {
  352. requestSettings.complete.call( callbackContext, {} , status );
  353. }
  354. // The request was completed
  355. if ( requestSettings.global ) {
  356. trigger( "ajaxComplete", [{}, requestSettings] );
  357. }
  358. // Handle the global AJAX counter
  359. if ( requestSettings.global && ! --jQuery.active ) {
  360. jQuery.event.trigger( "ajaxStop" );
  361. }
  362. }
  363. // The core $.ajax replacement.
  364. function handleAjax( url, origSettings ) {
  365. var mockRequest, requestSettings, mockHandler;
  366. // If url is an object, simulate pre-1.5 signature
  367. if ( typeof url === "object" ) {
  368. origSettings = url;
  369. url = undefined;
  370. } else {
  371. // work around to support 1.5 signature
  372. origSettings.url = url;
  373. }
  374. // Extend the original settings for the request
  375. requestSettings = jQuery.extend(true, {}, jQuery.ajaxSettings, origSettings);
  376. // Iterate over our mock handlers (in registration order) until we find
  377. // one that is willing to intercept the request
  378. for(var k = 0; k < mockHandlers.length; k++) {
  379. if ( !mockHandlers[k] ) {
  380. continue;
  381. }
  382. mockHandler = getMockForRequest( mockHandlers[k], requestSettings );
  383. if(!mockHandler) {
  384. // No valid mock found for this request
  385. continue;
  386. }
  387. // Handle console logging
  388. logMock( mockHandler, requestSettings );
  389. if ( requestSettings.dataType === "jsonp" ) {
  390. if ((mockRequest = processJsonpMock( requestSettings, mockHandler, origSettings ))) {
  391. // This mock will handle the JSONP request
  392. return mockRequest;
  393. }
  394. }
  395. // Removed to fix #54 - keep the mocking data object intact
  396. //mockHandler.data = requestSettings.data;
  397. mockHandler.cache = requestSettings.cache;
  398. mockHandler.timeout = requestSettings.timeout;
  399. mockHandler.global = requestSettings.global;
  400. (function(mockHandler, requestSettings, origSettings, origHandler) {
  401. mockRequest = _ajax.call($, $.extend(true, {}, origSettings, {
  402. // Mock the XHR object
  403. xhr: function() { return xhr( mockHandler, requestSettings, origSettings, origHandler ) }
  404. }));
  405. })(mockHandler, requestSettings, origSettings, mockHandlers[k]);
  406. return mockRequest;
  407. }
  408. // We don't have a mock request, trigger a normal request
  409. return _ajax.apply($, [origSettings]);
  410. }
  411. // Public
  412. $.extend({
  413. ajax: handleAjax
  414. });
  415. $.mockjaxSettings = {
  416. //url: null,
  417. //type: 'GET',
  418. log: function(msg) {
  419. window['console'] && window.console.log && window.console.log(msg);
  420. },
  421. status: 200,
  422. statusText: "OK",
  423. responseTime: 500,
  424. isTimeout: false,
  425. contentType: 'text/plain',
  426. response: '',
  427. responseText: '',
  428. responseXML: '',
  429. proxy: '',
  430. proxyType: 'GET',
  431. lastModified: null,
  432. etag: '',
  433. headers: {
  434. etag: 'IJF@H#@923uf8023hFO@I#H#',
  435. 'content-type' : 'text/plain'
  436. }
  437. };
  438. $.mockjax = function(settings) {
  439. var i = mockHandlers.length;
  440. mockHandlers[i] = settings;
  441. return i;
  442. };
  443. $.mockjaxClear = function(i) {
  444. if ( arguments.length == 1 ) {
  445. mockHandlers[i] = null;
  446. } else {
  447. mockHandlers = [];
  448. }
  449. };
  450. $.mockjax.handler = function(i) {
  451. if ( arguments.length == 1 ) {
  452. return mockHandlers[i];
  453. }
  454. };
  455. })(jQuery);