diff options
| author | Vojta Jina | 2011-08-23 22:19:36 +0200 |
|---|---|---|
| committer | Igor Minar | 2011-11-30 11:17:22 -0500 |
| commit | 5ad0c7d0e4a2aff071d3afb181fa618c982ce991 (patch) | |
| tree | f54cd54714acab092f4268085c3f46d7b6eafee7 | |
| parent | 540701a8d8d843c61192f1105b210d870099cea8 (diff) | |
| download | angular.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.js | 180 | ||||
| -rw-r--r-- | src/service/browser.js | 104 | ||||
| -rw-r--r-- | src/service/httpBackend.js | 84 | ||||
| -rw-r--r-- | test/service/browserSpecs.js | 197 | ||||
| -rw-r--r-- | test/service/httpBackendSpec.js | 179 |
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, */*</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 + }); +}); + |
