From b5594a773a6f07dcba914aa385f92d3305285b24 Mon Sep 17 00:00:00 2001 From: Karl Seamon Date: Fri, 22 Jul 2011 15:56:45 -0400 Subject: feat($xhr): add custom error callback to $xhr, $xhr.cache, $xhr.bulk, $resource Closes #408 --- src/Resource.js | 69 ++++++++++++++++++++------------- src/service/resource.js | 10 ++--- src/service/xhr.bulk.js | 60 ++++++++++++++++++++--------- src/service/xhr.cache.js | 90 +++++++++++++++++++++++++++++++------------ src/service/xhr.js | 29 ++++++++------ test/ResourceSpec.js | 42 +++++++++++--------- test/service/xhr.bulkSpec.js | 29 ++++++++++---- test/service/xhr.cacheSpec.js | 38 ++++++++++++++++-- test/service/xhr.errorSpec.js | 6 +-- test/service/xhrSpec.js | 42 +++++++++++++++++--- 10 files changed, 291 insertions(+), 124 deletions(-) diff --git a/src/Resource.js b/src/Resource.js index 5462826d..3c149d8b 100644 --- a/src/Resource.js +++ b/src/Resource.js @@ -67,29 +67,36 @@ ResourceFactory.prototype = { forEach(actions, function(action, name){ var isPostOrPut = action.method == 'POST' || action.method == 'PUT'; - Resource[name] = function (a1, a2, a3) { + Resource[name] = function (a1, a2, a3, a4) { var params = {}; var data; - var callback = noop; + var success = noop; + var error = null; switch(arguments.length) { - case 3: callback = a3; + case 4: + error = a4; + success = a3; + case 3: case 2: if (isFunction(a2)) { - callback = a2; + success = a2; + error = a3; //fallthrough } else { params = a1; data = a2; + success = a3; break; } case 1: - if (isFunction(a1)) callback = a1; + if (isFunction(a1)) success = a1; else if (isPostOrPut) data = a1; else params = a1; break; case 0: break; default: - throw "Expected between 0-3 arguments [params, data, callback], got " + arguments.length + " arguments."; + throw "Expected between 0-4 arguments [params, data, success, error], got " + + arguments.length + " arguments."; } var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data)); @@ -97,23 +104,20 @@ ResourceFactory.prototype = { action.method, route.url(extend({}, action.params || {}, extractParams(data), params)), data, - function(status, response, clear) { - if (200 <= status && status < 300) { - if (response) { - if (action.isArray) { - value.length = 0; - forEach(response, function(item){ - value.push(new Resource(item)); - }); - } else { - copy(response, value); - } + function(status, response) { + if (response) { + if (action.isArray) { + value.length = 0; + forEach(response, function(item) { + value.push(new Resource(item)); + }); + } else { + copy(response, value); } - (callback||noop)(value); - } else { - throw {status: status, response:response, message: status + ": " + response}; } + (success||noop)(value); }, + error || action.verifyCache, action.verifyCache); return value; }; @@ -122,18 +126,29 @@ ResourceFactory.prototype = { return self.route(url, extend({}, paramDefaults, additionalParamDefaults), actions); }; - Resource.prototype['$' + name] = function(a1, a2){ - var params = extractParams(this); - var callback = noop; + Resource.prototype['$' + name] = function(a1, a2, a3) { + var params = extractParams(this), + success = noop, + error; + switch(arguments.length) { - case 2: params = a1; callback = a2; - case 1: if (typeof a1 == $function) callback = a1; else params = a1; + case 3: params = a1; success = a2; error = a3; break; + case 2: + case 1: + if (isFunction(a1)) { + success = a1; + error = a2; + } else { + params = a1; + success = a2 || noop; + } case 0: break; default: - throw "Expected between 1-2 arguments [params, callback], got " + arguments.length + " arguments."; + throw "Expected between 1-3 arguments [params, success, error], got " + + arguments.length + " arguments."; } var data = isPostOrPut ? this : undefined; - Resource[name].call(this, params, data, callback); + Resource[name].call(this, params, data, success, error); }; }); return Resource; diff --git a/src/service/resource.js b/src/service/resource.js index 31d7ceeb..c11067b1 100644 --- a/src/service/resource.js +++ b/src/service/resource.js @@ -82,9 +82,9 @@ * The action methods on the class object or instance object can be invoked with the following * parameters: * - * - HTTP GET "class" actions: `Resource.action([parameters], [callback])` - * - non-GET "class" actions: `Resource.action(postData, [parameters], [callback])` - * - non-GET instance actions: `instance.$action([parameters], [callback])` + * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` + * - non-GET "class" actions: `Resource.action(postData, [parameters], [success], [error])` + * - non-GET instance actions: `instance.$action([parameters], [success], [error])` * * * @example @@ -142,8 +142,8 @@ }); * - * It's worth noting that the callback for `get`, `query` and other method gets passed in the - * response that came from the server, so one could rewrite the above example as: + * It's worth noting that the success callback for `get`, `query` and other method gets passed + * in the response that came from the server, so one could rewrite the above example as: *
      var User = $resource('/user/:userId', {userId:'@id'});
diff --git a/src/service/xhr.bulk.js b/src/service/xhr.bulk.js
index c0940d9d..d7fc7990 100644
--- a/src/service/xhr.bulk.js
+++ b/src/service/xhr.bulk.js
@@ -15,9 +15,10 @@
 angularServiceInject('$xhr.bulk', function($xhr, $error, $log){
   var requests = [],
       scope = this;
-  function bulkXHR(method, url, post, callback) {
+  function bulkXHR(method, url, post, success, error) {
     if (isFunction(post)) {
-      callback = post;
+      error = success;
+      success = post;
       post = null;
     }
     var currentQueue;
@@ -28,32 +29,55 @@ angularServiceInject('$xhr.bulk', function($xhr, $error, $log){
     });
     if (currentQueue) {
       if (!currentQueue.requests) currentQueue.requests = [];
-      currentQueue.requests.push({method: method, url: url, data:post, callback:callback});
+      var request = {
+          method: method,
+          url: url,
+          data: post,
+          success: success};
+      if (error) request.error = error;
+      currentQueue.requests.push(request);
     } else {
-      $xhr(method, url, post, callback);
+      $xhr(method, url, post, success, error);
     }
   }
   bulkXHR.urls = {};
-  bulkXHR.flush = function(callback){
-    forEach(bulkXHR.urls, function(queue, url){
+  bulkXHR.flush = function(success, error) {
+    forEach(bulkXHR.urls, function(queue, url) {
       var currentRequests = queue.requests;
       if (currentRequests && currentRequests.length) {
         queue.requests = [];
         queue.callbacks = [];
-        $xhr('POST', url, {requests:currentRequests}, function(code, response){
-          forEach(response, function(response, i){
-            try {
-              if (response.status == 200) {
-                (currentRequests[i].callback || noop)(response.status, response.response);
-              } else {
-                $error(currentRequests[i], response);
+        $xhr('POST', url, {requests: currentRequests},
+          function(code, response) {
+            forEach(response, function(response, i) {
+              try {
+                if (response.status == 200) {
+                  (currentRequests[i].success || noop)(response.status, response.response);
+                } else if (isFunction(currentRequests[i].error)) {
+                    currentRequests[i].error(response.status, response.response);
+                } else {
+                  $error(currentRequests[i], response);
+                }
+              } catch(e) {
+                $log.error(e);
               }
-            } catch(e) {
-              $log.error(e);
-            }
+            });
+            (success || noop)();
+          },
+          function(code, response) {
+            forEach(currentRequests, function(request, i) {
+              try {
+                if (isFunction(request.error)) {
+                  request.error(code, response);
+                } else {
+                  $error(request, response);
+                }
+              } catch(e) {
+                $log.error(e);
+              }
+            });
+            (error || noop)();
           });
-          (callback || noop)();
-        });
         scope.$eval();
       }
     });
diff --git a/src/service/xhr.cache.js b/src/service/xhr.cache.js
index 42b666e1..256b936e 100644
--- a/src/service/xhr.cache.js
+++ b/src/service/xhr.cache.js
@@ -5,7 +5,11 @@
  * @ngdoc service
  * @name angular.service.$xhr.cache
  * @function
- * @requires $xhr
+ *
+ * @requires $xhr.bulk
+ * @requires $defer
+ * @requires $xhr.error
+ * @requires $log
  *
  * @description
  * Acts just like the {@link angular.service.$xhr $xhr} service but caches responses for `GET`
@@ -18,27 +22,42 @@
  * @param {string} method HTTP method.
  * @param {string} url Destination URL.
  * @param {(string|Object)=} post Request body.
- * @param {function(number, (string|Object))} callback Response callback.
+ * @param {function(number, (string|Object))} success Response success callback.
+ * @param {function(number, (string|Object))=} error Response error callback.
  * @param {boolean=} [verifyCache=false] If `true` then a result is immediately returned from cache
  *   (if present) while a request is sent to the server for a fresh response that will update the
- *   cached entry. The `callback` function will be called when the response is received.
- * @param {boolean=} [sync=false] in case of cache hit execute `callback` synchronously.
+ *   cached entry. The `success` function will be called when the response is received.
+ * @param {boolean=} [sync=false] in case of cache hit execute `success` synchronously.
  */
-angularServiceInject('$xhr.cache', function($xhr, $defer, $log){
+angularServiceInject('$xhr.cache', function($xhr, $defer, $error, $log) {
   var inflight = {}, self = this;
-  function cache(method, url, post, callback, verifyCache, sync){
+  function cache(method, url, post, success, error, verifyCache, sync) {
     if (isFunction(post)) {
-      callback = post;
+      if (!isFunction(success)) {
+        verifyCache = success;
+        sync = error;
+        error = null;
+      } else {
+        sync = verifyCache;
+        verifyCache = error;
+        error = success;
+      }
+      success = post;
       post = null;
+    } else if (!isFunction(error)) {
+      sync = verifyCache;
+      verifyCache = error;
+      error = null;
     }
+
     if (method == 'GET') {
       var data, dataCached;
       if (dataCached = cache.data[url]) {
 
         if (sync) {
-          callback(200, copy(dataCached.value));
+          success(200, copy(dataCached.value));
         } else {
-          $defer(function() { callback(200, copy(dataCached.value)); });
+          $defer(function() { success(200, copy(dataCached.value)); });
         }
 
         if (!verifyCache)
@@ -46,30 +65,51 @@ angularServiceInject('$xhr.cache', function($xhr, $defer, $log){
       }
 
       if (data = inflight[url]) {
-        data.callbacks.push(callback);
+        data.successes.push(success);
+        data.errors.push(error);
       } else {
-        inflight[url] = {callbacks: [callback]};
-        cache.delegate(method, url, post, function(status, response){
-          if (status == 200)
-            cache.data[url] = { value: response };
-          var callbacks = inflight[url].callbacks;
-          delete inflight[url];
-          forEach(callbacks, function(callback){
-            try {
-              (callback||noop)(status, copy(response));
-            } catch(e) {
-              $log.error(e);
-            }
+        inflight[url] = {successes: [success], errors: [error]};
+        cache.delegate(method, url, post,
+          function(status, response) {
+            if (status == 200)
+              cache.data[url] = {value: response};
+            var successes = inflight[url].successes;
+            delete inflight[url];
+            forEach(successes, function(success) {
+              try {
+                (success||noop)(status, copy(response));
+              } catch(e) {
+                $log.error(e);
+              }
+            });
+          },
+          function(status, response) {
+            var errors = inflight[url].errors,
+                successes = inflight[url].successes;
+            delete inflight[url];
+
+            forEach(errors, function(error, i) {
+              try {
+                if (isFunction(error)) {
+                  error(status, copy(response));
+                } else {
+                  $error(
+                    {method: method, url: url, data: post, success: successes[i]},
+                    {status: status, body: response});
+                }
+              } catch(e) {
+                $log.error(e);
+              }
+            });
           });
-        });
       }
 
     } else {
       cache.data = {};
-      cache.delegate(method, url, post, callback);
+      cache.delegate(method, url, post, success, error);
     }
   }
   cache.data = {};
   cache.delegate = $xhr;
   return cache;
-}, ['$xhr.bulk', '$defer', '$log']);
+}, ['$xhr.bulk', '$defer', '$xhr.error', '$log']);
diff --git a/src/service/xhr.js b/src/service/xhr.js
index 5fc5223e..dc18419d 100644
--- a/src/service/xhr.js
+++ b/src/service/xhr.js
@@ -21,10 +21,10 @@
  * {@link angular.service.$resource $resource} service.
  *
  * # Error handling
- * All XHR responses with response codes other then `2xx` are delegated to
- * {@link angular.service.$xhr.error $xhr.error}. The `$xhr.error` can intercept the request
- * and process it in application specific way, or resume normal execution by calling the
- * request callback method.
+ * If no `error callback` is specified, XHR response with response code other then `2xx` will be
+ * delegated to {@link angular.service.$xhr.error $xhr.error}. The `$xhr.error` can intercept the
+ * request and process it in application specific way, or resume normal execution by calling the
+ * request `success` method.
  *
  * # HTTP Headers
  * The $xhr service will automatically add certain http headers to all requests. These defaults can
@@ -96,14 +96,16 @@
  *   angular generated callback function.
  * @param {(string|Object)=} post Request content as either a string or an object to be stringified
  *   as JSON before sent to the server.
- * @param {function(number, (string|Object))} callback A function to be called when the response is
- *   received. The callback will be called with:
+ * @param {function(number, (string|Object))} success A function to be called when the response is
+ *   received. The success function will be called with:
  *
  *   - {number} code [HTTP status code](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes) of
  *     the response. This will currently always be 200, since all non-200 responses are routed to
- *     {@link angular.service.$xhr.error} service.
+ *     {@link angular.service.$xhr.error} service (or custom error callback).
  *   - {string|Object} response Response object as string or an Object if the response was in JSON
  *     format.
+ * @param {function(number, (string|Object))} error A function to be called if the response code is
+ *   not 2xx.. Accepts the same arguments as success, above.
  *
  * @example
    
@@ -158,9 +160,10 @@ angularServiceInject('$xhr', function($browser, $error, $log, $updateView){
     patch: {}
   };
 
-  function xhr(method, url, post, callback){
+  function xhr(method, url, post, success, error) {
     if (isFunction(post)) {
-      callback = post;
+      error = success;
+      success = post;
       post = null;
     }
     if (post && isObject(post)) {
@@ -176,11 +179,13 @@ angularServiceInject('$xhr', function($browser, $error, $log, $updateView){
           }
         }
         if (200 <= code && code < 300) {
-          callback(code, response);
+          success(code, response);
+        } else if (isFunction(error)) {
+          error(code, response);
         } else {
           $error(
-            {method: method, url:url, data:post, callback:callback},
-            {status: code, body:response});
+            {method: method, url: url, data: post, success: success},
+            {status: code, body: response});
         }
       } catch (e) {
         $log.error(e);
diff --git a/test/ResourceSpec.js b/test/ResourceSpec.js
index 0b8d2187..81519f0f 100644
--- a/test/ResourceSpec.js
+++ b/test/ResourceSpec.js
@@ -1,12 +1,12 @@
 'use strict';
 
 describe("resource", function() {
-  var xhr, resource, CreditCard, callback;
+  var xhr, resource, CreditCard, callback, $xhrErr;
 
-  beforeEach(function(){
-    var browser = new MockBrowser();
-    xhr = browser.xhr;
-    resource = new ResourceFactory(xhr);
+  beforeEach(function() {
+    var scope = angular.scope({}, null, {'$xhr.error': $xhrErr = jasmine.createSpy('xhr.error')});
+    xhr = scope.$service('$browser').xhr;
+    resource = new ResourceFactory(scope.$service('$xhr'));
     CreditCard = resource.route('/CreditCard/:id:verb', {id:'@id.key'}, {
       charge:{
         method:'POST',
@@ -242,19 +242,25 @@ describe("resource", function() {
     dealoc(scope);
   });
 
-  describe('failure mode', function(){
-    it('should report error when non 200', function(){
-      xhr.expectGET('/CreditCard/123').respond(500, "Server Error");
-      var cc = CreditCard.get({id:123});
-      try {
-        xhr.flush();
-        fail('expected exception, non thrown');
-      } catch (e) {
-        expect(e.status).toEqual(500);
-        expect(e.response).toEqual('Server Error');
-        expect(e.message).toEqual('500: Server Error');
-      }
+  describe('failure mode', function() {
+    var ERROR_CODE = 500,
+        ERROR_RESPONSE = 'Server Error';
+
+    beforeEach(function() {
+      xhr.expectGET('/CreditCard/123').respond(ERROR_CODE, ERROR_RESPONSE);
     });
-  });
 
+    it('should report error when non 2xx if error callback is not provided', function() {
+      CreditCard.get({id:123});
+      xhr.flush();
+      expect($xhrErr).toHaveBeenCalled();
+    });
+
+    it('should call the error callback if provided on non 2xx response', function() {
+      CreditCard.get({id:123}, noop, callback);
+      xhr.flush();
+      expect(callback).toHaveBeenCalledWith(500, ERROR_RESPONSE);
+      expect($xhrErr).not.toHaveBeenCalled();
+    });
+  });
 });
diff --git a/test/service/xhr.bulkSpec.js b/test/service/xhr.bulkSpec.js
index bc8d03f8..adcb61fa 100644
--- a/test/service/xhr.bulkSpec.js
+++ b/test/service/xhr.bulkSpec.js
@@ -4,13 +4,11 @@ describe('$xhr.bulk', function() {
   var scope, $browser, $browserXhr, $log, $xhrBulk, $xhrError, log;
 
   beforeEach(function(){
-    scope = angular.scope({}, angular.service, {
-      '$xhr.error': $xhrError = jasmine.createSpy('$xhr.error'),
-      '$log': $log = {}
-    });
+    scope = angular.scope({}, null, {'$xhr.error': $xhrError = jasmine.createSpy('$xhr.error')});
     $browser = scope.$service('$browser');
     $browserXhr = $browser.xhr;
     $xhrBulk = scope.$service('$xhr.bulk');
+    $log = scope.$service('$log');
     log = '';
   });
 
@@ -60,12 +58,29 @@ describe('$xhr.bulk', function() {
     $browserXhr.flush();
 
     expect($xhrError).toHaveBeenCalled();
-    var cb = $xhrError.mostRecentCall.args[0].callback;
+    var cb = $xhrError.mostRecentCall.args[0].success;
     expect(typeof cb).toEqual($function);
     expect($xhrError).toHaveBeenCalledWith(
-        {url:'/req1', method:'GET', data:null, callback:cb},
-        {status:404, response:'NotFound'});
+        {url: '/req1', method: 'GET', data: null, success: cb},
+        {status: 404, response: 'NotFound'});
 
     expect(log).toEqual('"second";DONE');
   });
+
+  it('should handle non 200 status code by calling error callback if provided', function() {
+    var callback = jasmine.createSpy('error');
+
+    $xhrBulk.urls['/'] = {match: /.*/};
+    $xhrBulk('GET', '/req1', null, noop, callback);
+
+    $browserXhr.expectPOST('/', {
+      requests:[{method: 'GET',  url: '/req1', data: null}]
+    }).respond([{status: 404, response: 'NotFound'}]);
+
+    $xhrBulk.flush();
+    $browserXhr.flush();
+
+    expect($xhrError).not.toHaveBeenCalled();
+    expect(callback).toHaveBeenCalledWith(404, 'NotFound');
+  });
 });
diff --git a/test/service/xhr.cacheSpec.js b/test/service/xhr.cacheSpec.js
index 905a9dae..f4654cd4 100644
--- a/test/service/xhr.cacheSpec.js
+++ b/test/service/xhr.cacheSpec.js
@@ -1,10 +1,10 @@
 'use strict';
 
 describe('$xhr.cache', function() {
-  var scope, $browser, $browserXhr, cache, log;
+  var scope, $browser, $browserXhr, $xhrErr, cache, log;
 
-  beforeEach(function(){
-    scope = angular.scope();
+  beforeEach(function() {
+    scope = angular.scope({}, null, {'$xhr.error': $xhrErr = jasmine.createSpy('$xhr.error')});
     $browser = scope.$service('$browser');
     $browserXhr = $browser.xhr;
     cache = scope.$service('$xhr.cache');
@@ -143,4 +143,36 @@ describe('$xhr.cache', function() {
     $browser.defer.flush();
     expect(evalSpy).toHaveBeenCalled();
   });
+
+  it('should call the error callback on error if provided', function() {
+    var errorSpy = jasmine.createSpy('error'),
+        successSpy = jasmine.createSpy('success');
+
+    $browserXhr.expectGET('/url').respond(500, 'error');
+
+    cache('GET', '/url', null, successSpy, errorSpy, false, true);
+    $browserXhr.flush();
+    expect(errorSpy).toHaveBeenCalledWith(500, 'error');
+    expect(successSpy).not.toHaveBeenCalled();
+
+    errorSpy.reset();
+    cache('GET', '/url', successSpy, errorSpy, false, true);
+    $browserXhr.flush();
+    expect(errorSpy).toHaveBeenCalledWith(500, 'error');
+    expect(successSpy).not.toHaveBeenCalled();
+  });
+
+  it('should call the $xhr.error on error if error callback not provided', function() {
+    var errorSpy = jasmine.createSpy('error'),
+        successSpy = jasmine.createSpy('success');
+
+    $browserXhr.expectGET('/url').respond(500, 'error');
+    cache('GET', '/url', null, successSpy, false, true);
+    $browserXhr.flush();
+
+    expect(successSpy).not.toHaveBeenCalled();
+    expect($xhrErr).toHaveBeenCalledWith(
+      {method: 'GET', url: '/url', data: null, success: successSpy},
+      {status: 500, body: 'error'});
+  });
 });
diff --git a/test/service/xhr.errorSpec.js b/test/service/xhr.errorSpec.js
index fdca93ec..d3af4565 100644
--- a/test/service/xhr.errorSpec.js
+++ b/test/service/xhr.errorSpec.js
@@ -29,10 +29,10 @@ describe('$xhr.error', function() {
     $browserXhr.expectPOST('/req', 'MyData').respond(500, 'MyError');
     $xhr('POST', '/req', 'MyData', callback);
     $browserXhr.flush();
-    var cb = $xhrError.mostRecentCall.args[0].callback;
+    var cb = $xhrError.mostRecentCall.args[0].success;
     expect(typeof cb).toEqual($function);
     expect($xhrError).toHaveBeenCalledWith(
-        {url:'/req', method:'POST', data:'MyData', callback:cb},
-        {status:500, body:'MyError'});
+        {url: '/req', method: 'POST', data: 'MyData', success: cb},
+        {status: 500, body: 'MyError'});
   });
 });
diff --git a/test/service/xhrSpec.js b/test/service/xhrSpec.js
index ed7cfc93..9f496535 100644
--- a/test/service/xhrSpec.js
+++ b/test/service/xhrSpec.js
@@ -1,12 +1,11 @@
 'use strict';
 
 describe('$xhr', function() {
-  var scope, $browser, $browserXhr, $log, $xhr, log;
+  var scope, $browser, $browserXhr, $log, $xhr, $xhrErr, log;
 
   beforeEach(function(){
-    scope = angular.scope({}, angular.service, { '$log': $log = {
-        error: dump
-    } });
+    var scope = angular.scope({}, null, {'$xhr.error': $xhrErr = jasmine.createSpy('xhr.error')});
+    $log = scope.$service('$log');
     $browser = scope.$service('$browser');
     $browserXhr = $browser.xhr;
     $xhr = scope.$service('$xhr');
@@ -57,12 +56,11 @@ describe('$xhr', function() {
 
 
   it('should handle exceptions in callback', function(){
-    $log.error = jasmine.createSpy('$log.error');
     $browserXhr.expectGET('/reqGET').respond('first');
     $xhr('GET', '/reqGET', null, function(){ throw "MyException"; });
     $browserXhr.flush();
 
-    expect($log.error).toHaveBeenCalledWith("MyException");
+    expect($log.error.logs.shift()).toContain('MyException');
   });
 
 
@@ -104,6 +102,38 @@ describe('$xhr', function() {
     expect(response).toEqual([1, 'abc', {foo:'bar'}]);
   });
 
+  it('should call $xhr.error on error if no error callback provided', function() {
+    var successSpy = jasmine.createSpy('success');
+
+    $browserXhr.expectGET('/url').respond(500, 'error');
+    $xhr('GET', '/url', null, successSpy);
+    $browserXhr.flush();
+
+    expect(successSpy).not.toHaveBeenCalled();
+    expect($xhrErr).toHaveBeenCalledWith(
+      {method: 'GET', url: '/url', data: null, success: successSpy},
+      {status: 500, body: 'error'}
+    );
+  });
+
+  it('should call the error callback on error if provided', function() {
+    var errorSpy = jasmine.createSpy('error'),
+        successSpy = jasmine.createSpy('success');
+
+    $browserXhr.expectGET('/url').respond(500, 'error');
+    $xhr('GET', '/url', null, successSpy, errorSpy);
+    $browserXhr.flush();
+
+    expect(errorSpy).toHaveBeenCalledWith(500, 'error');
+    expect(successSpy).not.toHaveBeenCalled();
+
+    errorSpy.reset();
+    $xhr('GET', '/url', successSpy, errorSpy);
+    $browserXhr.flush();
+
+    expect(errorSpy).toHaveBeenCalledWith(500, 'error');
+    expect(successSpy).not.toHaveBeenCalled();
+  });
 
   describe('http headers', function() {
 
-- 
cgit v1.2.3