aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVojta Jina2011-08-23 22:19:36 +0200
committerIgor Minar2011-11-30 11:17:22 -0500
commit5ad0c7d0e4a2aff071d3afb181fa618c982ce991 (patch)
treef54cd54714acab092f4268085c3f46d7b6eafee7
parent540701a8d8d843c61192f1105b210d870099cea8 (diff)
downloadangular.js-5ad0c7d0e4a2aff071d3afb181fa618c982ce991.tar.bz2
feat($httpBackend): extract $browser.xhr into separate service
- remove whole $browser.xhr stuff - remove whole mock $browser.xhr stuff - add $httpBackend service + migrate unit tests from $browser - add temporary API to access $browser's outstandingRequests count
-rw-r--r--src/angular-mocks.js180
-rw-r--r--src/service/browser.js104
-rw-r--r--src/service/httpBackend.js84
-rw-r--r--test/service/browserSpecs.js197
-rw-r--r--test/service/httpBackendSpec.js179
5 files changed, 288 insertions, 456 deletions
diff --git a/src/angular-mocks.js b/src/angular-mocks.js
index 2aabb96d..00541c8f 100644
--- a/src/angular-mocks.js
+++ b/src/angular-mocks.js
@@ -57,6 +57,10 @@ angular.module.ngMock.$Browser = function() {
self.$$lastUrl = self.$$url; // used by url polling fn
self.pollFns = [];
+ // TODO(vojta): remove this temporary api
+ self.$$completeOutstandingRequest = noop;
+ self.$$incOutstandingRequestCount = noop;
+
// register url polling fn
@@ -73,165 +77,6 @@ angular.module.ngMock.$Browser = function() {
return listener;
};
-
- /**
- * @ngdoc method
- * @name angular.module.ngMock.$browser#xhr
- * @methodOf angular.module.ngMock.$browser
- *
- * @description
- * Generic method for training browser to expect a request in a test and respond to it.
- *
- * See also convenience methods for browser training:
- *
- * - {@link #xhr.expectGET}
- * - {@link #xhr.expectPOST}
- * - {@link #xhr.expectPUT}
- * - {@link #xhr.expectDELETE}
- * - {@link #xhr.expectJSON}
- *
- * To flush pending requests in tests use
- * {@link #xhr.flush}.
- *
- * @param {string} method Expected HTTP method.
- * @param {string} url Url path for which a request is expected.
- * @param {(object|string)=} data Expected body of the (POST) HTTP request.
- * @param {function(number, *)} callback Callback to call when response is flushed.
- * @param {object} headers Key-value pairs of expected headers.
- * @returns {object} Response configuration object. You can call its `respond()` method to
- * configure what should the browser mock return when the response is
- * {@link #xhr.flush flushed}.
- */
- self.xhr = function(method, url, data, callback, headers) {
- headers = headers || {};
- if (data && angular.isObject(data)) data = angular.toJson(data);
- if (data && angular.isString(data)) url += "|" + data;
- var expect = expectations[method] || {};
- var expectation = expect[url];
- if (!expectation) {
- throw new Error("Unexpected request for method '" + method + "' and url '" + url + "'.");
- }
- requests.push(function() {
- angular.forEach(expectation.headers, function(value, key){
- if (headers[key] !== value) {
- throw new Error("Missing HTTP request header: " + key + ": " + value);
- }
- });
- callback(expectation.code, expectation.response);
- });
- // TODO(vojta): return mock request object
- };
- self.xhr.expectations = expectations;
- self.xhr.requests = requests;
- self.xhr.expect = function(method, url, data, headers) {
- if (data && angular.isObject(data)) data = angular.toJson(data);
- if (data && angular.isString(data)) url += "|" + data;
- var expect = expectations[method] || (expectations[method] = {});
- return {
- respond: function(code, response) {
- if (!angular.isNumber(code)) {
- response = code;
- code = 200;
- }
- expect[url] = {code:code, response:response, headers: headers || {}};
- }
- };
- };
-
- /**
- * @ngdoc method
- * @name angular.module.ngMock.$browser#xhr.expectGET
- * @methodOf angular.module.ngMock.$browser
- *
- * @description
- * Trains browser to expect a `GET` request and respond to it.
- *
- * @param {string} url Url path for which a request is expected.
- * @returns {object} Response configuration object. You can call its `respond()` method to
- * configure what should the browser mock return when the response is
- * {@link angular.module.ngMock.$browser#xhr.flush flushed}.
- */
- self.xhr.expectGET = angular.bind(self, self.xhr.expect, 'GET');
-
- /**
- * @ngdoc method
- * @name angular.module.ngMock.$browser#xhr.expectPOST
- * @methodOf angular.module.ngMock.$browser
- *
- * @description
- * Trains browser to expect a `POST` request and respond to it.
- *
- * @param {string} url Url path for which a request is expected.
- * @returns {object} Response configuration object. You can call its `respond()` method to
- * configure what should the browser mock return when the response is
- * {@link angular.module.ngMock.$browser#xhr.flush flushed}.
- */
- self.xhr.expectPOST = angular.bind(self, self.xhr.expect, 'POST');
-
- /**
- * @ngdoc method
- * @name angular.module.ngMock.$browser#xhr.expectDELETE
- * @methodOf angular.module.ngMock.$browser
- *
- * @description
- * Trains browser to expect a `DELETE` request and respond to it.
- *
- * @param {string} url Url path for which a request is expected.
- * @returns {object} Response configuration object. You can call its `respond()` method to
- * configure what should the browser mock return when the response is
- * {@link angular.module.ngMock.$browser#xhr.flush flushed}.
- */
- self.xhr.expectDELETE = angular.bind(self, self.xhr.expect, 'DELETE');
-
- /**
- * @ngdoc method
- * @name angular.module.ngMock.$browser#xhr.expectPUT
- * @methodOf angular.module.ngMock.$browser
- *
- * @description
- * Trains browser to expect a `PUT` request and respond to it.
- *
- * @param {string} url Url path for which a request is expected.
- * @returns {object} Response configuration object. You can call its `respond()` method to
- * configure what should the browser mock return when the response is
- * {@link angular.module.ngMock.$browser#xhr.flush flushed}.
- */
- self.xhr.expectPUT = angular.bind(self, self.xhr.expect, 'PUT');
-
- /**
- * @ngdoc method
- * @name angular.module.ngMock.$browser#xhr.expectJSON
- * @methodOf angular.module.ngMock.$browser
- *
- * @description
- * Trains browser to expect a `JSON` request and respond to it.
- *
- * @param {string} url Url path for which a request is expected.
- * @returns {object} Response configuration object. You can call its `respond()` method to
- * configure what should the browser mock return when the response is
- * {@link angular.module.ngMock.$browser#xhr.flush flushed}.
- */
- self.xhr.expectJSON = angular.bind(self, self.xhr.expect, 'JSON');
-
- /**
- * @ngdoc method
- * @name angular.module.ngMock.$browser#xhr.flush
- * @methodOf angular.module.ngMock.$browser
- *
- * @description
- * Flushes all pending requests and executes xhr callbacks with the trained response as the
- * argument.
- */
- self.xhr.flush = function() {
- if (requests.length == 0) {
- throw new Error("No xhr requests to be flushed!");
- }
-
- while(requests.length) {
- requests.pop()();
- }
- };
-
self.cookieHash = {};
self.lastCookieHash = {};
self.deferredFns = [];
@@ -871,9 +716,24 @@ function MockHttpExpectation(method, url, data, headers) {
function MockXhr() {
- // hack for testing $http
+ // hack for testing $http, $httpBackend
MockXhr.$$lastInstance = this;
+ this.open = function(method, url, async) {
+ this.$$method = method;
+ this.$$url = url;
+ this.$$async = async;
+ this.$$headers = {};
+ };
+
+ this.send = function(data) {
+ this.$$data = data;
+ };
+
+ this.setRequestHeader = function(key, value) {
+ this.$$headers[key] = value;
+ };
+
this.getResponseHeader = function(name) {
return this.$$headers[name];
};
diff --git a/src/service/browser.js b/src/service/browser.js
index 74bea44c..97e9cf3e 100644
--- a/src/service/browser.js
+++ b/src/service/browser.js
@@ -1,16 +1,5 @@
'use strict';
-//////////////////////////////
-// Browser
-//////////////////////////////
-var XHR = window.XMLHttpRequest || function() {
- try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {}
- try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {}
- try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {}
- throw new Error("This browser does not support XMLHttpRequest.");
-};
-
-
/**
* @ngdoc object
* @name angular.module.ng.$browser
@@ -33,7 +22,7 @@ var XHR = window.XMLHttpRequest || function() {
* @param {object} $log console.log or an object with the same interface.
* @param {object} $sniffer $sniffer service
*/
-function Browser(window, document, body, XHR, $log, $sniffer) {
+function Browser(window, document, body, $log, $sniffer) {
var self = this,
rawDocument = document[0],
location = window.location,
@@ -44,13 +33,12 @@ function Browser(window, document, body, XHR, $log, $sniffer) {
self.isMock = false;
- //////////////////////////////////////////////////////////////
- // XHR API
- //////////////////////////////////////////////////////////////
- var idCounter = 0;
var outstandingRequestCount = 0;
var outstandingRequestCallbacks = [];
+ // TODO(vojta): remove this temporary api
+ self.$$completeOutstandingRequest = completeOutstandingRequest;
+ self.$$incOutstandingRequestCount = function() { outstandingRequestCount++; };
/**
* Executes the `fn` function(supports currying) and decrements the `outstandingRequestCallbacks`
@@ -73,88 +61,6 @@ function Browser(window, document, body, XHR, $log, $sniffer) {
}
}
- // normalize IE bug (http://bugs.jquery.com/ticket/1450)
- function fixStatus(status) {
- return status == 1223 ? 204 : status;
- }
-
- /**
- * @ngdoc method
- * @name angular.module.ng.$browser#xhr
- * @methodOf angular.module.ng.$browser
- *
- * @param {string} method Requested method (get|post|put|delete|head|json)
- * @param {string} url Requested url
- * @param {?string} post Post data to send (null if nothing to post)
- * @param {function(number, string)} callback Function that will be called on response
- * @param {object=} header additional HTTP headers to send with XHR.
- * Standard headers are:
- * <ul>
- * <li><tt>Content-Type</tt>: <tt>application/x-www-form-urlencoded</tt></li>
- * <li><tt>Accept</tt>: <tt>application/json, text/plain, &#42;/&#42;</tt></li>
- * <li><tt>X-Requested-With</tt>: <tt>XMLHttpRequest</tt></li>
- * </ul>
- *
- * @param {number=} timeout Timeout in ms, when the request will be aborted
- * @returns {XMLHttpRequest|undefined} Raw XMLHttpRequest object or undefined when JSONP method
- *
- * @description
- * Send ajax request
- *
- * TODO(vojta): change signature of this method to (method, url, data, headers, callback)
- */
- self.xhr = function(method, url, post, callback, headers, timeout) {
- outstandingRequestCount ++;
- if (lowercase(method) == 'jsonp') {
- var callbackId = ("angular_" + Math.random() + '_' + (idCounter++)).replace(/\d\./, '');
- window[callbackId] = function(data) {
- window[callbackId].data = data;
- };
-
- var script = self.addJs(url.replace('JSON_CALLBACK', callbackId), function() {
- if (window[callbackId].data) {
- completeOutstandingRequest(callback, 200, window[callbackId].data);
- } else {
- completeOutstandingRequest(callback, -2);
- }
- delete window[callbackId];
- body[0].removeChild(script);
- });
- } else {
- var xhr = new XHR();
- xhr.open(method, url, true);
- forEach(headers, function(value, key) {
- if (value) xhr.setRequestHeader(key, value);
- });
-
- var status;
- xhr.send(post || '');
-
- // IE6, IE7 bug - does sync when serving from cache
- if (xhr.readyState == 4) {
- setTimeout(function() {
- completeOutstandingRequest(callback, fixStatus(status || xhr.status), xhr.responseText);
- }, 0);
- } else {
- xhr.onreadystatechange = function() {
- if (xhr.readyState == 4) {
- completeOutstandingRequest(callback, fixStatus(status || xhr.status),
- xhr.responseText);
- }
- };
- }
-
- if (timeout > 0) {
- setTimeout(function() {
- status = -1;
- xhr.abort();
- }, timeout);
- }
-
- return xhr;
- }
- };
-
/**
* @private
* Note: this method is used only by scenario runner
@@ -502,6 +408,6 @@ function Browser(window, document, body, XHR, $log, $sniffer) {
function $BrowserProvider(){
this.$get = ['$window', '$log', '$sniffer', '$document',
function( $window, $log, $sniffer, $document){
- return new Browser($window, $document, $document.find('body'), XHR, $log, $sniffer);
+ return new Browser($window, $document, $document.find('body'), $log, $sniffer);
}];
}
diff --git a/src/service/httpBackend.js b/src/service/httpBackend.js
index af3de970..28700940 100644
--- a/src/service/httpBackend.js
+++ b/src/service/httpBackend.js
@@ -1,6 +1,86 @@
+var XHR = window.XMLHttpRequest || function() {
+ try { return new ActiveXObject("Msxml2.XMLHTTP.6.0"); } catch (e1) {}
+ try { return new ActiveXObject("Msxml2.XMLHTTP.3.0"); } catch (e2) {}
+ try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e3) {}
+ throw new Error("This browser does not support XMLHttpRequest.");
+};
+
+
+/**
+ * @ngdoc object
+ * @name angular.module.ng.$httpBackend
+ * @requires $browser
+ * @requires $window
+ * @requires $document
+ *
+ * @description
+ */
function $HttpBackendProvider() {
- this.$get = ['$browser', function($browser) {
- return $browser.xhr;
+ this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) {
+ return createHttpBackend($browser, XHR, $browser.defer, $window, $document[0].body);
}];
}
+function createHttpBackend($browser, XHR, $browserDefer, $window, body) {
+ var idCounter = 0;
+
+ function completeRequest(callback, status, response) {
+ // normalize IE bug (http://bugs.jquery.com/ticket/1450)
+ callback(status == 1223 ? 204 : status, response);
+ $browser.$$completeOutstandingRequest(noop);
+ }
+
+ // TODO(vojta): fix the signature
+ return function(method, url, post, callback, headers, timeout) {
+ $browser.$$incOutstandingRequestCount();
+
+ if (lowercase(method) == 'jsonp') {
+ var callbackId = ('angular_' + Math.random() + '_' + (idCounter++)).replace(/\d\./, '');
+ $window[callbackId] = function(data) {
+ $window[callbackId].data = data;
+ };
+
+ var script = $browser.addJs(url.replace('JSON_CALLBACK', callbackId), null, function() {
+ if ($window[callbackId].data) {
+ completeRequest(callback, 200, $window[callbackId].data);
+ } else {
+ completeRequest(callback, -2);
+ }
+ delete $window[callbackId];
+ body.removeChild(script);
+ });
+ } else {
+ var xhr = new XHR();
+ xhr.open(method, url, true);
+ forEach(headers, function(value, key) {
+ if (value) xhr.setRequestHeader(key, value);
+ });
+
+ var status;
+ xhr.send(post || '');
+
+ // IE6, IE7 bug - does sync when serving from cache
+ if (xhr.readyState == 4) {
+ $browserDefer(function() {
+ completeRequest(callback, status || xhr.status, xhr.responseText);
+ }, 0);
+ } else {
+ xhr.onreadystatechange = function() {
+ if (xhr.readyState == 4) {
+ completeRequest(callback, status || xhr.status, xhr.responseText);
+ }
+ };
+ }
+
+ if (timeout > 0) {
+ $browserDefer(function() {
+ status = -1;
+ xhr.abort();
+ }, timeout);
+ }
+
+ return xhr;
+ }
+ };
+}
+
diff --git a/test/service/browserSpecs.js b/test/service/browserSpecs.js
index 2ec000f4..4563d14b 100644
--- a/test/service/browserSpecs.js
+++ b/test/service/browserSpecs.js
@@ -48,34 +48,17 @@ function MockWindow() {
describe('browser', function() {
- var browser, fakeWindow, xhr, logs, scripts, removedScripts, sniffer;
+ var browser, fakeWindow, logs, scripts, removedScripts, sniffer;
beforeEach(function() {
scripts = [];
removedScripts = [];
- xhr = null;
sniffer = {history: true, hashchange: true};
fakeWindow = new MockWindow();
var fakeBody = [{appendChild: function(node){scripts.push(node);},
removeChild: function(node){removedScripts.push(node);}}];
- var FakeXhr = function() {
- xhr = this;
- this.open = function(method, url, async){
- xhr.method = method;
- xhr.url = url;
- xhr.async = async;
- xhr.headers = {};
- };
- this.setRequestHeader = function(key, value){
- xhr.headers[key] = value;
- };
- this.send = function(post){
- xhr.post = post;
- };
- };
-
logs = {log:[], warn:[], info:[], error:[]};
var fakeLog = {log: function() { logs.log.push(slice.call(arguments)); },
@@ -83,8 +66,7 @@ describe('browser', function() {
info: function() { logs.info.push(slice.call(arguments)); },
error: function() { logs.error.push(slice.call(arguments)); }};
- browser = new Browser(fakeWindow, jqLite(window.document), fakeBody, FakeXhr,
- fakeLog, sniffer);
+ browser = new Browser(fakeWindow, jqLite(window.document), fakeBody, fakeLog, sniffer);
});
it('should contain cookie cruncher', function() {
@@ -97,183 +79,8 @@ describe('browser', function() {
browser.notifyWhenNoOutstandingRequests(callback);
expect(callback).toHaveBeenCalled();
});
-
- it('should queue callbacks with outstanding requests', function() {
- var callback = jasmine.createSpy('callback');
- browser.xhr('GET', '/url', null, noop);
- browser.notifyWhenNoOutstandingRequests(callback);
- expect(callback).not.toHaveBeenCalled();
-
- xhr.readyState = 4;
- xhr.onreadystatechange();
- expect(callback).toHaveBeenCalled();
- });
});
- describe('xhr', function() {
- describe('JSONP', function() {
- var log;
-
- function callback(code, data) {
- log += code + ':' + data + ';';
- }
-
- beforeEach(function() {
- log = "";
- });
-
-
- // We don't have unit tests for IE because script.readyState is readOnly.
- // Instead we run e2e tests on all browsers - see e2e for $http.
- if (!msie) {
-
- it('should add script tag for JSONP request', function() {
- var notify = jasmine.createSpy('notify');
- browser.xhr('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback);
- browser.notifyWhenNoOutstandingRequests(notify);
- expect(notify).not.toHaveBeenCalled();
- expect(scripts.length).toEqual(1);
- var script = scripts[0];
- var url = script.src.split('?cb=');
- expect(url[0]).toEqual('http://example.org/path');
- expect(typeof fakeWindow[url[1]]).toEqual('function');
- fakeWindow[url[1]]('data');
- script.onload();
-
- expect(notify).toHaveBeenCalled();
- expect(log).toEqual('200:data;');
- expect(scripts).toEqual(removedScripts);
- expect(fakeWindow[url[1]]).toBeUndefined();
- });
-
-
- it('should call callback with status -2 when script fails to load', function() {
- browser.xhr('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback);
- var script = scripts[0];
- expect(typeof script.onload).toBe('function');
- expect(typeof script.onerror).toBe('function');
- script.onerror();
-
- expect(log).toEqual('-2:undefined;');
- });
-
-
- it('should update the outstandingRequests counter for successful requests', function() {
- var notify = jasmine.createSpy('notify');
- browser.xhr('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback);
- browser.notifyWhenNoOutstandingRequests(notify);
- expect(notify).not.toHaveBeenCalled();
-
- var script = scripts[0];
- var url = script.src.split('?cb=');
- fakeWindow[url[1]]('data');
- script.onload();
-
- expect(notify).toHaveBeenCalled();
- });
-
-
- it('should update the outstandingRequests counter for failed requests', function() {
- var notify = jasmine.createSpy('notify');
- browser.xhr('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback);
- browser.notifyWhenNoOutstandingRequests(notify);
- expect(notify).not.toHaveBeenCalled();
-
- scripts[0].onerror();
-
- expect(notify).toHaveBeenCalled();
- });
- }
- });
-
-
- it('should normalize IE\'s 1223 status code into 204', function() {
- var callback = jasmine.createSpy('XHR');
-
- browser.xhr('GET', 'URL', 'POST', callback);
-
- xhr.status = 1223;
- xhr.readyState = 4;
- xhr.onreadystatechange();
-
- expect(callback).toHaveBeenCalled();
- expect(callback.argsForCall[0][0]).toEqual(204);
- });
-
- it('should set only the requested headers', function() {
- var code, response, headers = {};
- browser.xhr('POST', 'URL', null, function(c,r){
- code = c;
- response = r;
- }, {'X-header1': 'value1', 'X-header2': 'value2'});
-
- expect(xhr.method).toEqual('POST');
- expect(xhr.url).toEqual('URL');
- expect(xhr.post).toEqual('');
- expect(xhr.headers).toEqual({
- "X-header1":"value1",
- "X-header2":"value2"
- });
-
- xhr.status = 202;
- xhr.responseText = 'RESPONSE';
- xhr.readyState = 4;
- xhr.onreadystatechange();
-
- expect(code).toEqual(202);
- expect(response).toEqual('RESPONSE');
- });
-
- it('should return raw xhr object', function() {
- expect(browser.xhr('GET', '/url', null, noop)).toBe(xhr);
- });
-
- it('should abort request on timeout', function() {
- var callback = jasmine.createSpy('done').andCallFake(function(status, response) {
- expect(status).toBe(-1);
- });
-
- browser.xhr('GET', '/url', null, callback, {}, 2000);
- xhr.abort = jasmine.createSpy('xhr.abort');
-
- fakeWindow.setTimeout.flush();
- expect(xhr.abort).toHaveBeenCalledOnce();
-
- xhr.status = 0;
- xhr.readyState = 4;
- xhr.onreadystatechange();
- expect(callback).toHaveBeenCalledOnce();
- });
-
- it('should be async even if xhr.send() is sync', function() {
- // IE6, IE7 is sync when serving from cache
- var xhr;
- function FakeXhr() {
- xhr = this;
- this.open = this.setRequestHeader = noop;
- this.send = function() {
- this.status = 200;
- this.responseText = 'response';
- this.readyState = 4;
- };
- }
-
- var callback = jasmine.createSpy('done').andCallFake(function(status, response) {
- expect(status).toBe(200);
- expect(response).toBe('response');
- });
-
- browser = new Browser(fakeWindow, jqLite(window.document), null, FakeXhr, null);
- browser.xhr('GET', '/url', null, callback);
- expect(callback).not.toHaveBeenCalled();
-
- fakeWindow.setTimeout.flush();
- expect(callback).toHaveBeenCalledOnce();
-
- (xhr.onreadystatechange || noop)();
- expect(callback).toHaveBeenCalledOnce();
- });
- });
describe('defer', function() {
it('should execute fn asynchroniously via setTimeout', function() {
diff --git a/test/service/httpBackendSpec.js b/test/service/httpBackendSpec.js
new file mode 100644
index 00000000..e609eea6
--- /dev/null
+++ b/test/service/httpBackendSpec.js
@@ -0,0 +1,179 @@
+describe('$httpBackend', function() {
+
+ var $backend, $browser, $window,
+ xhr, fakeBody, callback;
+
+ // TODO(vojta): should be replaced by $defer mock
+ function fakeTimeout(fn, delay) {
+ fakeTimeout.fns.push(fn);
+ fakeTimeout.delays.push(delay);
+ }
+
+ fakeTimeout.fns = [];
+ fakeTimeout.delays = [];
+ fakeTimeout.flush = function() {
+ var len = fakeTimeout.fns.length;
+ fakeTimeout.delays = [];
+ while (len--) fakeTimeout.fns.shift()();
+ };
+
+
+ beforeEach(inject(function($injector) {
+ $window = {};
+ $browser = $injector.get('$browser');
+ fakeBody = {removeChild: jasmine.createSpy('body.removeChild')};
+ $backend = createHttpBackend($browser, MockXhr, fakeTimeout, $window, fakeBody);
+ callback = jasmine.createSpy('done');
+ }));
+
+
+ it('should do basics - open async xhr and send data', function() {
+ $backend('GET', '/some-url', 'some-data', noop);
+ xhr = MockXhr.$$lastInstance;
+
+ expect(xhr.$$method).toBe('GET');
+ expect(xhr.$$url).toBe('/some-url');
+ expect(xhr.$$data).toBe('some-data');
+ expect(xhr.$$async).toBe(true);
+ });
+
+
+ it('should normalize IE\'s 1223 status code into 204', function() {
+ callback.andCallFake(function(status) {
+ expect(status).toBe(204);
+ });
+
+ $backend('GET', 'URL', null, callback);
+ xhr = MockXhr.$$lastInstance;
+
+ xhr.status = 1223;
+ xhr.readyState = 4;
+ xhr.onreadystatechange();
+
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ it('should set only the requested headers', function() {
+ $backend('POST', 'URL', null, noop, {'X-header1': 'value1', 'X-header2': 'value2'});
+ xhr = MockXhr.$$lastInstance;
+
+ expect(xhr.$$headers).toEqual({
+ 'X-header1': 'value1',
+ 'X-header2': 'value2'
+ });
+ });
+
+
+ it('should return raw xhr object', function() {
+ expect($backend('GET', '/url', null, noop)).toBe(MockXhr.$$lastInstance);
+ });
+
+
+ it('should abort request on timeout', function() {
+ callback.andCallFake(function(status, response) {
+ expect(status).toBe(-1);
+ });
+
+ $backend('GET', '/url', null, callback, {}, 2000);
+ xhr = MockXhr.$$lastInstance;
+ spyOn(xhr, 'abort');
+
+ expect(fakeTimeout.delays[0]).toBe(2000);
+
+ fakeTimeout.flush();
+ expect(xhr.abort).toHaveBeenCalledOnce();
+
+ xhr.status = 0;
+ xhr.readyState = 4;
+ xhr.onreadystatechange();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ it('should be async even if xhr.send() is sync', function() {
+ // IE6, IE7 is sync when serving from cache
+ function SyncXhr() {
+ xhr = this;
+ this.open = this.setRequestHeader = noop;
+ this.send = function() {
+ this.status = 200;
+ this.responseText = 'response';
+ this.readyState = 4;
+ };
+ }
+
+ callback.andCallFake(function(status, response) {
+ expect(status).toBe(200);
+ expect(response).toBe('response');
+ });
+
+ $backend = createHttpBackend($browser, SyncXhr, fakeTimeout);
+ $backend('GET', '/url', null, callback);
+ expect(callback).not.toHaveBeenCalled();
+
+ fakeTimeout.flush();
+ expect(callback).toHaveBeenCalledOnce();
+
+ (xhr.onreadystatechange || noop)();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ describe('JSONP', function() {
+
+ it('should add script tag for JSONP request', function() {
+ callback.andCallFake(function(status, response) {
+ expect(status).toBe(200);
+ expect(response).toBe('some-data');
+ });
+
+ $backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback);
+ expect($browser.$$scripts.length).toBe(1);
+
+ var script = $browser.$$scripts.shift(),
+ url = script.url.split('?cb=');
+
+ expect(url[0]).toBe('http://example.org/path');
+ $window[url[1]]('some-data');
+ script.done();
+
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ it('should clean up the callback and remove the script', function() {
+ $backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback);
+ expect($browser.$$scripts.length).toBe(1);
+
+ var script = $browser.$$scripts.shift(),
+ callbackId = script.url.split('?cb=')[1];
+
+ $window[callbackId]('some-data');
+ script.done();
+
+ expect($window[callbackId]).toBeUndefined();
+ expect(fakeBody.removeChild).toHaveBeenCalledOnce();
+ expect(fakeBody.removeChild).toHaveBeenCalledWith(script);
+ });
+
+
+ it('should call callback with status -2 when script fails to load', function() {
+ callback.andCallFake(function(status, response) {
+ expect(status).toBe(-2);
+ expect(response).toBeUndefined();
+ });
+
+ $backend('JSONP', 'http://example.org/path?cb=JSON_CALLBACK', null, callback);
+ expect($browser.$$scripts.length).toBe(1);
+
+ $browser.$$scripts.shift().done();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+
+ // TODO(vojta): test whether it fires "async-start"
+ // TODO(vojta): test whether it fires "async-end" on both success and error
+ });
+});
+