diff options
| -rw-r--r-- | src/angular-mocks.js | 57 | ||||
| -rw-r--r-- | src/service/http.js | 680 | ||||
| -rw-r--r-- | src/service/httpBackend.js | 40 | ||||
| -rw-r--r-- | test/angular-mocksSpec.js | 288 | ||||
| -rw-r--r-- | test/service/httpBackendSpec.js | 23 | ||||
| -rw-r--r-- | test/service/httpSpec.js | 53 | ||||
| -rw-r--r-- | test/widgetsSpec.js | 9 | 
7 files changed, 533 insertions, 617 deletions
diff --git a/src/angular-mocks.js b/src/angular-mocks.js index 16be7038..b4a1cbbc 100644 --- a/src/angular-mocks.js +++ b/src/angular-mocks.js @@ -48,9 +48,7 @@ angular.module.ngMock.$BrowserProvider = function(){    };  };  angular.module.ngMock.$Browser = function() { -  var self = this, -      expectations = {}, -      requests = []; +  var self = this;    this.isMock = true;    self.$$url = "http://server"; @@ -590,6 +588,10 @@ angular.module.ngMock.dump = function(object){  /**   * @ngdoc object   * @name angular.module.ngMock.$httpBackend + * @describe + * Fake HTTP backend used by the $http service during testing. This implementation can be used to + * respond with static or dynamic responses via the `expect` and `when` apis and their shortcuts + * (`expectGET`, `whenPOST`, etc).   */  angular.module.ngMock.$HttpBackendProvider = function() {    this.$get = function() { @@ -598,7 +600,13 @@ angular.module.ngMock.$HttpBackendProvider = function() {          responses = [];      function createResponse(status, data, headers) { -      return angular.isNumber(status) ? [status, data, headers] : [200, status, data]; +      if (isFunction(status)) return status; + +      return function() { +        return angular.isNumber(status) +            ? [status, data, headers] +            : [200, status, data]; +      }      }      // TODO(vojta): change params to: method, url, data, headers, callback @@ -608,28 +616,29 @@ angular.module.ngMock.$HttpBackendProvider = function() {            wasExpected = false;        function prettyPrint(data) { -        if (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) -          return data; -        return angular.toJson(data); +        return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) +            ? data +            : angular.toJson(data);        }        if (expectation && expectation.match(method, url)) {          if (!expectation.matchData(data))            throw Error('Expected ' + expectation + ' with different data\n' + -              'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); +              'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT:      ' + data);          if (!expectation.matchHeaders(headers))            throw Error('Expected ' + expectation + ' with different headers\n' + -              'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + prettyPrint(headers)); +              'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT:      ' + prettyPrint(headers));          expectations.shift();          if (expectation.response) {            responses.push(function() { -            xhr.$$headers = expectation.response[2]; -            callback(expectation.response[0], expectation.response[1]); +            var response = expectation.response(method, url, data, headers); +            xhr.$$respHeaders = response[2]; +            callback(response[0], response[1], xhr.getAllResponseHeaders());            }); -          return method == 'JSONP' ? undefined : xhr; +          return;          }          wasExpected = true;        } @@ -639,12 +648,11 @@ angular.module.ngMock.$HttpBackendProvider = function() {          if (definition.match(method, url, data, headers || {})) {            if (!definition.response) throw Error('No response defined !');            responses.push(function() { -            var response = angular.isFunction(definition.response) ? -                           definition.response(method, url, data, headers) : definition.response; -            xhr.$$headers = response[2]; -            callback(response[0], response[1]); +            var response = definition.response(method, url, data, headers); +            xhr.$$respHeaders = response[2]; +            callback(response[0], response[1], xhr.getAllResponseHeaders());            }); -          return method == 'JSONP' ? undefined : xhr; +          return;          }        }        throw wasExpected ? @@ -658,7 +666,7 @@ angular.module.ngMock.$HttpBackendProvider = function() {        definitions.push(definition);        return {          respond: function(status, data, headers) { -          definition.response = angular.isFunction(status) ? status : createResponse(status, data, headers); +          definition.response = createResponse(status, data, headers);          }        };      }; @@ -756,7 +764,8 @@ function MockXhr() {      this.$$method = method;      this.$$url = url;      this.$$async = async; -    this.$$headers = {}; +    this.$$reqHeaders = {}; +    this.$$respHeaders = {};    };    this.send = function(data) { @@ -764,20 +773,20 @@ function MockXhr() {    };    this.setRequestHeader = function(key, value) { -    this.$$headers[key] = value; +    this.$$reqHeaders[key] = value;    };    this.getResponseHeader = function(name) {      // the lookup must be case insensitive, that's why we try two quick lookups and full scan at last -    var header = this.$$headers[name]; +    var header = this.$$respHeaders[name];      if (header) return header;      name = angular.lowercase(name); -    header = this.$$headers[name]; +    header = this.$$respHeaders[name];      if (header) return header;      header = undefined; -    angular.forEach(this.$$headers, function(headerVal, headerName) { +    angular.forEach(this.$$respHeaders, function(headerVal, headerName) {        if (!header && angular.lowercase(headerName) == name) header = headerVal;      });      return header; @@ -786,7 +795,7 @@ function MockXhr() {    this.getAllResponseHeaders = function() {      var lines = []; -    angular.forEach(this.$$headers, function(value, key) { +    angular.forEach(this.$$respHeaders, function(value, key) {        lines.push(key + ': ' + value);      });      return lines.join('\n'); diff --git a/src/service/http.js b/src/service/http.js index e6a42b65..bd8e6e65 100644 --- a/src/service/http.js +++ b/src/service/http.js @@ -4,11 +4,13 @@   * Parse headers into key value object   *   * @param {string} headers Raw headers as a string - * @returns {Object} Parsed headers as key valu object + * @returns {Object} Parsed headers as key value object   */  function parseHeaders(headers) {    var parsed = {}, key, val, i; +  if (!headers) return parsed; +    forEach(headers.split('\n'), function(line) {      i = line.indexOf(':');      key = lowercase(trim(line.substr(0, i))); @@ -26,6 +28,34 @@ function parseHeaders(headers) {    return parsed;  } + +/** + * Returns a function that provides access to parsed headers. + * + * Headers are lazy parsed when first requested. + * @see parseHeaders + * + * @param {(string|Object)} headers Headers to provide access to. + * @returns {function(string=)} Returns a getter function which if called with: + * + *   - if called with single an argument returns a single header value or null + *   - if called with no arguments returns an object containing all headers. + */ +function headersGetter(headersString) { +  var headers = isObject(headersString) ? headersString : undefined; + +  return function(name) { +    if (!headers) headers =  parseHeaders(headersString); + +    if (name) { +      return headers[lowercase(name)] || null; +    } + +    return headers; +  }; +} + +  /**   * Chain all given functions   * @@ -36,7 +66,7 @@ function parseHeaders(headers) {   * @param {*=} param Optional parameter to be passed to all transform functions.   * @returns {*} Transformed data.   */ -function transform(data, fns, param) { +function transformData(data, fns, param) {    if (isFunction(fns))      return fns(data); @@ -48,13 +78,18 @@ function transform(data, fns, param) {  } +function isSuccess(status) { +  return 200 <= status && status < 300; +} + +  function $HttpProvider() {    var JSON_START = /^\s*(\[|\{[^\{])/,        JSON_END = /[\}\]]\s*$/,        PROTECTION_PREFIX = /^\)\]\}',?\n/;    var $config = this.defaults = { -    // transform in-coming reponse data +    // transform incoming response data      transformResponse: function(data) {        if (isString(data)) {          // strip json vulnerability protection prefix @@ -65,7 +100,7 @@ function $HttpProvider() {        return data;      }, -    // transform out-going request data +    // transform outgoing request data      transformRequest: function(d) {        return isObject(d) ? toJson(d) : d;      }, @@ -81,431 +116,328 @@ function $HttpProvider() {      }    }; -  var responseInterceptors = this.responseInterceptors = []; +  var providerResponseInterceptors = this.responseInterceptors = [];    this.$get = ['$httpBackend', '$browser', '$exceptionHandler', '$cacheFactory', '$rootScope', '$q', '$injector',        function($httpBackend, $browser, $exceptionHandler, $cacheFactory, $rootScope, $q, $injector) { -  var defaultCache = $cacheFactory('$http'); +    var defaultCache = $cacheFactory('$http'), +        responseInterceptors = []; -  forEach(responseInterceptors, function(interceptor, index) { -    if (isString(interceptor)) { -      responseInterceptors[index] = $injector.get(interceptor); -    } -  }); +    forEach(providerResponseInterceptors, function(interceptor) { +      responseInterceptors.push(isString(interceptor) ? $injector.get(interceptor) : interceptor); +    }); -  /** -   * @ngdoc function -   * @name angular.module.ng.$http -   * @requires $httpBacked -   * @requires $browser -   * @requires $exceptionHandler -   * @requires $cacheFactory -   * -   * @param {object} config Object describing the request to be made and how it should be processed. -   *    The object has following properties: -   * -   *    - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) -   *    - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. -   *    - **data** – `{string|Object}` – Data to be sent as the request message data. -   *    - **headers** – `{Object}` – Map of strings representing HTTP headers to send to the server. -   *    - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the -   *      GET request, otherwise if a cache instance built with $cacheFactory, this cache will be -   *      used for caching. -   * -   * @returns {HttpPromise} Returns a promise object with the standard `then` method and two http -   *   specific methods: `success` and `error`. The `then` method takes two arguments a success and -   *   an error callback which will be called with a response object. The `success` and `error` -   *   methods take a single argument - a function that will be called when the request succeeds or -   *   fails respectively. The arguments passed into these functions are destructured representation -   *   of the response object passed into the `then` method. The response object has these -   *   properties: -   * -   *   - **data** – `{string|Object}` – The response body transformed with the transform functions. -   *   - **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. -   * -   * @property {Array.<Object>} pendingRequests Array of config objects for pending requests. -   *   This is primarily meant to be used for debugging purposes. -   * -   * @description -   * $http is a service through which XHR and JSONP requests can be made. -   */ -  function $http(config) { -    var req = new XhrFuture().send(config), -        deferredResp = $q.defer(), -        promise = deferredResp.promise; - -    forEach(responseInterceptors, function(interceptor) { -      promise = interceptor(promise); -    }); +    /** +     * @ngdoc function +     * @name angular.module.ng.$http +     * @requires $httpBacked +     * @requires $browser +     * @requires $exceptionHandler //TODO(i): still needed? +     * @requires $cacheFactory +     * +     * @param {object} config Object describing the request to be made and how it should be processed. +     *    The object has following properties: +     * +     *    - **method** – `{string}` – HTTP method (e.g. 'GET', 'POST', etc) +     *    - **url** – `{string}` – Absolute or relative URL of the resource that is being requested. +     *    - **data** – `{string|Object}` – Data to be sent as the request message data. +     *    - **headers** – `{Object}` – Map of strings representing HTTP headers to send to the server. +     *    - **cache** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the +     *      GET request, otherwise if a cache instance built with $cacheFactory, this cache will be +     *      used for caching. +     * +     * @returns {HttpPromise} Returns a promise object with the standard `then` method and two http +     *   specific methods: `success` and `error`. The `then` method takes two arguments a success and +     *   an error callback which will be called with a response object. The `success` and `error` +     *   methods take a single argument - a function that will be called when the request succeeds or +     *   fails respectively. The arguments passed into these functions are destructured representation +     *   of the response object passed into the `then` method. The response object has these +     *   properties: +     * +     *   - **data** – `{string|Object}` – The response body transformed with the transform functions. +     *   - **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. +     * +     * @property {Array.<Object>} pendingRequests Array of config objects for pending requests. +     *   This is primarily meant to be used for debugging purposes. +     * +     * @description +     * $http is a service through which XHR and JSONP requests can be made. +     */ +    function $http(config) { +      config.method = uppercase(config.method); -    promise.success = function(fn) { -      promise.then(function(response) { -        fn(response.data, response.status, response.headers, config); -      }); -      return promise; -    }; +      var reqTransformFn = config.transformRequest || $config.transformRequest, +          respTransformFn = config.transformResponse || $config.transformResponse, +          reqData = transformData(config.data, reqTransformFn), +          defHeaders = $config.headers, +          reqHeaders = extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']}, +              defHeaders.common, defHeaders[lowercase(config.method)], config.headers), +          promise; -    promise.error = function(fn) { -      promise.then(null, function(response) { -        fn(response.data, response.status, response.headers, config); -      }); -      return promise; -    }; -    req.on('success', function(data, status, headers) { -      deferredResp.resolve({data: data, status: status, headers: headers, config: config}); -    }).on('error', function(data, status, headers) { -      deferredResp.reject({data: data, status: status, headers: headers, config: config}); -    }); +      // send request +      promise = sendReq(config, reqData, reqHeaders); + -    return promise; -  } - -  $http.pendingRequests = []; - -  /** -   * @ngdoc method -   * @name angular.module.ng.$http#get -   * @methodOf angular.module.ng.$http -   * -   * @description -   * Shortcut method to perform `GET` request -   * -   * @param {string} url Relative or absolute URL specifying the destination of the request -   * @param {Object=} config Optional configuration object -   * @returns {HttpPromise} Future object -   */ - -  /** -   * @ngdoc method -   * @name angular.module.ng.$http#delete -   * @methodOf angular.module.ng.$http -   * -   * @description -   * Shortcut method to perform `DELETE` request -   * -   * @param {string} url Relative or absolute URL specifying the destination of the request -   * @param {Object=} config Optional configuration object -   * @returns {HttpPromise} Future object -   */ - -  /** -   * @ngdoc method -   * @name angular.module.ng.$http#head -   * @methodOf angular.module.ng.$http -   * -   * @description -   * Shortcut method to perform `HEAD` request -   * -   * @param {string} url Relative or absolute URL specifying the destination of the request -   * @param {Object=} config Optional configuration object -   * @returns {XhrFuture} Future object -   */ - -  /** -   * @ngdoc method -   * @name angular.module.ng.$http#patch -   * @methodOf angular.module.ng.$http -   * -   * @description -   * Shortcut method to perform `PATCH` request -   * -   * @param {string} url Relative or absolute URL specifying the destination of the request -   * @param {Object=} config Optional configuration object -   * @returns {HttpPromise} Future object -   */ - -  /** -   * @ngdoc method -   * @name angular.module.ng.$http#jsonp -   * @methodOf angular.module.ng.$http -   * -   * @description -   * Shortcut method to perform `JSONP` request -   * -   * @param {string} url Relative or absolute URL specifying the destination of the request. -   *                     Should contain `JSON_CALLBACK` string. -   * @param {Object=} config Optional configuration object -   * @returns {XhrFuture} Future object -   */ -  createShortMethods('get', 'delete', 'head', 'patch', 'jsonp'); - -  /** -   * @ngdoc method -   * @name angular.module.ng.$http#post -   * @methodOf angular.module.ng.$http -   * -   * @description -   * Shortcut method to perform `POST` request -   * -   * @param {string} url Relative or absolute URL specifying the destination of the request -   * @param {*} data Request content -   * @param {Object=} config Optional configuration object -   * @returns {HttpPromise} Future object -   */ - -  /** -   * @ngdoc method -   * @name angular.module.ng.$http#put -   * @methodOf angular.module.ng.$http -   * -   * @description -   * Shortcut method to perform `PUT` request -   * -   * @param {string} url Relative or absolute URL specifying the destination of the request -   * @param {*} data Request content -   * @param {Object=} config Optional configuration object -   * @returns {XhrFuture} Future object -   */ -  createShortMethodsWithData('post', 'put'); - -  return $http; - -  function createShortMethods(names) { -    forEach(arguments, function(name) { -      $http[name] = function(url, config) { -        return $http(extend(config || {}, { -          method: name, -          url: url -        })); +      // transform future response +      promise = promise.then(transformResponse, transformResponse); + +      // apply interceptors +      forEach(responseInterceptors, function(interceptor) { +        promise = interceptor(promise); +      }); + +      promise.success = function(fn) { +        promise.then(function(response) { +          fn(response.data, response.status, response.headers, config); +        }); +        return promise;        }; -    }); -  } - -  function createShortMethodsWithData(name) { -    forEach(arguments, function(name) { -      $http[name] = function(url, data, config) { -        return $http(extend(config || {}, { -          method: name, -          url: url, -          data: data -        })); + +      promise.error = function(fn) { +        promise.then(null, function(response) { +          fn(response.data, response.status, response.headers, config); +        }); +        return promise;        }; -    }); -  } - -  /** -   * Represents Request object, returned by $http() -   * -   * !!! ACCESSES CLOSURE VARS: -   * $httpBackend, $browser, $config, $log, $rootScope, defaultCache, $http.pendingRequests -   */ -  function XhrFuture() { -    var rawRequest, parsedHeaders, -        cfg = {}, callbacks = [], -        defHeaders = $config.headers, -        self = this; + +      return promise; + +      function transformResponse(response) { +        // make a copy since the response must be cacheable +        var resp = extend({}, response, { +          data: transformData(response.data, respTransformFn, response.headers) +        }); +        return (isSuccess(response.status)) +          ? resp +          : $q.reject(resp); +      } +    } + +    $http.pendingRequests = [];      /** -     * Callback registered to $httpBackend(): -     *  - caches the response if desired -     *  - calls fireCallbacks() -     *  - clears the reference to raw request object +     * @ngdoc method +     * @name angular.module.ng.$http#get +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `GET` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {Object=} config Optional configuration object +     * @returns {HttpPromise} Future object       */ -    function done(status, response) { -      // aborted request or jsonp -      if (!rawRequest) parsedHeaders = {}; - -      if (cfg.cache && cfg.method == 'GET') { -        var cache = isObject(cfg.cache) && cfg.cache || defaultCache; -        if (200 <= status && status < 300) { -          parsedHeaders = parsedHeaders || parseHeaders(rawRequest.getAllResponseHeaders()); -          cache.put(cfg.url, [status, response, parsedHeaders]); -        } else { -          // remove future object from cache -          cache.remove(cfg.url); -        } -      } -      fireCallbacks(response, status); -      // TODO(i): we can't null the rawRequest because we might need to be able to call -      // rawRequest.getAllResponseHeaders from a promise -      // rawRequest = null; -    } +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#delete +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `DELETE` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {Object=} config Optional configuration object +     * @returns {HttpPromise} Future object +     */      /** -     * Fire all registered callbacks for given status code +     * @ngdoc method +     * @name angular.module.ng.$http#head +     * @methodOf angular.module.ng.$http       * -     * This method when: -     *  - serving response from real request -     *  - serving response from cache +     * @description +     * Shortcut method to perform `HEAD` request       * -     * It does: -     *  - transform the response -     *  - call proper callbacks -     *  - log errors -     *  - apply the $scope -     *  - clear parsed headers +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {Object=} config Optional configuration object +     * @returns {XhrFuture} Future object       */ -    function fireCallbacks(response, status) { -      var strStatus = status + ''; - -      // transform the response -      response = transform(response, cfg.transformResponse || $config.transformResponse, rawRequest); - -      var idx; // remove from pending requests -      if ((idx = indexOf($http.pendingRequests, cfg)) !== -1) -        $http.pendingRequests.splice(idx, 1); - -      // normalize internal statuses to 0 -      status = Math.max(status, 0); -      forEach(callbacks, function(callback) { -        if (callback.regexp.test(strStatus)) { -          try { -            // use local var to call it without context -            var fn = callback.fn; -            fn(response, status, headers); -          } catch(e) { -            $exceptionHandler(e); -          } -        } -      }); -      $rootScope.$apply(); -      parsedHeaders = null; -    } +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#patch +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `PATCH` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {Object=} config Optional configuration object +     * @returns {HttpPromise} Future object +     */      /** -     * This is the third argument in any user callback -     * @see parseHeaders +     * @ngdoc method +     * @name angular.module.ng.$http#jsonp +     * @methodOf angular.module.ng.$http       * -     * Return single header value or all headers parsed as object. -     * Headers all lazy parsed when first requested. +     * @description +     * Shortcut method to perform `JSONP` request       * -     * @param {string=} name Name of header -     * @returns {string|Object} +     * @param {string} url Relative or absolute URL specifying the destination of the request. +     *                     Should contain `JSON_CALLBACK` string. +     * @param {Object=} config Optional configuration object +     * @returns {XhrFuture} Future object       */ -    function headers(name) { -      if (name) { -        return parsedHeaders ? -          parsedHeaders[lowercase(name)] || null : -          rawRequest.getResponseHeader(name); -      } +    createShortMethods('get', 'delete', 'head', 'patch', 'jsonp'); -      parsedHeaders = parsedHeaders || parseHeaders(rawRequest.getAllResponseHeaders()); +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#post +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `POST` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {*} data Request content +     * @param {Object=} config Optional configuration object +     * @returns {HttpPromise} Future object +     */ -      return parsedHeaders; +    /** +     * @ngdoc method +     * @name angular.module.ng.$http#put +     * @methodOf angular.module.ng.$http +     * +     * @description +     * Shortcut method to perform `PUT` request +     * +     * @param {string} url Relative or absolute URL specifying the destination of the request +     * @param {*} data Request content +     * @param {Object=} config Optional configuration object +     * @returns {XhrFuture} Future object +     */ +    createShortMethodsWithData('post', 'put'); + + +    return $http; + + +    function createShortMethods(names) { +      forEach(arguments, function(name) { +        $http[name] = function(url, config) { +          return $http(extend(config || {}, { +            method: name, +            url: url +          })); +        }; +      });      } + +    function createShortMethodsWithData(name) { +      forEach(arguments, function(name) { +        $http[name] = function(url, data, config) { +          return $http(extend(config || {}, { +            method: name, +            url: url, +            data: data +          })); +        }; +      }); +    } + +      /** -     * Retry the request +     * Makes the request       * -     * @param {Object=} config Optional config object to extend the original configuration -     * @returns {HttpPromise} +     * !!! ACCESSES CLOSURE VARS: +     * $httpBackend, $config, $log, $rootScope, defaultCache, $http.pendingRequests       */ -    this.retry = function(config) { -      if (rawRequest) throw 'Can not retry request. Abort pending request first.'; +    function sendReq(config, reqData, reqHeaders) { +      var deferred = $q.defer(), +          promise = deferred.promise, +          cache, +          cachedResp; -      extend(cfg, config); -      cfg.method = uppercase(cfg.method); +      $http.pendingRequests.push(config); +      promise.then(removePendingReq, removePendingReq); -      var data = transform(cfg.data, cfg.transformRequest || $config.transformRequest), -          headers = extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']}, -                           defHeaders.common, defHeaders[lowercase(cfg.method)], cfg.headers); -      var cache = isObject(cfg.cache) && cfg.cache || defaultCache, -          fromCache; +      if (config.cache && config.method == 'GET') { +        cache = isObject(config.cache) ? config.cache : defaultCache; +      } -      if (cfg.cache && cfg.method == 'GET') { -        fromCache = cache.get(cfg.url); -        if (fromCache) { -          if (fromCache instanceof XhrFuture) { -            // cached request has already been sent, but there is no reponse yet, +      if (cache) { +        cachedResp = cache.get(config.url); +        if (cachedResp) { +          if (cachedResp.then) { +            // cached request has already been sent, but there is no response yet,              // we need to register callback and fire callbacks when the request is back              // note, we have to get the values from cache and perform transformations on them,              // as the configurations don't have to be same -            fromCache.on('always', function() { -              var requestFromCache = cache.get(cfg.url); -              parsedHeaders = requestFromCache[2]; -              fireCallbacks(requestFromCache[1], requestFromCache[0]); -            }); +            cachedResp.then(removePendingReq, removePendingReq); +            return cachedResp;            } else { -            // serving from cache - still needs to be async -            $browser.defer(function() { -              parsedHeaders = fromCache[2]; -              fireCallbacks(fromCache[1], fromCache[0]); -            }); +            // serving from cache +            if (isArray(cachedResp)) { +              resolvePromise(cachedResp[1], cachedResp[0], copy(cachedResp[2])); +            } else { +              resolvePromise(cachedResp, 200, {}); +            }            }          } else { -          // put future object into cache -          cache.put(cfg.url, self); +          // put the promise for the non-transformed response into cache as a placeholder +          cache.put(config.url, promise);          }        } -      // really send the request -      if (!cfg.cache || cfg.method !== 'GET' || !fromCache) { -        rawRequest = $httpBackend(cfg.method, cfg.url, data, done, headers, cfg.timeout); +      // if we won't have the response in cache, send the request to the backend +      if (!cachedResp) { +        $httpBackend(config.method, config.url, reqData, done, reqHeaders, config.timeout);        } -      $http.pendingRequests.push(cfg); -      return self; -    }; +      return promise; -    // just alias so that in stack trace we can see send() instead of retry() -    this.send = this.retry; -    /** -     * Abort the request -     */ -    this.abort = function() { -      if (rawRequest) { -        rawRequest.abort(); +      /** +       * Callback registered to $httpBackend(): +       *  - caches the response if desired +       *  - resolves the raw $http promise +       *  - calls $apply +       */ +      function done(status, response, headersString) { +        if (cache) { +          if (isSuccess(status)) { +            cache.put(config.url, [status, response, parseHeaders(headersString)]); +          } else { +            // remove promise from the cache +            cache.remove(config.url); +          } +        } + +        resolvePromise(response, status, headersString); +        $rootScope.$apply();        } -      return this; -    }; -    /** -     * Register a callback function based on status code -     * Note: all matched callbacks will be called, preserving registered order ! -     * -     * Internal statuses: -     *  `-2` = jsonp error -     *  `-1` = timeout -     *   `0` = aborted -     * -     * @example -     *   .on('2xx', function(){}); -     *   .on('2x1', function(){}); -     *   .on('404', function(){}); -     *   .on('20x,3xx', function(){}); -     *   .on('success', function(){}); -     *   .on('error', function(){}); -     *   .on('always', function(){}); -     *   .on('timeout', function(){}); -     *   .on('abort', function(){}); -     * -     * @param {string} pattern Status code pattern with "x" for any number -     * @param {function(*, number, function)} callback Function to be called when response arrives -     * @returns {XhrFuture} -     */ -    this.on = function(pattern, callback) { -      var alias = { -        success: '2xx', -        error: '-2,-1,0,4xx,5xx', -        always: 'xxx,xx,x', -        timeout: '-1', -        abort: '0' -      }; -      callbacks.push({ -        fn: callback, -        // create regexp from given pattern -        regexp: new RegExp('^(' + (alias[pattern] || pattern).replace(/,/g, '|'). -                                                              replace(/x/g, '.') + ')$') -      }); +      /** +       * Resolves the raw $http promise. +       */ +      function resolvePromise(response, status, headers) { +        // normalize internal statuses to 0 +        status = Math.max(status, 0); -      return this; -    }; +        (isSuccess(status) ? deferred.resolve : deferred.reject)({ +          data: response, +          status: status, +          headers: headersGetter(headers), +          config: config +        }); +      } -    /** -     * Configuration object of the request -     */ -    this.config = cfg; -  } -}]; -} +      function removePendingReq() { +        var idx = indexOf($http.pendingRequests, config); +        if (idx !== -1) $http.pendingRequests.splice(idx, 1); +      } +    } +  }]; +} diff --git a/src/service/httpBackend.js b/src/service/httpBackend.js index c64519d9..c3114814 100644 --- a/src/service/httpBackend.js +++ b/src/service/httpBackend.js @@ -14,6 +14,11 @@ var XHR = window.XMLHttpRequest || function() {   * @requires $document   *   * @description + * HTTP backend used by the {@link angular.module.ng.$http service} that delegates to + * XMLHttpRequest object. + * + * During testing this implementation is swapped with {@link angular.module.ngMock.$httpBackend mock + * $httpBackend} which can be trained with responses.   */  function $HttpBackendProvider() {    this.$get = ['$browser', '$window', '$document', function($browser, $window, $document) { @@ -46,24 +51,21 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, body, locati        var xhr = new XHR();        xhr.open(method, url, true);        forEach(headers, function(value, key) { -          if (value) xhr.setRequestHeader(key, value); +        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); -          } -        }; -      } +      // In IE6 and 7, this might be called synchronously when xhr.send below is called and the +      // response is in the cache +      xhr.onreadystatechange = function() { +        if (xhr.readyState == 4) { +          completeRequest( +              callback, status || xhr.status, xhr.responseText, xhr.getAllResponseHeaders()); +        } +      }; + +      xhr.send(post || '');        if (timeout > 0) {          $browserDefer(function() { @@ -71,23 +73,21 @@ function createHttpBackend($browser, XHR, $browserDefer, callbacks, body, locati            xhr.abort();          }, timeout);        } - -      return xhr;      } -    function completeRequest(callback, status, response) { + +    function completeRequest(callback, status, response, headersString) {        // URL_MATCH is defined in src/service/location.js        var protocol = (url.match(URL_MATCH) || ['', locationProtocol])[1];        // fix status code for file protocol (it's always 0) -      status = protocol == 'file' ? (response ? 200 : 404) : status; +      status = (protocol == 'file') ? (response ? 200 : 404) : status;        // normalize IE bug (http://bugs.jquery.com/ticket/1450)        status = status == 1223 ? 204 : status; -      callback(status, response); +      callback(status, response, headersString);        $browser.$$completeOutstandingRequest(noop);      }    };  } - diff --git a/test/angular-mocksSpec.js b/test/angular-mocksSpec.js index a8328f41..88e6a590 100644 --- a/test/angular-mocksSpec.js +++ b/test/angular-mocksSpec.js @@ -465,39 +465,6 @@ describe('mocks', function() {      }); -    it('should expose given headers', function() { -      hb.when('GET', '/u1').respond(200, null, {'X-Fake': 'Header', 'Content-Type': 'application/json'}); -      var xhr = hb('GET', '/u1', null, noop, {}); -      hb.flush(); -      expect(xhr.getResponseHeader('X-Fake')).toBe('Header'); -      expect(xhr.getAllResponseHeaders()).toBe('X-Fake: Header\nContent-Type: application/json'); -    }); - - -    it('should normalize when header name case when accessed via getResponseHeader', function() { -      hb.when('GET', '/u1').respond(200, null, {'X-Fake': 'Header', -                                                'Content-Type': 'application/json', -                                                'Location': '/foo'}); -      var xhr = hb('GET', '/u1', null, noop, {}); -      hb.flush(); -      expect(xhr.getResponseHeader('x-fAKE')).toBe('Header'); -      expect(xhr.getResponseHeader('content-type')).toBe('application/json'); -      expect(xhr.getResponseHeader('Location')).toBe('/foo'); -    }); - - -    it('should normalize expect header name case when accessed via getResponseHeader', function() { -      hb.expect('GET', '/u1').respond(200, null, {'X-Fake': 'Header', -                                                'Content-Type': 'application/json', -                                                'Location': '/foo'}); -      var xhr = hb('GET', '/u1', null, noop, {}); -      hb.flush(); -      expect(xhr.getResponseHeader('x-fAKE')).toBe('Header'); -      expect(xhr.getResponseHeader('content-type')).toBe('application/json'); -      expect(xhr.getResponseHeader('Location')).toBe('/foo'); -    }); - -      it('should preserve the order of requests', function() {        hb.when('GET', '/url1').respond(200, 'first');        hb.when('GET', '/url2').respond(201, 'second'); @@ -508,186 +475,179 @@ describe('mocks', 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', '']);      }); -    it('respond() should take function', function() { -      hb.when('GET', '/some').respond(function(m, u, d, h) { -        return [301, m + u + ';' + d + ';a=' + h.a, {'Connection': 'keep-alive'}]; -      }); - -      var xhr = hb('GET', '/some', 'data', callback, {a: 'b'}); -      hb.flush(); +    describe('respond()', function() { +      it('should take values', function() { +        hb.expect('GET', '/url1').respond(200, 'first', {'header': 'val'}); +        hb('GET', '/url1', undefined, callback); +        hb.flush(); -      expect(callback).toHaveBeenCalledOnce(); -      expect(callback.mostRecentCall.args[0]).toBe(301); -      expect(callback.mostRecentCall.args[1]).toBe('GET/some;data;a=b'); -      expect(xhr.getResponseHeader('Connection')).toBe('keep-alive'); -    }); +        expect(callback).toHaveBeenCalledOnceWith(200, 'first', 'header: val'); +      }); +      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'}]; +        }); -    it('expect() should require specified order', function() { -      hb.expect('GET', '/url1').respond(200, ''); -      hb.expect('GET', '/url2').respond(200, ''); +        hb('GET', '/some', 'data', callback, {a: 'b'}); +        hb.flush(); -      expect(function() { -        hb('GET', '/url2', null, noop, {}); -      }).toThrow('Unexpected request: GET /url2\nExpected GET /url1'); -    }); +        expect(callback).toHaveBeenCalledOnceWith(301, 'GET/some;data;a=b', 'Connection: keep-alive'); +      }); +      it('should default status code to 200', function() { +        callback.andCallFake(function(status, response) { +          expect(status).toBe(200); +          expect(response).toBe('some-data'); +        }); -    it('expect() should have precendence over when()', function() { -      callback.andCallFake(function(status, response) { -        expect(status).toBe(300); -        expect(response).toBe('expect'); +        hb.expect('GET', '/url1').respond('some-data'); +        hb.expect('GET', '/url2').respond('some-data', {'X-Header': 'true'}); +        hb('GET', '/url1', null, callback); +        hb('GET', '/url2', null, callback); +        hb.flush(); +        expect(callback).toHaveBeenCalled(); +        expect(callback.callCount).toBe(2);        }); -      hb.when('GET', '/url').respond(200, 'when'); -      hb.expect('GET', '/url').respond(300, 'expect'); -      hb('GET', '/url', null, callback, {}); -      hb.flush(); -      expect(callback).toHaveBeenCalledOnce(); -    }); +      it('should default response headers to ""', function() { +        hb.expect('GET', '/url1').respond(200, 'first'); +        hb.expect('GET', '/url2').respond('second'); +        hb('GET', '/url1', null, callback); +        hb('GET', '/url2', null, callback); -    it ('should throw exception when only headers differes from expectation', function() { -      hb.when('GET').respond(200, '', {}); -      hb.expect('GET', '/match', undefined, {'Content-Type': 'application/json'}); +        hb.flush(); -      expect(function() { -        hb('GET', '/match', null, noop, {}); -      }).toThrow('Expected GET /match with different headers\n' + -                 'EXPECTED: {"Content-Type":"application/json"}\nGOT: {}'); +        expect(callback.callCount).toBe(2); +        expect(callback.argsForCall[0]).toEqual([200, 'first', '']); +        expect(callback.argsForCall[1]).toEqual([200, 'second', '']); +      });      }); -    it ('should throw exception when only data differes from expectation', function() { -      hb.when('GET').respond(200, '', {}); -      hb.expect('GET', '/match', 'some-data'); - -      expect(function() { -        hb('GET', '/match', 'different', noop, {}); -      }).toThrow('Expected GET /match with different data\n' + -                 'EXPECTED: some-data\nGOT: different'); -    }); - +    describe('expect()', function() { +      it('should require specified order', function() { +        hb.expect('GET', '/url1').respond(200, ''); +        hb.expect('GET', '/url2').respond(200, ''); -    it('expect() should without respond() and use respond()', function() { -      callback.andCallFake(function(status, response) { -        expect(status).toBe(201); -        expect(response).toBe('data'); +        expect(function() { +          hb('GET', '/url2', null, noop, {}); +        }).toThrow('Unexpected request: GET /url2\nExpected GET /url1');        }); -      hb.when('GET', '/some').respond(201, 'data'); -      hb.expect('GET', '/some'); -      hb('GET', '/some', null, callback); -      hb.flush(); -      expect(callback).toHaveBeenCalled(); -      expect(function() { hb.verifyNoOutstandingExpectation(); }).not.toThrow(); -    }); +      it('should have precedence over when()', function() { +        callback.andCallFake(function(status, response) { +          expect(status).toBe(300); +          expect(response).toBe('expect'); +        }); +        hb.when('GET', '/url').respond(200, 'when'); +        hb.expect('GET', '/url').respond(300, 'expect'); -    it('flush() should flush requests fired during callbacks', function() { -      hb.when('GET').respond(200, ''); -      hb('GET', '/some', null, function() { -        hb('GET', '/other', null, callback); +        hb('GET', '/url', null, callback, {}); +        hb.flush(); +        expect(callback).toHaveBeenCalledOnce();        }); -      hb.flush(); -      expect(callback).toHaveBeenCalled(); -    }); - -    it('flush() should flush given number of pending requests', function() { -      hb.when('GET').respond(200, ''); -      hb('GET', '/some', null, callback); -      hb('GET', '/some', null, callback); -      hb('GET', '/some', null, callback); +      it ('should throw exception when only headers differs from expectation', function() { +        hb.when('GET').respond(200, '', {}); +        hb.expect('GET', '/match', undefined, {'Content-Type': 'application/json'}); -      hb.flush(2); -      expect(callback).toHaveBeenCalled(); -      expect(callback.callCount).toBe(2); -    }); +        expect(function() { +          hb('GET', '/match', null, noop, {}); +        }).toThrow('Expected GET /match with different headers\n' + +                   'EXPECTED: {"Content-Type":"application/json"}\nGOT:      {}'); +      }); -    it('flush() should throw exception when flushing more requests than pending', function() { -      hb.when('GET').respond(200, ''); -      hb('GET', '/url', null, callback); +      it ('should throw exception when only data differs from expectation', function() { +        hb.when('GET').respond(200, '', {}); +        hb.expect('GET', '/match', 'some-data'); -      expect(function() {hb.flush(2);}).toThrow('No more pending request to flush !'); -      expect(callback).toHaveBeenCalledOnce(); -    }); +        expect(function() { +          hb('GET', '/match', 'different', noop, {}); +        }).toThrow('Expected GET /match with different data\n' + +                   'EXPECTED: some-data\nGOT:      different'); +      }); -    it('(flush) should throw exception when no request to flush', function() { -      expect(function() {hb.flush();}).toThrow('No pending request to flush !'); +      it("should use when's respond() when no expect() respond is defined", function() { +        callback.andCallFake(function(status, response) { +          expect(status).toBe(201); +          expect(response).toBe('data'); +        }); -      hb.when('GET').respond(200, ''); -      hb('GET', '/some', null, callback); -      hb.flush(); +        hb.when('GET', '/some').respond(201, 'data'); +        hb.expect('GET', '/some'); +        hb('GET', '/some', null, callback); +        hb.flush(); -      expect(function() {hb.flush();}).toThrow('No pending request to flush !'); +        expect(callback).toHaveBeenCalled(); +        expect(function() { hb.verifyNoOutstandingExpectation(); }).not.toThrow(); +      });      }); -    it('(flush) should throw exception if not all expectations satasfied', function() { -      hb.expect('GET', '/url1').respond(); -      hb.expect('GET', '/url2').respond(); +    describe('flush()', function() { +      it('flush() should flush requests fired during callbacks', function() { +        hb.when('GET').respond(200, ''); +        hb('GET', '/some', null, function() { +          hb('GET', '/other', null, callback); +        }); -      hb('GET', '/url1', null, angular.noop); -      expect(function() {hb.flush();}).toThrow('Unsatisfied requests: GET /url2'); -    }); +        hb.flush(); +        expect(callback).toHaveBeenCalled(); +      }); -    it('respond() should set default status 200 if not defined', function() { -      callback.andCallFake(function(status, response) { -        expect(status).toBe(200); -        expect(response).toBe('some-data'); +      it('should flush given number of pending requests', function() { +        hb.when('GET').respond(200, ''); +        hb('GET', '/some', null, callback); +        hb('GET', '/some', null, callback); +        hb('GET', '/some', null, callback); + +        hb.flush(2); +        expect(callback).toHaveBeenCalled(); +        expect(callback.callCount).toBe(2);        }); -      hb.expect('GET', '/url1').respond('some-data'); -      hb.expect('GET', '/url2').respond('some-data', {'X-Header': 'true'}); -      hb('GET', '/url1', null, callback); -      hb('GET', '/url2', null, callback); -      hb.flush(); -      expect(callback).toHaveBeenCalled(); -      expect(callback.callCount).toBe(2); -    }); +      it('should throw exception when flushing more requests than pending', function() { +        hb.when('GET').respond(200, ''); +        hb('GET', '/url', null, callback); -    it('respond() should set default status 200 if not defined', function() { -      callback.andCallFake(function(status, response) { -        expect(status).toBe(200); -        expect(response).toBe('some-data'); +        expect(function() {hb.flush(2);}).toThrow('No more pending request to flush !'); +        expect(callback).toHaveBeenCalledOnce();        }); -      hb.when('GET', '/url1').respond('some-data'); -      hb.when('GET', '/url2').respond('some-data', {'X-Header': 'true'}); -      hb('GET', '/url1', null, callback); -      hb('GET', '/url2', null, callback); -      hb.flush(); -      expect(callback).toHaveBeenCalled(); -      expect(callback.callCount).toBe(2); -    }); +      it('should throw exception when no request to flush', function() { +        expect(function() {hb.flush();}).toThrow('No pending request to flush !'); -    it('should respond with definition if no response for expectation', function() { -      callback.andCallFake(function(status, response) { -        expect(status).toBe(201); -        expect(response).toBe('def-response'); +        hb.when('GET').respond(200, ''); +        hb('GET', '/some', null, callback); +        hb.flush(); + +        expect(function() {hb.flush();}).toThrow('No pending request to flush !');        }); -      hb.when('GET').respond(201, 'def-response'); -      hb.expect('GET', '/some-url'); -      hb('GET', '/some-url', null, callback); -      hb.flush(); -      expect(callback).toHaveBeenCalledOnce(); -      hb.verifyNoOutstandingExpectation(); +      it('should throw exception if not all expectations satisfied', function() { +        hb.expect('GET', '/url1').respond(); +        hb.expect('GET', '/url2').respond(); + +        hb('GET', '/url1', null, angular.noop); +        expect(function() {hb.flush();}).toThrow('Unsatisfied requests: GET /url2'); +      });      }); @@ -699,7 +659,7 @@ describe('mocks', function() {      }); -    it('should throw an exception if no response for expection and no definition', function() { +    it('should throw an exception if no response for exception and no definition', function() {        hb.expect('GET', '/url');        expect(function() {          hb('GET', '/url', null, callback); @@ -762,7 +722,7 @@ describe('mocks', function() {      }); -    describe('reset', function() { +    describe('resetExpectations', function() {        it('should remove all expectations', function() {          hb.expect('GET', '/u2').respond(200, '', {}); @@ -773,7 +733,7 @@ describe('mocks', function() {        }); -      it('should remove all responses', function() { +      it('should remove all pending responses', function() {          var cancelledClb = jasmine.createSpy('cancelled');          hb.expect('GET', '/url').respond(200, ''); diff --git a/test/service/httpBackendSpec.js b/test/service/httpBackendSpec.js index b9bf2b18..1c6d3a51 100644 --- a/test/service/httpBackendSpec.js +++ b/test/service/httpBackendSpec.js @@ -58,18 +58,13 @@ describe('$httpBackend', function() {      $backend('POST', 'URL', null, noop, {'X-header1': 'value1', 'X-header2': 'value2'});      xhr = MockXhr.$$lastInstance; -    expect(xhr.$$headers).toEqual({ +    expect(xhr.$$reqHeaders).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); @@ -91,16 +86,20 @@ describe('$httpBackend', function() {    }); -  it('should be async even if xhr.send() is sync', function() { -    // IE6, IE7 is sync when serving from cache +  it('should register onreadystatechange callback before sending', function() { +    // send() in 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; +        this.onreadystatechange();        }; + +      this.getAllResponseHeaders = valueFn('');      }      callback.andCallFake(function(status, response) { @@ -108,14 +107,8 @@ describe('$httpBackend', function() {        expect(response).toBe('response');      }); -    $backend = createHttpBackend($browser, SyncXhr, fakeTimeout); +    $backend = createHttpBackend($browser, SyncXhr);      $backend('GET', '/url', null, callback); -    expect(callback).not.toHaveBeenCalled(); - -    fakeTimeout.flush(); -    expect(callback).toHaveBeenCalledOnce(); - -    (xhr.onreadystatechange || noop)();      expect(callback).toHaveBeenCalledOnce();    }); diff --git a/test/service/httpSpec.js b/test/service/httpSpec.js index 5b8f43f5..c1f8645e 100644 --- a/test/service/httpSpec.js +++ b/test/service/httpSpec.js @@ -66,7 +66,7 @@ describe('$http', function() {            expect(data).toBe('Hello!?');            expect(status).toBe(209);            callback(); -        }) +        });          $httpBackend.flush();          expect(callback).toHaveBeenCalledOnce();        })); @@ -550,7 +550,7 @@ describe('$http', function() {      }); -    describe('transform', function() { +    describe('transformData', function() {        describe('request', function() { @@ -648,17 +648,19 @@ describe('$http', function() {          cache = $cacheFactory('testCache');        })); +        function doFirstCacheRequest(method, respStatus, headers) {          $httpBackend.expect(method || 'GET', '/url').respond(respStatus || 200, 'content', headers);          $http({method: method || 'GET', url: '/url', cache: cache});          $httpBackend.flush();        } -      it('should cache GET request when cache is provided', inject(function($browser) { + +      it('should cache GET request when cache is provided', inject(function($rootScope) {          doFirstCacheRequest();          $http({method: 'get', url: '/url', cache: cache}).success(callback); -        $browser.defer.flush(); +        $rootScope.$digest();          expect(callback).toHaveBeenCalledOnce();          expect(callback.mostRecentCall.args[0]).toBe('content'); @@ -737,7 +739,7 @@ describe('$http', function() {        }); -      it('should cache the headers as well', inject(function($browser) { +      it('should cache the headers as well', inject(function($rootScope) {          doFirstCacheRequest('GET', 200, {'content-encoding': 'gzip', 'server': 'Apache'});          callback.andCallFake(function(r, s, headers) {            expect(headers()).toEqual({'content-encoding': 'gzip', 'server': 'Apache'}); @@ -745,24 +747,37 @@ describe('$http', function() {          });          $http({method: 'GET', url: '/url', cache: cache}).success(callback); -        $browser.defer.flush(); +        $rootScope.$digest();          expect(callback).toHaveBeenCalledOnce();        })); -      it('should cache status code as well', inject(function($browser) { +      it('should not share the cached headers object instance', inject(function($rootScope) { +        doFirstCacheRequest('GET', 200, {'content-encoding': 'gzip', 'server': 'Apache'}); +        callback.andCallFake(function(r, s, headers) { +          expect(headers()).toEqual(cache.get('/url')[2]); +          expect(headers()).not.toBe(cache.get('/url')[2]); +        }); + +        $http({method: 'GET', url: '/url', cache: cache}).success(callback); +        $rootScope.$digest(); +        expect(callback).toHaveBeenCalledOnce(); +      })); + + +      it('should cache status code as well', inject(function($rootScope) {          doFirstCacheRequest('GET', 201);          callback.andCallFake(function(r, status, h) {            expect(status).toBe(201);          });          $http({method: 'get', url: '/url', cache: cache}).success(callback); -        $browser.defer.flush(); +        $rootScope.$digest();          expect(callback).toHaveBeenCalledOnce();        })); -      it('should use cache even if request fired before first response is back', function() { +      it('should use cache even if second request was made before the first returned', function() {          $httpBackend.expect('GET', '/url').respond(201, 'fake-response');          callback.andCallFake(function(response, status, headers) { @@ -777,6 +792,22 @@ describe('$http', function() {          expect(callback).toHaveBeenCalled();          expect(callback.callCount).toBe(2);        }); + + +      it('should default to status code 200 and empty headers if cache contains a non-array element', +          inject(function($rootScope) { +            cache.put('/myurl', 'simple response'); +            $http.get('/myurl', {cache: cache}).success(function(data, status, headers) { +              expect(data).toBe('simple response'); +              expect(status).toBe(200); +              expect(headers()).toEqual({}); +              callback(); +            }); + +            $rootScope.$digest(); +            expect(callback).toHaveBeenCalledOnce(); +          }) +      );      }); @@ -794,7 +825,7 @@ describe('$http', function() {        }); -      it('should update pending requests even when served from cache', inject(function($browser) { +      it('should update pending requests even when served from cache', inject(function($rootScope) {          $httpBackend.when('GET').respond(200);          $http({method: 'get', url: '/cached', cache: true}); @@ -807,7 +838,7 @@ describe('$http', function() {          $http({method: 'get', url: '/cached', cache: true});          expect($http.pendingRequests.length).toBe(1); -        $browser.defer.flush(); +        $rootScope.$apply();          expect($http.pendingRequests.length).toBe(0);        })); diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js index e8ff4b27..09d807b5 100644 --- a/test/widgetsSpec.js +++ b/test/widgetsSpec.js @@ -72,7 +72,6 @@ describe('widget', function() {        $rootScope.childScope.name = 'misko';        $rootScope.url = 'myUrl';        $rootScope.$digest(); -      $browser.defer.flush();        expect(element.text()).toEqual('misko');      })); @@ -86,7 +85,6 @@ describe('widget', function() {        $rootScope.childScope.name = 'igor';        $rootScope.url = 'myUrl';        $rootScope.$digest(); -      $browser.defer.flush();        expect(element.text()).toEqual('igor'); @@ -103,7 +101,6 @@ describe('widget', function() {        element = $compile(element)($rootScope);        $rootScope.url = 'myUrl';        $rootScope.$digest(); -      $browser.defer.flush();        // TODO(misko): because we are using scope==this, the eval gets registered        // during the flush phase and hence does not get called. @@ -125,7 +122,6 @@ describe('widget', function() {        $rootScope.url = 'myUrl';        $rootScope.$digest(); -      $browser.defer.flush();        expect(element.text()).toEqual('my partial');        expect($rootScope.loaded).toBe(true); @@ -141,7 +137,6 @@ describe('widget', function() {        $rootScope.url = 'myUrl';        $rootScope.$digest(); -      $browser.defer.flush();        expect($rootScope.$$childHead).toBeTruthy();        $rootScope.url = null; @@ -166,7 +161,6 @@ describe('widget', function() {        $rootScope.url = 'myUrl';        $rootScope.$digest(); -      $browser.defer.flush();        expect(element.text()).toEqual('my partial');        dealoc($rootScope);      })); @@ -199,7 +193,6 @@ describe('widget', function() {        });        $rootScope.$digest(); -      $browser.defer.flush();        expect(element.text()).toBe('my partial');      })); @@ -746,7 +739,6 @@ describe('widget', function() {        $rootScope.log = [];        $location.path('/foo');        $rootScope.$apply(); -      $browser.defer.flush();        expect($rootScope.log).toEqual(['parent', 'init', 'child']);      })); @@ -801,7 +793,6 @@ describe('widget', function() {        });        $rootScope.$digest(); -      $browser.defer.flush();        expect(element.text()).toBe('my partial');      }));    });  | 
