fix($http): fix and cleanup $http and friends

$http:
- use promises internally
- get rid of XhrFuture that was previously used internally
- get rid of $browser.defer calls for async stuff (serving from cache),
  promises will take care of asynchronicity
- fix transformation bugs (when caching requested + multiple request
  pending + error is returned)
- get rid of native header parsing and instead just lazily parse the
  header string

$httpBackend:
- don't return raw/mock XMLHttpRequest object (we don't use it for
  anything anymore)
- call the callback with response headers string

mock $httpBackend:
- unify response api for expect and when
- call the callback with response headers string
- changed the expect/when failure error message so that EXPECTED and GOT
  values are aligned

Conflicts:

	src/service/http.js
	test/service/compilerSpec.js
	test/service/httpSpec.js
This commit is contained in:
Igor Minar 2011-12-28 09:26:22 -08:00 committed by Vojta Jina
parent 63cca9afbc
commit a13b5ed3bc
7 changed files with 578 additions and 662 deletions

57
src/angular-mocks.js vendored
View file

@ -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');

View file

@ -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);
}
});
/**
* @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);
forEach(providerResponseInterceptors, function(interceptor) {
responseInterceptors.push(isString(interceptor) ? $injector.get(interceptor) : interceptor);
});
promise.success = function(fn) {
promise.then(function(response) {
fn(response.data, response.status, response.headers, config);
});
return 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});
});
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
}));
};
});
}
function createShortMethodsWithData(name) {
forEach(arguments, function(name) {
$http[name] = function(url, data, config) {
return $http(extend(config || {}, {
method: name,
url: url,
data: data
}));
};
});
}
/**
* 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;
/**
* Callback registered to $httpBackend():
* - caches the response if desired
* - calls fireCallbacks()
* - clears the reference to raw request object
* @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 done(status, response) {
// aborted request or jsonp
if (!rawRequest) parsedHeaders = {};
function $http(config) {
config.method = uppercase(config.method);
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);
}
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;
// send request
promise = sendReq(config, reqData, reqHeaders);
// 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;
};
promise.error = function(fn) {
promise.then(null, function(response) {
fn(response.data, response.status, response.headers, config);
});
return promise;
};
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 = [];
/**
* @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
}));
};
});
}
function createShortMethodsWithData(name) {
forEach(arguments, function(name) {
$http[name] = function(url, data, config) {
return $http(extend(config || {}, {
method: name,
url: url,
data: data
}));
};
});
}
/**
* Makes the request
*
* !!! ACCESSES CLOSURE VARS:
* $httpBackend, $config, $log, $rootScope, defaultCache, $http.pendingRequests
*/
function sendReq(config, reqData, reqHeaders) {
var deferred = $q.defer(),
promise = deferred.promise,
cache,
cachedResp;
$http.pendingRequests.push(config);
promise.then(removePendingReq, removePendingReq);
if (config.cache && config.method == 'GET') {
cache = isObject(config.cache) ? config.cache : defaultCache;
}
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;
}
/**
* Fire all registered callbacks for given status code
*
* This method when:
* - serving response from real request
* - serving response from cache
*
* It does:
* - transform the response
* - call proper callbacks
* - log errors
* - apply the $scope
* - clear parsed headers
*/
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;
}
/**
* This is the third argument in any user callback
* @see parseHeaders
*
* Return single header value or all headers parsed as object.
* Headers all lazy parsed when first requested.
*
* @param {string=} name Name of header
* @returns {string|Object}
*/
function headers(name) {
if (name) {
return parsedHeaders ?
parsedHeaders[lowercase(name)] || null :
rawRequest.getResponseHeader(name);
}
parsedHeaders = parsedHeaders || parseHeaders(rawRequest.getAllResponseHeaders());
return parsedHeaders;
}
/**
* Retry the request
*
* @param {Object=} config Optional config object to extend the original configuration
* @returns {HttpPromise}
*/
this.retry = function(config) {
if (rawRequest) throw 'Can not retry request. Abort pending request first.';
extend(cfg, config);
cfg.method = uppercase(cfg.method);
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 (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);
}
}
}];
}

View file

@ -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);
}
};
}

View file

@ -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'}];
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).toHaveBeenCalledOnceWith(200, 'first', 'header: val');
});
var xhr = hb('GET', '/some', 'data', callback, {a: 'b'});
hb.flush();
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'}];
});
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');
});
hb('GET', '/some', 'data', callback, {a: 'b'});
hb.flush();
it('expect() should require specified order', function() {
hb.expect('GET', '/url1').respond(200, '');
hb.expect('GET', '/url2').respond(200, '');
expect(function() {
hb('GET', '/url2', null, noop, {});
}).toThrow('Unexpected request: GET /url2\nExpected GET /url1');
});
it('expect() should have precendence over when()', function() {
callback.andCallFake(function(status, response) {
expect(status).toBe(300);
expect(response).toBe('expect');
expect(callback).toHaveBeenCalledOnceWith(301, 'GET/some;data;a=b', 'Connection: keep-alive');
});
hb.when('GET', '/url').respond(200, 'when');
hb.expect('GET', '/url').respond(300, 'expect');
it('should default status code to 200', function() {
callback.andCallFake(function(status, response) {
expect(status).toBe(200);
expect(response).toBe('some-data');
});
hb('GET', '/url', null, callback, {});
hb.flush();
expect(callback).toHaveBeenCalledOnce();
});
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'});
expect(function() {
hb('GET', '/match', null, noop, {});
}).toThrow('Expected GET /match with different headers\n' +
'EXPECTED: {"Content-Type":"application/json"}\nGOT: {}');
});
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');
});
it('expect() should without respond() and use respond()', function() {
callback.andCallFake(function(status, response) {
expect(status).toBe(201);
expect(response).toBe('data');
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', '/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 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);
hb.flush();
expect(callback.callCount).toBe(2);
expect(callback.argsForCall[0]).toEqual([200, 'first', '']);
expect(callback.argsForCall[1]).toEqual([200, 'second', '']);
});
});
it('flush() should flush requests fired during callbacks', function() {
hb.when('GET').respond(200, '');
hb('GET', '/some', null, function() {
hb('GET', '/other', null, callback);
describe('expect()', function() {
it('should require specified order', function() {
hb.expect('GET', '/url1').respond(200, '');
hb.expect('GET', '/url2').respond(200, '');
expect(function() {
hb('GET', '/url2', null, noop, {});
}).toThrow('Unexpected request: GET /url2\nExpected GET /url1');
});
hb.flush();
expect(callback).toHaveBeenCalled();
});
it('should have precedence over when()', function() {
callback.andCallFake(function(status, response) {
expect(status).toBe(300);
expect(response).toBe('expect');
});
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);
hb.when('GET', '/url').respond(200, 'when');
hb.expect('GET', '/url').respond(300, 'expect');
hb.flush(2);
expect(callback).toHaveBeenCalled();
expect(callback.callCount).toBe(2);
});
it('flush() should throw exception when flushing more requests than pending', function() {
hb.when('GET').respond(200, '');
hb('GET', '/url', null, callback);
expect(function() {hb.flush(2);}).toThrow('No more pending request to flush !');
expect(callback).toHaveBeenCalledOnce();
});
it('(flush) should throw exception when no request to flush', function() {
expect(function() {hb.flush();}).toThrow('No pending request to flush !');
hb.when('GET').respond(200, '');
hb('GET', '/some', null, callback);
hb.flush();
expect(function() {hb.flush();}).toThrow('No pending request to flush !');
});
it('(flush) should throw exception if not all expectations satasfied', 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');
});
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');
hb('GET', '/url', null, callback, {});
hb.flush();
expect(callback).toHaveBeenCalledOnce();
});
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 only headers differs from expectation', function() {
hb.when('GET').respond(200, '', {});
hb.expect('GET', '/match', undefined, {'Content-Type': 'application/json'});
expect(function() {
hb('GET', '/match', null, noop, {});
}).toThrow('Expected GET /match with different headers\n' +
'EXPECTED: {"Content-Type":"application/json"}\nGOT: {}');
});
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('GET', '/match', 'different', noop, {});
}).toThrow('Expected GET /match with different data\n' +
'EXPECTED: some-data\nGOT: different');
});
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', '/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('respond() should set default status 200 if not defined', function() {
callback.andCallFake(function(status, response) {
expect(status).toBe(200);
expect(response).toBe('some-data');
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.flush();
expect(callback).toHaveBeenCalled();
});
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 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 respond with definition if no response for expectation', function() {
callback.andCallFake(function(status, response) {
expect(status).toBe(201);
expect(response).toBe('def-response');
hb.flush(2);
expect(callback).toHaveBeenCalled();
expect(callback.callCount).toBe(2);
});
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 when flushing more requests than pending', function() {
hb.when('GET').respond(200, '');
hb('GET', '/url', null, callback);
expect(function() {hb.flush(2);}).toThrow('No more pending request to flush !');
expect(callback).toHaveBeenCalledOnce();
});
it('should throw exception when no request to flush', function() {
expect(function() {hb.flush();}).toThrow('No pending request to flush !');
hb.when('GET').respond(200, '');
hb('GET', '/some', null, callback);
hb.flush();
expect(function() {hb.flush();}).toThrow('No pending request to flush !');
});
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, '');

View file

@ -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();
});

View file

@ -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);
}));

View file

@ -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');
}));
});