Created
January 17, 2012 09:16
-
-
Save Integralist/1625810 to your computer and use it in GitHub Desktop.
Sinon.js Fake XHR example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
define(['Utils/isIE', 'Utils/json'], function(isIE, JSON){ | |
// Note: wasn't able to require() in the json.js script for browsers that don't support it | |
// As it would load asynchronusly and the below callback function would have executed before the json.js was loaded | |
// I could have used when.js to handle this situation but seems a bit OTT. | |
// I could also of have had one if statement at the top of this module which checked for support and forked there but I didn't like the idea of having all my code in an else statement | |
// So decided to just load the script for all users (once it's minified and gzip'ed it shouldn't be much of a performance issue). | |
// Used by ajax method to store errors | |
var errors = []; | |
/** | |
* XMLHttpRequest abstraction. | |
* | |
* @return xhr { XMLHttpRequest|ActiveXObject } a new instance of either the native XMLHttpRequest object or the corresponding ActiveXObject | |
*/ | |
var __xhr = (function() { | |
// Create local variable which will cache the results of this function | |
var xhr; | |
return function() { | |
// Check if function has already cached the value | |
if (xhr) { | |
// Create a new XMLHttpRequest instance | |
return new xhr(); | |
} else { | |
// Check what XMLHttpRequest object is available and cache it | |
xhr = (!window.XMLHttpRequest) ? function() { | |
return new ActiveXObject( | |
// Internet Explorer 5 uses a different XMLHTTP object from Internet Explorer 6 | |
(isIE < 6) ? "Microsoft.XMLHTTP" : "MSXML2.XMLHTTP" | |
); | |
} : window.XMLHttpRequest; | |
// Return a new XMLHttpRequest instance | |
return new xhr(); | |
} | |
}; | |
}()); | |
/** | |
* A basic AJAX method. | |
* | |
* @param settings { Object } user configuration | |
* @return undefined { } no explicitly returned value | |
*/ | |
var ajax = function(settings) { | |
// JavaScript engine will 'hoist' variables so we'll be specific and declare them here | |
var xhr, url, requestDone, xhrTimeout, | |
// Load the config object with defaults, if no values were provided by the user | |
config = { | |
// The type of HTTP Request | |
method: settings.method || 'GET', | |
// The data to POST to the server | |
data: settings.data || '', | |
// The URL the request will be made to | |
url: settings.url || '', | |
// How long to wait before considering the request to be a timeout | |
timeout: settings.timeout || 5000, | |
// Functions to call when the request fails, succeeds, or completes (either fail or succeed) | |
onComplete: settings.onComplete || function(){}, | |
onError: settings.onError || function(){}, | |
onSuccess: settings.onSuccess || function(){}, | |
// The data type that'll be returned from the server | |
// the default is simply to determine what data was returned from the server and act accordingly. | |
dataType: settings.dataType || '' | |
}; | |
// Create new cross-browser XMLHttpRequest instance | |
xhr = __xhr(); | |
// Open the asynchronous request | |
xhr.open(config.method, config.url, true); | |
// Determine the success of the HTTP response | |
function httpSuccess(r) { | |
try { | |
// If no server status is provided, and we're actually | |
// requesting a local file, then it was successful | |
return !r.status && location.protocol == 'file:' || | |
// Any status in the 200 range is good | |
( r.status >= 200 && r.status < 300 ) || | |
// Successful if the document has not been modified | |
r.status == 304 || | |
// Safari returns an empty status if the file has not been modified | |
navigator.userAgent.indexOf('Safari') >= 0 && typeof r.status == 'undefined'; | |
} catch(e){ | |
// Throw a corresponding error | |
throw new Error("httpSuccess Error = " + e); | |
} | |
// If checking the status failed, then assume that the request failed too | |
return false; | |
} | |
// Extract the correct data from the HTTP response | |
function httpData(xhr, type) { | |
if (type === 'json') { | |
return JSON.parse(xhr.responseText); | |
//return eval('(' + xhr.responseText + ')'); | |
} | |
// | |
else if (type === 'html') { | |
return xhr.responseText; | |
} | |
// | |
else if (type === 'xml') { | |
return xhr.responseXML; | |
} | |
// Attempt to work out the content type | |
else { | |
// Get the content-type header | |
var contentType = xhr.getResponseHeader("content-type"), | |
data = !type && contentType && contentType.indexOf("xml") >= 0; // If no default type was provided, determine if some form of XML was returned from the server | |
// Get the XML Document object if XML was returned from the server, | |
// otherwise return the text contents returned by the server | |
data = (type == "xml" || data) ? xhr.responseXML : xhr.responseText; | |
// Return the response data (either an XML Document or a text string) | |
return data; | |
} | |
} | |
// Initalize a callback which will fire within the timeout range, also cancelling the request (if it has not already occurred) | |
xhrTimeout = window.setTimeout(function() { | |
requestDone = true; | |
config.onComplete(); | |
}, config.timeout); | |
// Watch for when the state of the document gets updated | |
xhr.onreadystatechange = function() { | |
// Wait until the data is fully loaded, and make sure that the request hasn't already timed out | |
if (xhr.readyState == 4 && !requestDone) { | |
// Check to see if the request was successful | |
if (httpSuccess(xhr)) { | |
// Execute the success callback | |
config.onSuccess(httpData(xhr, config.dataType)); | |
} | |
/** | |
* For some reason, in an example PHP script that returns JSON data, | |
* even though the request 'timed out' it still generated a readyState of 4. | |
* I believe this was because although the script used sleep() to delay the data returned, the fact it returned data after the timeout caused an error. | |
* So when the httpSuccess expression used in the above condition returns false we need to execute the onError handler. | |
*/ | |
else { | |
config.onError(xhr); | |
} | |
// Call the completion callback | |
config.onComplete(); | |
// Clean up after ourselves (+ help to avoid memory leaks) | |
clearTimeout(xhrTimeout); | |
xhr.onreadystatechange = null; | |
xhr = null; | |
} else if (requestDone && xhr.readyState != 4) { | |
// If the script timed out then keep a log of it so the developer can query this and handle any exceptions | |
errors.push(url + " { timed out } "); | |
// Bail out of the request immediately | |
xhr.onreadystatechange = null; | |
xhr = null; | |
} | |
}; | |
// Get if we should POST or GET... | |
if (config.data && config.method === 'POST') { | |
// Settings | |
xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded"); | |
// Establish the connection to the server | |
xhr.send(config.data); | |
} else { | |
// Establish the connection to the server | |
xhr.send(null); | |
} | |
} | |
return ajax; | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
define(function(){ | |
/** | |
* Following property indicates whether the current rendering engine is Trident (i.e. Internet Explorer) | |
* | |
* @return v { Integer|undefined } if IE then returns the version, otherwise returns 'undefined' to indicate NOT a IE browser | |
*/ | |
var isIE = (function() { | |
var undef, | |
v = 3, | |
div = document.createElement('div'), | |
all = div.getElementsByTagName('i'); | |
while ( | |
div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->', | |
all[0] | |
); | |
return v > 4 ? v : undef; | |
}()); | |
return isIE; | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
define(function(){ | |
// Taken directly from Douglas Crockfords JSON2.js implementation (https://raw.github.com/douglascrockford/JSON-js/master/json2.js) | |
// Create a JSON object only if one does not already exist. We create the | |
// methods in a closure to avoid creating global variables. | |
var JSON; | |
if (!JSON) { | |
JSON = {}; | |
} | |
function f(n) { | |
// Format integers to have at least two digits. | |
return n < 10 ? '0' + n : n; | |
} | |
if (typeof Date.prototype.toJSON !== 'function') { | |
Date.prototype.toJSON = function (key) { | |
return isFinite(this.valueOf()) | |
? this.getUTCFullYear() + '-' + | |
f(this.getUTCMonth() + 1) + '-' + | |
f(this.getUTCDate()) + 'T' + | |
f(this.getUTCHours()) + ':' + | |
f(this.getUTCMinutes()) + ':' + | |
f(this.getUTCSeconds()) + 'Z' | |
: null; | |
}; | |
String.prototype.toJSON = | |
Number.prototype.toJSON = | |
Boolean.prototype.toJSON = function (key) { | |
return this.valueOf(); | |
}; | |
} | |
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, | |
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, | |
gap, | |
indent, | |
meta = { // table of character substitutions | |
'\b': '\\b', | |
'\t': '\\t', | |
'\n': '\\n', | |
'\f': '\\f', | |
'\r': '\\r', | |
'"' : '\\"', | |
'\\': '\\\\' | |
}, | |
rep; | |
function quote(string) { | |
// If the string contains no control characters, no quote characters, and no | |
// backslash characters, then we can safely slap some quotes around it. | |
// Otherwise we must also replace the offending characters with safe escape | |
// sequences. | |
escapable.lastIndex = 0; | |
return escapable.test(string) ? '"' + string.replace(escapable, function (a) { | |
var c = meta[a]; | |
return typeof c === 'string' | |
? c | |
: '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); | |
}) + '"' : '"' + string + '"'; | |
} | |
function str(key, holder) { | |
// Produce a string from holder[key]. | |
var i, // The loop counter. | |
k, // The member key. | |
v, // The member value. | |
length, | |
mind = gap, | |
partial, | |
value = holder[key]; | |
// If the value has a toJSON method, call it to obtain a replacement value. | |
if (value && typeof value === 'object' && | |
typeof value.toJSON === 'function') { | |
value = value.toJSON(key); | |
} | |
// If we were called with a replacer function, then call the replacer to | |
// obtain a replacement value. | |
if (typeof rep === 'function') { | |
value = rep.call(holder, key, value); | |
} | |
// What happens next depends on the value's type. | |
switch (typeof value) { | |
case 'string': | |
return quote(value); | |
case 'number': | |
// JSON numbers must be finite. Encode non-finite numbers as null. | |
return isFinite(value) ? String(value) : 'null'; | |
case 'boolean': | |
case 'null': | |
// If the value is a boolean or null, convert it to a string. Note: | |
// typeof null does not produce 'null'. The case is included here in | |
// the remote chance that this gets fixed someday. | |
return String(value); | |
// If the type is 'object', we might be dealing with an object or an array or | |
// null. | |
case 'object': | |
// Due to a specification blunder in ECMAScript, typeof null is 'object', | |
// so watch out for that case. | |
if (!value) { | |
return 'null'; | |
} | |
// Make an array to hold the partial results of stringifying this object value. | |
gap += indent; | |
partial = []; | |
// Is the value an array? | |
if (Object.prototype.toString.apply(value) === '[object Array]') { | |
// The value is an array. Stringify every element. Use null as a placeholder | |
// for non-JSON values. | |
length = value.length; | |
for (i = 0; i < length; i += 1) { | |
partial[i] = str(i, value) || 'null'; | |
} | |
// Join all of the elements together, separated with commas, and wrap them in | |
// brackets. | |
v = partial.length === 0 | |
? '[]' | |
: gap | |
? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' | |
: '[' + partial.join(',') + ']'; | |
gap = mind; | |
return v; | |
} | |
// If the replacer is an array, use it to select the members to be stringified. | |
if (rep && typeof rep === 'object') { | |
length = rep.length; | |
for (i = 0; i < length; i += 1) { | |
if (typeof rep[i] === 'string') { | |
k = rep[i]; | |
v = str(k, value); | |
if (v) { | |
partial.push(quote(k) + (gap ? ': ' : ':') + v); | |
} | |
} | |
} | |
} else { | |
// Otherwise, iterate through all of the keys in the object. | |
for (k in value) { | |
if (Object.prototype.hasOwnProperty.call(value, k)) { | |
v = str(k, value); | |
if (v) { | |
partial.push(quote(k) + (gap ? ': ' : ':') + v); | |
} | |
} | |
} | |
} | |
// Join all of the member texts together, separated with commas, | |
// and wrap them in braces. | |
v = partial.length === 0 | |
? '{}' | |
: gap | |
? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' | |
: '{' + partial.join(',') + '}'; | |
gap = mind; | |
return v; | |
} | |
} | |
// If the JSON object does not yet have a stringify method, give it one. | |
if (typeof JSON.stringify !== 'function') { | |
JSON.stringify = function (value, replacer, space) { | |
// The stringify method takes a value and an optional replacer, and an optional | |
// space parameter, and returns a JSON text. The replacer can be a function | |
// that can replace values, or an array of strings that will select the keys. | |
// A default replacer method can be provided. Use of the space parameter can | |
// produce text that is more easily readable. | |
var i; | |
gap = ''; | |
indent = ''; | |
// If the space parameter is a number, make an indent string containing that | |
// many spaces. | |
if (typeof space === 'number') { | |
for (i = 0; i < space; i += 1) { | |
indent += ' '; | |
} | |
// If the space parameter is a string, it will be used as the indent string. | |
} else if (typeof space === 'string') { | |
indent = space; | |
} | |
// If there is a replacer, it must be a function or an array. | |
// Otherwise, throw an error. | |
rep = replacer; | |
if (replacer && typeof replacer !== 'function' && | |
(typeof replacer !== 'object' || | |
typeof replacer.length !== 'number')) { | |
throw new Error('JSON.stringify'); | |
} | |
// Make a fake root object containing our value under the key of ''. | |
// Return the result of stringifying the value. | |
return str('', {'': value}); | |
}; | |
} | |
// If the JSON object does not yet have a parse method, give it one. | |
if (typeof JSON.parse !== 'function') { | |
JSON.parse = function (text, reviver) { | |
// The parse method takes a text and an optional reviver function, and returns | |
// a JavaScript value if the text is a valid JSON text. | |
var j; | |
function walk(holder, key) { | |
// The walk method is used to recursively walk the resulting structure so | |
// that modifications can be made. | |
var k, v, value = holder[key]; | |
if (value && typeof value === 'object') { | |
for (k in value) { | |
if (Object.prototype.hasOwnProperty.call(value, k)) { | |
v = walk(value, k); | |
if (v !== undefined) { | |
value[k] = v; | |
} else { | |
delete value[k]; | |
} | |
} | |
} | |
} | |
return reviver.call(holder, key, value); | |
} | |
// Parsing happens in four stages. In the first stage, we replace certain | |
// Unicode characters with escape sequences. JavaScript handles many characters | |
// incorrectly, either silently deleting them, or treating them as line endings. | |
text = String(text); | |
cx.lastIndex = 0; | |
if (cx.test(text)) { | |
text = text.replace(cx, function (a) { | |
return '\\u' + | |
('0000' + a.charCodeAt(0).toString(16)).slice(-4); | |
}); | |
} | |
// In the second stage, we run the text against regular expressions that look | |
// for non-JSON patterns. We are especially concerned with '()' and 'new' | |
// because they can cause invocation, and '=' because it can cause mutation. | |
// But just to be safe, we want to reject all unexpected forms. | |
// We split the second stage into 4 regexp operations in order to work around | |
// crippling inefficiencies in IE's and Safari's regexp engines. First we | |
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we | |
// replace all simple value tokens with ']' characters. Third, we delete all | |
// open brackets that follow a colon or comma or that begin the text. Finally, | |
// we look to see that the remaining characters are only whitespace or ']' or | |
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. | |
if (/^[\],:{}\s]*$/ | |
.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') | |
.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') | |
.replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { | |
// In the third stage we use the eval function to compile the text into a | |
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity | |
// in JavaScript: it can begin a block or an object literal. We wrap the text | |
// in parens to eliminate the ambiguity. | |
j = eval('(' + text + ')'); | |
// In the optional fourth stage, we recursively walk the new structure, passing | |
// each name/value pair to a reviver function for possible transformation. | |
return typeof reviver === 'function' | |
? walk({'': j}, '') | |
: j; | |
} | |
// If the text is not JSON parseable, then a SyntaxError is thrown. | |
throw new SyntaxError('JSON.parse'); | |
}; | |
} | |
return JSON; | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
define(['Utils/ajax'], function(ajax){ | |
// Test Suite | |
describe('AJAX test', function() { | |
beforeEach(function() { | |
this.addMatchers({ | |
toBeNumber: function(expected) { | |
return /\d+/.test(this.actual); | |
} | |
}); | |
// Sinon.js code follows… | |
this.myAjax = sinon.useFakeXMLHttpRequest(); | |
var requests = this.requests = []; | |
this.myAjax.onCreate = function (xhr) { | |
requests.push(xhr); | |
}; | |
}); | |
afterEach(function() { | |
// Sinon.js code follows… | |
this.myAjax.restore(); | |
}); | |
it('Grabs data and returns a json object', function(){ | |
var callback = sinon.spy(); | |
ajax({ | |
url: 'JSON.php', | |
dataType: 'json', | |
onSuccess: callback | |
}); | |
this.requests[0].respond(200, { "Content-Type": "application/json" }, | |
'[{ "id": 12, "comment": "Hey there" }]'); | |
expect(this.requests.length).toBeNumber(); | |
expect(this.requests.length).toBe(1); | |
expect(callback.calledWith([{ "id": 12, "comment": "Hey there" }])).toBeTruthy(); | |
}); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment