From 32c09c1d195fcb98f6e29fc7e554a867f4762301 Mon Sep 17 00:00:00 2001 From: jim lyndon Date: Thu, 18 Jul 2013 02:08:38 +1000 Subject: [PATCH] feat($http): add xhr statusText to completeRequest callback Makes xhr status text accessible is $http success/error callback. See www.w3.org/TR/XMLHttpRequest/#dom-xmlhttprequest-statustext Closes #2335 Closes #2665 Closes #6713 --- src/ng/http.js | 16 ++++++----- src/ng/httpBackend.js | 10 ++++--- src/ngMock/angular-mocks.js | 46 +++++++++++++++++--------------- test/ng/httpBackendSpec.js | 26 ++++++++++++++++++ test/ng/httpSpec.js | 24 +++++++++++++++++ test/ngMock/angular-mocksSpec.js | 20 +++++++------- 6 files changed, 100 insertions(+), 42 deletions(-) diff --git a/src/ng/http.js b/src/ng/http.js index a7a15d7e982b..0c86a0b7fe4b 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -571,6 +571,7 @@ function $HttpProvider() { * - **status** – `{number}` – HTTP status code of the response. * - **headers** – `{function([headerName])}` – Header getter function. * - **config** – `{Object}` – The configuration object that was used to generate the request. + * - **statusText** – `{string}` – HTTP status text of the response. * * @property {Array.} pendingRequests Array of config objects for currently pending * requests. This is primarily meant to be used for debugging purposes. @@ -945,9 +946,9 @@ function $HttpProvider() { } else { // serving from cache if (isArray(cachedResp)) { - resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2])); + resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2]), cachedResp[3]); } else { - resolvePromise(cachedResp, 200, {}); + resolvePromise(cachedResp, 200, {}, 'OK'); } } } else { @@ -971,17 +972,17 @@ function $HttpProvider() { * - resolves the raw $http promise * - calls $apply */ - function done(status, response, headersString) { + function done(status, response, headersString, statusText) { if (cache) { if (isSuccess(status)) { - cache.put(url, [status, response, parseHeaders(headersString)]); + cache.put(url, [status, response, parseHeaders(headersString), statusText]); } else { // remove promise from the cache cache.remove(url); } } - resolvePromise(response, status, headersString); + resolvePromise(response, status, headersString, statusText); if (!$rootScope.$$phase) $rootScope.$apply(); } @@ -989,7 +990,7 @@ function $HttpProvider() { /** * Resolves the raw $http promise. */ - function resolvePromise(response, status, headers) { + function resolvePromise(response, status, headers, statusText) { // normalize internal statuses to 0 status = Math.max(status, 0); @@ -997,7 +998,8 @@ function $HttpProvider() { data: response, status: status, headers: headersGetter(headers), - config: config + config: config, + statusText : statusText }); } diff --git a/src/ng/httpBackend.js b/src/ng/httpBackend.js index 28107966f643..63b0745658da 100644 --- a/src/ng/httpBackend.js +++ b/src/ng/httpBackend.js @@ -97,7 +97,8 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc completeRequest(callback, status || xhr.status, response, - responseHeaders); + responseHeaders, + xhr.statusText || ''); } }; @@ -138,7 +139,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc xhr && xhr.abort(); } - function completeRequest(callback, status, response, headersString) { + function completeRequest(callback, status, response, headersString, statusText) { // cancel timeout and subsequent timeout promise resolution timeoutId && $browserDefer.cancel(timeoutId); jsonpDone = xhr = null; @@ -151,9 +152,10 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc } // normalize IE bug (http://bugs.jquery.com/ticket/1450) - status = status == 1223 ? 204 : status; + status = status === 1223 ? 204 : status; + statusText = statusText || ''; - callback(status, response, headersString); + callback(status, response, headersString, statusText); $browser.$$completeOutstandingRequest(noop); } }; diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 67decaceb33f..cb10ba8100e7 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -1090,12 +1090,12 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { responsesPush = angular.bind(responses, responses.push), copy = angular.copy; - function createResponse(status, data, headers) { + function createResponse(status, data, headers, statusText) { if (angular.isFunction(status)) return status; return function() { return angular.isNumber(status) - ? [status, data, headers] + ? [status, data, headers, statusText] : [200, status, data]; }; } @@ -1120,7 +1120,8 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { function handleResponse() { var response = wrapped.response(method, url, data, headers); xhr.$$respHeaders = response[2]; - callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders()); + callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders(), + copy(response[3] || '')); } function handleTimeout() { @@ -1187,16 +1188,17 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * request is handled. * * - respond – - * `{function([status,] data[, headers])|function(function(method, url, data, headers)}` - * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string) and response headers - * (Object). + * `{function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers)}` + * – The respond method takes a set of static data to be returned or a function that can + * return an array containing response status (number), response data (string), response + * headers (Object), and the text for the status (string). */ $httpBackend.when = function(method, url, data, headers) { var definition = new MockHttpExpectation(method, url, data, headers), chain = { - respond: function(status, data, headers) { - definition.response = createResponse(status, data, headers); + respond: function(status, data, headers, statusText) { + definition.response = createResponse(status, data, headers, statusText); } }; @@ -1304,17 +1306,18 @@ function createHttpBackendMock($rootScope, $delegate, $browser) { * request is handled. * * - respond – - * `{function([status,] data[, headers])|function(function(method, url, data, headers)}` - * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string) and response headers - * (Object). + * `{function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers)}` + * – The respond method takes a set of static data to be returned or a function that can + * return an array containing response status (number), response data (string), response + * headers (Object), and the text for the status (string). */ $httpBackend.expect = function(method, url, data, headers) { var expectation = new MockHttpExpectation(method, url, data, headers); expectations.push(expectation); return { - respond: function(status, data, headers) { - expectation.response = createResponse(status, data, headers); + respond: function (status, data, headers, statusText) { + expectation.response = createResponse(status, data, headers, statusText); } }; }; @@ -1816,13 +1819,14 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { * control how a matched request is handled. * * - respond – - * `{function([status,] data[, headers])|function(function(method, url, data, headers)}` + * `{function([status,] data[, headers, statusText]) + * | function(function(method, url, data, headers)}` * – The respond method takes a set of static data to be returned or a function that can return - * an array containing response status (number), response data (string) and response headers - * (Object). - * - passThrough – `{function()}` – Any request matching a backend definition with `passThrough` - * handler will be passed through to the real backend (an XHR request will be made to the - * server.) + * an array containing response status (number), response data (string), response headers + * (Object), and the text for the status (string). + * - passThrough – `{function()}` – Any request matching a backend definition with + * `passThrough` handler will be passed through to the real backend (an XHR request will be made + * to the server.) */ /** diff --git a/test/ng/httpBackendSpec.js b/test/ng/httpBackendSpec.js index 5c9cbf586c36..a549b6a894a3 100644 --- a/test/ng/httpBackendSpec.js +++ b/test/ng/httpBackendSpec.js @@ -75,6 +75,32 @@ describe('$httpBackend', function() { expect(xhr.$$data).toBe(null); }); + it('should call completion function with xhr.statusText if present', function() { + callback.andCallFake(function(status, response, headers, statusText) { + expect(statusText).toBe('OK'); + }); + + $backend('GET', '/some-url', null, callback); + xhr = MockXhr.$$lastInstance; + xhr.statusText = 'OK'; + xhr.readyState = 4; + xhr.onreadystatechange(); + expect(callback).toHaveBeenCalledOnce(); + }); + + it('should call completion function with empty string if not present', function() { + callback.andCallFake(function(status, response, headers, statusText) { + expect(statusText).toBe(''); + }); + + $backend('GET', '/some-url', null, callback); + xhr = MockXhr.$$lastInstance; + xhr.readyState = 4; + xhr.onreadystatechange(); + expect(callback).toHaveBeenCalledOnce(); + }); + + it('should normalize IE\'s 1223 status code into 204', function() { callback.andCallFake(function(status) { expect(status).toBe(204); diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js index 86ab72ea8bd8..14ac8433437f 100644 --- a/test/ng/httpSpec.js +++ b/test/ng/httpSpec.js @@ -481,6 +481,30 @@ describe('$http', function() { }); + it('should pass statusText in response object when a request is successful', function() { + $httpBackend.expect('GET', '/url').respond(200, 'SUCCESS', {}, 'OK'); + $http({url: '/url', method: 'GET'}).then(function(response) { + expect(response.statusText).toBe('OK'); + callback(); + }); + + $httpBackend.flush(); + expect(callback).toHaveBeenCalledOnce(); + }); + + + it('should pass statusText in response object when a request fails', function() { + $httpBackend.expect('GET', '/url').respond(404, 'ERROR', {}, 'Not Found'); + $http({url: '/url', method: 'GET'}).then(null, function(response) { + expect(response.statusText).toBe('Not Found'); + callback(); + }); + + $httpBackend.flush(); + expect(callback).toHaveBeenCalledOnce(); + }); + + it('should pass in the response object when a request failed', function() { $httpBackend.expect('GET', '/url').respond(543, 'bad error', {'request-id': '123'}); $http({url: '/url', method: 'GET'}).then(null, function(response) { diff --git a/test/ngMock/angular-mocksSpec.js b/test/ngMock/angular-mocksSpec.js index b78e9bbf207a..c10bbc83e0fa 100644 --- a/test/ngMock/angular-mocksSpec.js +++ b/test/ngMock/angular-mocksSpec.js @@ -1068,29 +1068,29 @@ describe('ngMock', function() { hb.flush(); expect(callback.callCount).toBe(2); - expect(callback.argsForCall[0]).toEqual([201, 'second', '']); - expect(callback.argsForCall[1]).toEqual([200, 'first', '']); + expect(callback.argsForCall[0]).toEqual([201, 'second', '', '']); + expect(callback.argsForCall[1]).toEqual([200, 'first', '', '']); }); describe('respond()', function() { it('should take values', function() { - hb.expect('GET', '/url1').respond(200, 'first', {'header': 'val'}); + hb.expect('GET', '/url1').respond(200, 'first', {'header': 'val'}, 'OK'); hb('GET', '/url1', undefined, callback); hb.flush(); - expect(callback).toHaveBeenCalledOnceWith(200, 'first', 'header: val'); + expect(callback).toHaveBeenCalledOnceWith(200, 'first', 'header: val', 'OK'); }); it('should take function', function() { - hb.expect('GET', '/some').respond(function(m, u, d, h) { - return [301, m + u + ';' + d + ';a=' + h.a, {'Connection': 'keep-alive'}]; + hb.expect('GET', '/some').respond(function (m, u, d, h) { + return [301, m + u + ';' + d + ';a=' + h.a, {'Connection': 'keep-alive'}, 'Moved Permanently']; }); hb('GET', '/some', 'data', callback, {a: 'b'}); hb.flush(); - expect(callback).toHaveBeenCalledOnceWith(301, 'GET/some;data;a=b', 'Connection: keep-alive'); + expect(callback).toHaveBeenCalledOnceWith(301, 'GET/some;data;a=b', 'Connection: keep-alive', 'Moved Permanently'); }); it('should default status code to 200', function() { @@ -1119,8 +1119,8 @@ describe('ngMock', function() { hb.flush(); expect(callback.callCount).toBe(2); - expect(callback.argsForCall[0]).toEqual([200, 'first', '']); - expect(callback.argsForCall[1]).toEqual([200, 'second', '']); + expect(callback.argsForCall[0]).toEqual([200, 'first', '', '']); + expect(callback.argsForCall[1]).toEqual([200, 'second', '', '']); }); }); @@ -1415,7 +1415,7 @@ describe('ngMock', function() { hb[shortcut]('/foo').respond('bar'); hb(method, '/foo', undefined, callback); hb.flush(); - expect(callback).toHaveBeenCalledOnceWith(200, 'bar', ''); + expect(callback).toHaveBeenCalledOnceWith(200, 'bar', '', ''); }); }); });