feat($http): new $http service, removing $xhr.*

Features:
- aborting requests
- more flexible callbacks (per status code)
- custom request headers (per request)
- access to response headers
- custom transform functions (both request, response)
- caching
- shortcut methods (get, head, post, put, delete, patch, jsonp)
- exposing pendingCount()
- setting timeout
Breaks Renaming $xhr to $http
Breaks Takes one parameter now - configuration object
Breaks $xhr.cache removed - use configuration cache: true instead
Breaks $xhr.error, $xhr.bulk removed
Breaks Callback functions get parameters: response, status, headers
Closes #38
Closes #80
Closes #180
Closes #299
Closes #342
Closes #395
Closes #413
Closes #414
Closes #507
This commit is contained in:
Vojta Jina 2011-08-05 01:24:41 +02:00 committed by Igor Minar
parent 497839f583
commit 59adadca08
17 changed files with 1576 additions and 1095 deletions

5
angularFiles.js vendored
View file

@ -32,10 +32,7 @@ angularFiles = {
'src/service/scope.js',
'src/service/sniffer.js',
'src/service/window.js',
'src/service/xhr.bulk.js',
'src/service/xhr.cache.js',
'src/service/xhr.error.js',
'src/service/xhr.js',
'src/service/http.js',
'src/service/locale.js',
'src/directives.js',
'src/markups.js',

View file

@ -77,6 +77,7 @@ function ngModule($provide, $injector) {
$provide.service('$exceptionHandler', $ExceptionHandlerProvider);
$provide.service('$filter', $FilterProvider);
$provide.service('$formFactory', $FormFactoryProvider);
$provide.service('$http', $HttpProvider);
$provide.service('$location', $LocationProvider);
$provide.service('$log', $LogProvider);
$provide.service('$parse', $ParseProvider);
@ -86,9 +87,5 @@ function ngModule($provide, $injector) {
$provide.service('$rootScope', $RootScopeProvider);
$provide.service('$sniffer', $SnifferProvider);
$provide.service('$window', $WindowProvider);
$provide.service('$xhr.bulk', $XhrBulkProvider);
$provide.service('$xhr.cache', $XhrCacheProvider);
$provide.service('$xhr.error', $XhrErrorProvider);
$provide.service('$xhr', $XhrProvider);
}

428
src/service/http.js Normal file
View file

@ -0,0 +1,428 @@
'use strict';
/**
* Parse headers into key value object
*
* @param {string} headers Raw headers as a string
* @returns {Object} Parsed headers as key valu object
*/
function parseHeaders(headers) {
var parsed = {}, key, val, i;
forEach(headers.split('\n'), function(line) {
i = line.indexOf(':');
key = lowercase(trim(line.substr(0, i)));
val = trim(line.substr(i + 1));
if (key) {
if (parsed[key]) {
parsed[key] += ', ' + val;
} else {
parsed[key] = val;
}
}
});
return parsed;
}
/**
* Chain all given functions
*
* This function is used for both request and response transforming
*
* @param {*} data Data to transform.
* @param {function|Array.<function>} fns Function or an array of functions.
* @param {*=} param Optional parameter to be passed to all transform functions.
* @returns {*} Transformed data.
*/
function transform(data, fns, param) {
if (isFunction(fns))
return fns(data);
forEach(fns, function(fn) {
data = fn(data, param);
});
return data;
}
/**
* @ngdoc object
* @name angular.module.ng.$http
* @requires $browser
* @requires $exceptionHandler
* @requires $cacheFactory
*
* @description
*/
function $HttpProvider() {
var $config = this.defaults = {
// transform in-coming reponse data
transformResponse: function(data) {
if (isString(data)) {
if (/^\)\]\}',\n/.test(data)) data = data.substr(6);
if (/^\s*[\[\{]/.test(data) && /[\}\]]\s*$/.test(data))
data = fromJson(data, true);
}
return data;
},
// transform out-going request data
transformRequest: function(d) {
return isObject(d) ? toJson(d) : d;
},
// default headers
headers: {
common: {
'Accept': 'application/json, text/plain, */*',
'X-Requested-With': 'XMLHttpRequest'
},
post: {'Content-Type': 'application/json'},
put: {'Content-Type': 'application/json'}
}
};
this.$get = ['$browser', '$exceptionHandler', '$cacheFactory', '$rootScope',
function($browser, $exceptionHandler, $cacheFactory, $rootScope) {
var cache = $cacheFactory('$http'),
pendingRequestsCount = 0;
// the actual service
function $http(config) {
return new XhrFuture().retry(config);
}
/**
* @workInProgress
* @ngdoc method
* @name angular.service.$http#pendingCount
* @methodOf angular.service.$http
*
* @description
* Return number of pending requests
*
* @returns {number} Number of pending requests
*/
$http.pendingCount = function() {
return pendingRequestsCount;
};
/**
* @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 {XhrFuture} 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 {XhrFuture} 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 {XhrFuture} 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 {XhrFuture} 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()
*
* !!! ACCESS CLOSURE VARS: $browser, $config, $log, $rootScope, cache, pendingRequestsCount
*/
function XhrFuture() {
var rawRequest, cfg = {}, callbacks = [],
defHeaders = $config.headers,
parsedHeaders;
/**
* Callback registered to $browser.xhr:
* - caches the response if desired
* - calls fireCallbacks()
* - clears the reference to raw request object
*/
function done(status, response) {
// aborted request or jsonp
if (!rawRequest) parsedHeaders = {};
if (cfg.cache && cfg.method == 'GET' && 200 <= status && status < 300) {
parsedHeaders = parsedHeaders || parseHeaders(rawRequest.getAllResponseHeaders());
cache.put(cfg.url, [status, response, parsedHeaders]);
}
fireCallbacks(response, status);
rawRequest = null;
}
/**
* Fire all registered callbacks for given status code
*
* This method when:
* - serving response from real request ($browser.xhr callback)
* - serving response from cache
*
* It does:
* - transform the response
* - call proper callbacks
* - log errors
* - apply the $scope
* - clear parsed headers
*/
function fireCallbacks(response, status) {
// transform the response
response = transform(response, cfg.transformResponse || $config.transformResponse, rawRequest);
var regexp = statusToRegexp(status),
pattern, callback;
pendingRequestsCount--;
// normalize internal statuses to 0
status = Math.max(status, 0);
for (var i = 0; i < callbacks.length; i += 2) {
pattern = callbacks[i];
callback = callbacks[i + 1];
if (regexp.test(pattern)) {
try {
callback(response, status, headers);
} catch(e) {
$exceptionHandler(e);
}
}
}
$rootScope.$apply();
parsedHeaders = null;
}
/**
* Convert given status code number into regexp
*
* It would be much easier to convert registered statuses (e.g. "2xx") into regexps,
* but this has an advantage of creating just one regexp, instead of one regexp per
* registered callback. Anyway, probably not big deal.
*
* @param status
* @returns {RegExp}
*/
function statusToRegexp(status) {
var strStatus = status + '',
regexp = '';
for (var i = Math.min(0, strStatus.length - 3); i < strStatus.length; i++) {
regexp += '(' + (strStatus.charAt(i) || 0) + '|x)';
}
return new RegExp(regexp);
}
/**
* 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 {XhrFuture}
*/
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 fromCache;
if (cfg.cache && cfg.method == 'GET' && (fromCache = cache.get(cfg.url))) {
$browser.defer(function() {
parsedHeaders = fromCache[2];
fireCallbacks(fromCache[1], fromCache[0]);
});
} else {
rawRequest = $browser.xhr(cfg.method, cfg.url, data, done, headers, cfg.timeout);
}
pendingRequestsCount++;
return this;
};
/**
* Abort the request
*/
this.abort = function() {
if (rawRequest) {
rawRequest.abort();
}
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('xxx', function(){});
* .on('20x,3xx', function(){});
* .on('success', function(){});
* .on('error', function(){});
* .on('always', function(){});
*
* @param {string} pattern Status code pattern with "x" for any number
* @param {function(*, number, Object)} callback Function to be called when response arrives
* @returns {XhrFuture}
*/
this.on = function(pattern, callback) {
var alias = {
success: '2xx',
error: '0-2,0-1,000,4xx,5xx',
always: 'xxx',
timeout: '0-1',
abort: '000'
};
callbacks.push(alias[pattern] || pattern);
callbacks.push(callback);
return this;
};
}
}];
}

View file

@ -1,89 +0,0 @@
'use strict';
/**
* @ngdoc object
* @name angular.module.ng.$xhr.bulk
* @requires $xhr
* @requires $xhr.error
* @requires $log
*
* @description
*
* @example
*/
function $XhrBulkProvider() {
this.$get = ['$rootScope', '$xhr', '$xhr.error', '$log',
function( $rootScope, $xhr, $error, $log) {
var requests = [];
function bulkXHR(method, url, post, success, error) {
if (isFunction(post)) {
error = success;
success = post;
post = null;
}
var currentQueue;
forEach(bulkXHR.urls, function(queue){
if (isFunction(queue.match) ? queue.match(url) : queue.match.exec(url)) {
currentQueue = queue;
}
});
if (currentQueue) {
if (!currentQueue.requests) currentQueue.requests = [];
var request = {
method: method,
url: url,
data: post,
success: success};
if (error) request.error = error;
currentQueue.requests.push(request);
} else {
$xhr(method, url, post, success, error);
}
}
bulkXHR.urls = {};
bulkXHR.flush = function(success, errorback) {
assertArgFn(success = success || noop, 0);
assertArgFn(errorback = errorback || noop, 1);
forEach(bulkXHR.urls, function(queue, url) {
var currentRequests = queue.requests;
if (currentRequests && currentRequests.length) {
queue.requests = [];
queue.callbacks = [];
$xhr('POST', url, {requests: currentRequests},
function(code, response) {
forEach(response, function(response, i) {
try {
if (response.status == 200) {
(currentRequests[i].success || noop)(response.status, response.response);
} else if (isFunction(currentRequests[i].error)) {
currentRequests[i].error(response.status, response.response);
} else {
$error(currentRequests[i], response);
}
} catch(e) {
$log.error(e);
}
});
success();
},
function(code, response) {
forEach(currentRequests, function(request, i) {
try {
if (isFunction(request.error)) {
request.error(code, response);
} else {
$error(request, response);
}
} catch(e) {
$log.error(e);
}
});
noop();
});
}
});
};
$rootScope.$watch(function() { bulkXHR.flush(); });
return bulkXHR;
}];
}

View file

@ -1,118 +0,0 @@
'use strict';
/**
* @ngdoc object
* @name angular.module.ng.$xhr.cache
* @function
*
* @requires $xhr.bulk
* @requires $defer
* @requires $xhr.error
* @requires $log
*
* @description
* Acts just like the {@link angular.module.ng.$xhr $xhr} service but caches responses for `GET`
* requests. All cache misses are delegated to the $xhr service.
*
* @property {function()} delegate Function to delegate all the cache misses to. Defaults to
* the {@link angular.module.ng.$xhr $xhr} service.
* @property {object} data The hashmap where all cached entries are stored.
*
* @param {string} method HTTP method.
* @param {string} url Destination URL.
* @param {(string|Object)=} post Request body.
* @param {function(number, (string|Object))} success Response success callback.
* @param {function(number, (string|Object))=} error Response error callback.
* @param {boolean=} [verifyCache=false] If `true` then a result is immediately returned from cache
* (if present) while a request is sent to the server for a fresh response that will update the
* cached entry. The `success` function will be called when the response is received.
* @param {boolean=} [sync=false] in case of cache hit execute `success` synchronously.
*/
function $XhrCacheProvider() {
this.$get = ['$xhr.bulk', '$defer', '$xhr.error', '$log',
function($xhr, $defer, $error, $log) {
var inflight = {};
function cache(method, url, post, success, error, verifyCache, sync) {
if (isFunction(post)) {
if (!isFunction(success)) {
verifyCache = success;
sync = error;
error = null;
} else {
sync = verifyCache;
verifyCache = error;
error = success;
}
success = post;
post = null;
} else if (!isFunction(error)) {
sync = verifyCache;
verifyCache = error;
error = null;
}
if (method == 'GET') {
var data, dataCached;
if ((dataCached = cache.data[url])) {
if (sync) {
success(200, copy(dataCached.value));
} else {
$defer(function() { success(200, copy(dataCached.value)); });
}
if (!verifyCache)
return;
}
if ((data = inflight[url])) {
data.successes.push(success);
data.errors.push(error);
} else {
inflight[url] = {successes: [success], errors: [error]};
cache.delegate(method, url, post,
function(status, response) {
if (status == 200)
cache.data[url] = {value: response};
var successes = inflight[url].successes;
delete inflight[url];
forEach(successes, function(success) {
try {
(success||noop)(status, copy(response));
} catch(e) {
$log.error(e);
}
});
},
function(status, response) {
var errors = inflight[url].errors,
successes = inflight[url].successes;
delete inflight[url];
forEach(errors, function(error, i) {
try {
if (isFunction(error)) {
error(status, copy(response));
} else {
$error(
{method: method, url: url, data: post, success: successes[i]},
{status: status, body: response});
}
} catch(e) {
$log.error(e);
}
});
});
}
} else {
cache.data = {};
cache.delegate(method, url, post, success, error);
}
}
cache.data = {};
cache.delegate = $xhr;
return cache;
}];
}

View file

@ -1,44 +0,0 @@
'use strict';
/**
* @ngdoc object
* @name angular.module.ng.$xhr.error
* @function
* @requires $log
*
* @description
* Error handler for {@link angular.module.ng.$xhr $xhr service}. An application can replaces this
* service with one specific for the application. The default implementation logs the error to
* {@link angular.module.ng.$log $log.error}.
*
* @param {Object} request Request object.
*
* The object has the following properties
*
* - `method` `{string}` The http request method.
* - `url` `{string}` The request destination.
* - `data` `{(string|Object)=} An optional request body.
* - `success` `{function()}` The success callback function
*
* @param {Object} response Response object.
*
* The response object has the following properties:
*
* - status {number} Http status code.
* - body {string|Object} Body of the response.
*
* @example
<doc:example>
<doc:source>
fetch a non-existent file and log an error in the console:
<button ng:click="$service('$xhr')('GET', '/DOESNT_EXIST')">fetch</button>
</doc:source>
</doc:example>
*/
function $XhrErrorProvider() {
this.$get = ['$log', function($log) {
return function(request, response){
$log.error('ERROR: XHR: ' + request.url, request, response);
};
}];
}

View file

@ -1,231 +0,0 @@
'use strict';
/**
* @ngdoc object
* @name angular.module.ng.$xhr
* @function
* @requires $browser $xhr delegates all XHR requests to the `$browser.xhr()`. A mock version
* of the $browser exists which allows setting expectations on XHR requests
* in your tests
* @requires $xhr.error $xhr delegates all non `2xx` response code to this service.
* @requires $log $xhr delegates all exceptions to `$log.error()`.
*
* @description
* Generates an XHR request. The $xhr service delegates all requests to
* {@link angular.module.ng.$browser $browser.xhr()} and adds error handling and security features.
* While $xhr service provides nicer api than raw XmlHttpRequest, it is still considered a lower
* level api in angular. For a higher level abstraction that utilizes `$xhr`, please check out the
* {@link angular.module.ng.$resource $resource} service.
*
* # Error handling
* If no `error callback` is specified, XHR response with response code other then `2xx` will be
* delegated to {@link angular.module.ng.$xhr.error $xhr.error}. The `$xhr.error` can intercept the
* request and process it in application specific way, or resume normal execution by calling the
* request `success` method.
*
* # HTTP Headers
* The $xhr service will automatically add certain http headers to all requests. These defaults can
* be fully configured by accessing the `$xhr.defaults.headers` configuration object, which
* currently contains this default configuration:
*
* - `$xhr.defaults.headers.common` (headers that are common for all requests):
* - `Accept: application/json, text/plain, *\/*`
* - `X-Requested-With: XMLHttpRequest`
* - `$xhr.defaults.headers.post` (header defaults for HTTP POST requests):
* - `Content-Type: application/x-www-form-urlencoded`
*
* To add or overwrite these defaults, simple add or remove a property from this configuration
* object. To add headers for an HTTP method other than POST, simple create a new object with name
* equal to the lowercased http method name, e.g. `$xhr.defaults.headers.get['My-Header']='value'`.
*
*
* # Security Considerations
* When designing web applications your design needs to consider security threats from
* {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx
* JSON Vulnerability} and {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF}.
* Both server and the client must cooperate in order to eliminate these threats. Angular comes
* pre-configured with strategies that address these issues, but for this to work backend server
* cooperation is required.
*
* ## JSON Vulnerability Protection
* A {@link http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx
* JSON Vulnerability} allows third party web-site to turn your JSON resource URL into
* {@link http://en.wikipedia.org/wiki/JSON#JSONP JSONP} request under some conditions. To
* counter this your server can prefix all JSON requests with following string `")]}',\n"`.
* Angular will automatically strip the prefix before processing it as JSON.
*
* For example if your server needs to return:
* <pre>
* ['one','two']
* </pre>
*
* which is vulnerable to attack, your server can return:
* <pre>
* )]}',
* ['one','two']
* </pre>
*
* angular will strip the prefix, before processing the JSON.
*
*
* ## Cross Site Request Forgery (XSRF) Protection
* {@link http://en.wikipedia.org/wiki/Cross-site_request_forgery XSRF} is a technique by which an
* unauthorized site can gain your user's private data. Angular provides following mechanism to
* counter XSRF. When performing XHR requests, the $xhr service reads a token from a cookie
* called `XSRF-TOKEN` and sets it as the HTTP header `X-XSRF-TOKEN`. Since only JavaScript that
* runs on your domain could read the cookie, your server can be assured that the XHR came from
* JavaScript running on your domain.
*
* To take advantage of this, your server needs to set a token in a JavaScript readable session
* cookie called `XSRF-TOKEN` on first HTTP GET request. On subsequent non-GET requests the server
* can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure that only
* JavaScript running on your domain could have read the token. The token must be unique for each
* user and must be verifiable by the server (to prevent the JavaScript making up its own tokens).
* We recommend that the token is a digest of your site's authentication cookie with
* {@link http://en.wikipedia.org/wiki/Rainbow_table salt for added security}.
*
* @param {string} method HTTP method to use. Valid values are: `GET`, `POST`, `PUT`, `DELETE`, and
* `JSONP`. `JSONP` is a special case which causes a
* [JSONP](http://en.wikipedia.org/wiki/JSON#JSONP) cross domain request using script tag
* insertion.
* @param {string} url Relative or absolute URL specifying the destination of the request. For
* `JSON` requests, `url` should include `JSON_CALLBACK` string to be replaced with a name of an
* angular generated callback function.
* @param {(string|Object)=} post Request content as either a string or an object to be stringified
* as JSON before sent to the server.
* @param {function(number, (string|Object))} success A function to be called when the response is
* received. The success function will be called with:
*
* - {number} code [HTTP status code](http://en.wikipedia.org/wiki/List_of_HTTP_status_codes) of
* the response. This will currently always be 200, since all non-200 responses are routed to
* {@link angular.module.ng.$xhr.error} service (or custom error callback).
* - {string|Object} response Response object as string or an Object if the response was in JSON
* format.
* @param {function(number, (string|Object))} error A function to be called if the response code is
* not 2xx.. Accepts the same arguments as success, above.
*
* @example
<doc:example>
<doc:source jsfiddle="false">
<script>
function FetchCntl($xhr) {
var self = this;
this.url = 'index.html';
this.fetch = function() {
self.code = null;
self.response = null;
$xhr(self.method, self.url, function(code, response) {
self.code = code;
self.response = response;
}, function(code, response) {
self.code = code;
self.response = response || "Request failed";
});
};
this.updateModel = function(method, url) {
self.method = method;
self.url = url;
};
}
FetchCntl.$inject = ['$xhr'];
</script>
<div ng:controller="FetchCntl">
<select ng:model="method">
<option>GET</option>
<option>JSONP</option>
</select>
<input type="text" ng:model="url" size="80"/>
<button ng:click="fetch()">fetch</button><br>
<button ng:click="updateModel('GET', 'index.html')">Sample GET</button>
<button ng:click="updateModel('JSONP', 'http://angularjs.org/greet.php?callback=JSON_CALLBACK&name=Super%20Hero')">Sample JSONP</button>
<button ng:click="updateModel('JSONP', 'http://angularjs.org/doesntexist&callback=JSON_CALLBACK')">Invalid JSONP</button>
<pre>code={{code}}</pre>
<pre>response={{response}}</pre>
</div>
</doc:source>
<doc:scenario>
it('should make xhr GET request', function() {
element(':button:contains("Sample GET")').click();
element(':button:contains("fetch")').click();
expect(binding('code')).toBe('code=200');
expect(binding('response')).toMatch(/angularjs.org/);
});
it('should make JSONP request to the angularjs.org', function() {
element(':button:contains("Sample JSONP")').click();
element(':button:contains("fetch")').click();
expect(binding('code')).toBe('code=200');
expect(binding('response')).toMatch(/Super Hero!/);
});
it('should make JSONP request to invalid URL and invoke the error handler',
function() {
element(':button:contains("Invalid JSONP")').click();
element(':button:contains("fetch")').click();
expect(binding('code')).toBe('code=-2');
expect(binding('response')).toBe('response=Request failed');
});
</doc:scenario>
</doc:example>
*/
function $XhrProvider() {
this.$get = ['$rootScope', '$browser', '$xhr.error', '$log',
function( $rootScope, $browser, $error, $log){
var xhrHeaderDefaults = {
common: {
"Accept": "application/json, text/plain, */*",
"X-Requested-With": "XMLHttpRequest"
},
post: {'Content-Type': 'application/x-www-form-urlencoded'},
get: {}, // all these empty properties are needed so that client apps can just do:
head: {}, // $xhr.defaults.headers.head.foo="bar" without having to create head object
put: {}, // it also means that if we add a header for these methods in the future, it
'delete': {}, // won't be easily silently lost due to an object assignment.
patch: {}
};
function xhr(method, url, post, success, error) {
if (isFunction(post)) {
error = success;
success = post;
post = null;
}
if (post && isObject(post)) {
post = toJson(post);
}
$browser.xhr(method, url, post, function(code, response){
try {
if (isString(response)) {
if (response.match(/^\)\]\}',\n/)) response=response.substr(6);
if (/^\s*[\[\{]/.exec(response) && /[\}\]]\s*$/.exec(response)) {
response = fromJson(response, true);
}
}
$rootScope.$apply(function() {
if (200 <= code && code < 300) {
success(code, response);
} else if (isFunction(error)) {
error(code, response);
} else {
$error(
{method: method, url: url, data: post, success: success},
{status: code, body: response});
}
});
} catch (e) {
$log.error(e);
}
}, extend({'X-XSRF-TOKEN': $browser.cookies()['XSRF-TOKEN']},
xhrHeaderDefaults.common,
xhrHeaderDefaults[lowercase(method)]));
}
xhr.defaults = {headers: xhrHeaderDefaults};
return xhr;
}];
}

View file

@ -90,12 +90,15 @@ angularWidget('ng:include', function(element){
this.directives(true);
} else {
element[0]['ng:compiled'] = true;
return ['$xhr.cache', '$autoScroll', '$element', function($xhr, $autoScroll, element) {
return ['$http', '$cacheFactory', '$autoScroll', '$element',
function($http, $cacheFactory, $autoScroll, element) {
var scope = this,
changeCounter = 0,
releaseScopes = [],
childScope,
oldScope;
oldScope,
// TODO(vojta): configure the cache / extract into $tplCache service ?
cache = $cacheFactory.get('templates') || $cacheFactory('templates');
function incrementChange() { changeCounter++;}
this.$watch(srcExp, incrementChange);
@ -108,26 +111,42 @@ angularWidget('ng:include', function(element){
});
this.$watch(function() {return changeCounter;}, function(scope) {
var src = scope.$eval(srcExp),
useScope = scope.$eval(scopeExp);
useScope = scope.$eval(scopeExp),
fromCache;
function updateContent(content) {
element.html(content);
if (useScope) {
childScope = useScope;
} else {
releaseScopes.push(childScope = scope.$new());
}
compiler.compile(element)(childScope);
$autoScroll();
scope.$eval(onloadExp);
}
function clearContent() {
childScope = null;
element.html('');
}
while(releaseScopes.length) {
releaseScopes.pop().$destroy();
}
if (src) {
$xhr('GET', src, null, function(code, response) {
element.html(response);
if (useScope) {
childScope = useScope;
} else {
releaseScopes.push(childScope = scope.$new());
}
compiler.compile(element)(childScope);
$autoScroll();
scope.$eval(onloadExp);
}, false, true);
if ((fromCache = cache.get(src))) {
scope.$evalAsync(function() {
updateContent(fromCache);
});
} else {
$http.get(src).on('success', function(response) {
updateContent(response);
cache.put(src, response);
}).on('error', clearContent);
}
} else {
childScope = null;
element.html('');
clearContent();
}
});
}];
@ -556,29 +575,49 @@ angularWidget('ng:view', function(element) {
if (!element[0]['ng:compiled']) {
element[0]['ng:compiled'] = true;
return ['$xhr.cache', '$route', '$autoScroll', '$element', function($xhr, $route, $autoScroll, element) {
return ['$http', '$cacheFactory', '$route', '$autoScroll', '$element',
function($http, $cacheFactory, $route, $autoScroll, element) {
var template;
var changeCounter = 0;
// TODO(vojta): configure the cache / extract into $tplCache service ?
var cache = $cacheFactory.get('templates') || $cacheFactory('templates');
this.$on('$afterRouteChange', function() {
changeCounter++;
});
this.$watch(function() {return changeCounter;}, function(scope, newChangeCounter) {
var template = $route.current && $route.current.template;
if (template) {
//xhr's callback must be async, see commit history for more info
$xhr('GET', template, function(code, response) {
// ignore callback if another route change occured since
if (newChangeCounter == changeCounter) {
element.html(response);
compiler.compile(element)($route.current.scope);
$autoScroll();
}
});
} else {
var template = $route.current && $route.current.template,
fromCache;
function updateContent(content) {
element.html(content);
compiler.compile(element)($route.current.scope);
}
function clearContent() {
element.html('');
}
if (template) {
if ((fromCache = cache.get(template))) {
scope.$evalAsync(function() {
updateContent(fromCache);
});
} else {
// xhr's callback must be async, see commit history for more info
$http.get(template).on('success', function(response) {
// ignore callback if another route change occured since
if (newChangeCounter == changeCounter)
updateContent(response);
cache.put(template, response);
$autoScroll();
}).on('error', clearContent);
}
} else {
clearContent();
}
});
}];
} else {

View file

@ -1,6 +1,6 @@
'use strict';
describe("resource", function() {
xdescribe("resource", function() {
var resource, CreditCard, callback;
function nakedExpect(obj) {

View file

@ -502,12 +502,12 @@ describe("directive", function() {
expect(element.text()).toEqual('hey dude!');
}));
it('should infer injection arguments', inject(function($rootScope, $compile, $xhr) {
temp.MyController = function($xhr){
this.$root.someService = $xhr;
it('should infer injection arguments', inject(function($rootScope, $compile, $http) {
temp.MyController = function($http) {
this.$root.someService = $http;
};
var element = $compile('<div ng:controller="temp.MyController"></div>')($rootScope);
expect($rootScope.someService).toBe($xhr);
expect($rootScope.someService).toBe($http);
}));
});

View file

@ -124,7 +124,7 @@ describe('browser', function() {
// We don't have unit tests for IE because script.readyState is readOnly.
// Instead we run e2e tests on all browsers - see e2e for $xhr.
// Instead we run e2e tests on all browsers - see e2e for $http.
if (!msie) {
it('should add script tag for JSONP request', function() {

983
test/service/httpSpec.js Normal file
View file

@ -0,0 +1,983 @@
'use strict';
// TODO(vojta): refactor these tests to use new inject() syntax
describe('$http', function() {
var $http, $browser, $exceptionHandler, // services
method, url, data, headers, timeout, // passed arguments
onSuccess, onError, // callback spies
scope, errorLogs, respond, rawXhrObject, future;
beforeEach(inject(function($injector) {
$injector.get('$exceptionHandlerProvider').mode('log');
scope = $injector.get('$rootScope');
$http = $injector.get('$http');
$browser = $injector.get('$browser');
$exceptionHandler = $injector.get('$exceptionHandler');
// TODO(vojta): move this into mock browser ?
respond = method = url = data = headers = null;
rawXhrObject = {
abort: jasmine.createSpy('request.abort'),
getResponseHeader: function(h) {return h + '-val';},
getAllResponseHeaders: function() {
return 'content-encoding: gzip\nserver: Apache\n';
}
};
spyOn(scope, '$apply');
spyOn($browser, 'xhr').andCallFake(function(m, u, d, c, h, t) {
method = m;
url = u;
data = d;
respond = c;
headers = h;
timeout = t;
return rawXhrObject;
});
}));
afterEach(function() {
// expect($exceptionHandler.errors.length).toBe(0);
});
function doCommonXhr(method, url) {
future = $http({method: method || 'GET', url: url || '/url'});
onSuccess = jasmine.createSpy('on200');
onError = jasmine.createSpy('on400');
future.on('200', onSuccess);
future.on('400', onError);
return future;
}
it('should do basic request', function() {
$http({url: '/url', method: 'GET'});
expect($browser.xhr).toHaveBeenCalledOnce();
expect(url).toBe('/url');
expect(method).toBe('GET');
});
it('should pass data if specified', function() {
$http({url: '/url', method: 'POST', data: 'some-data'});
expect($browser.xhr).toHaveBeenCalledOnce();
expect(data).toBe('some-data');
});
it('should pass timeout if specified', function() {
$http({url: '/url', method: 'POST', timeout: 5000});
expect($browser.xhr).toHaveBeenCalledOnce();
expect(timeout).toBe(5000);
});
describe('callbacks', function() {
beforeEach(doCommonXhr);
it('should log exceptions', function() {
onSuccess.andThrow('exception in success callback');
onError.andThrow('exception in error callback');
respond(200, 'content');
expect($exceptionHandler.errors.pop()).toContain('exception in success callback');
respond(400, '');
expect($exceptionHandler.errors.pop()).toContain('exception in error callback');
});
it('should log more exceptions', function() {
onError.andThrow('exception in error callback');
future.on('500', onError).on('50x', onError);
respond(500, '');
expect($exceptionHandler.errors.length).toBe(2);
$exceptionHandler.errors = [];
});
it('should get response as first param', function() {
respond(200, 'response');
expect(onSuccess).toHaveBeenCalledOnce();
expect(onSuccess.mostRecentCall.args[0]).toBe('response');
respond(400, 'empty');
expect(onError).toHaveBeenCalledOnce();
expect(onError.mostRecentCall.args[0]).toBe('empty');
});
it('should get status code as second param', function() {
respond(200, 'response');
expect(onSuccess).toHaveBeenCalledOnce();
expect(onSuccess.mostRecentCall.args[1]).toBe(200);
respond(400, 'empty');
expect(onError).toHaveBeenCalledOnce();
expect(onError.mostRecentCall.args[1]).toBe(400);
});
});
describe('response headers', function() {
var callback;
beforeEach(function() {
callback = jasmine.createSpy('callback');
});
it('should return single header', function() {
callback.andCallFake(function(r, s, header) {
expect(header('date')).toBe('date-val');
});
$http({url: '/url', method: 'GET'}).on('200', callback);
respond(200, '');
expect(callback).toHaveBeenCalledOnce();
});
it('should return null when single header does not exist', function() {
callback.andCallFake(function(r, s, header) {
header(); // we need that to get headers parsed first
expect(header('nothing')).toBe(null);
});
$http({url: '/url', method: 'GET'}).on('200', callback);
respond(200, '');
expect(callback).toHaveBeenCalledOnce();
});
it('should return all headers as object', function() {
callback.andCallFake(function(r, s, header) {
expect(header()).toEqual({'content-encoding': 'gzip', 'server': 'Apache'});
});
$http({url: '/url', method: 'GET'}).on('200', callback);
respond(200, '');
expect(callback).toHaveBeenCalledOnce();
});
it('should return empty object for jsonp request', function() {
// jsonp doesn't return raw object
rawXhrObject = undefined;
callback.andCallFake(function(r, s, headers) {
expect(headers()).toEqual({});
});
$http({url: '/some', method: 'JSONP'}).on('200', callback);
respond(200, '');
expect(callback).toHaveBeenCalledOnce();
});
});
describe('response headers parser', function() {
it('should parse basic', function() {
var parsed = parseHeaders(
'date: Thu, 04 Aug 2011 20:23:08 GMT\n' +
'content-encoding: gzip\n' +
'transfer-encoding: chunked\n' +
'x-cache-info: not cacheable; response has already expired, not cacheable; response has already expired\n' +
'connection: Keep-Alive\n' +
'x-backend-server: pm-dekiwiki03\n' +
'pragma: no-cache\n' +
'server: Apache\n' +
'x-frame-options: DENY\n' +
'content-type: text/html; charset=utf-8\n' +
'vary: Cookie, Accept-Encoding\n' +
'keep-alive: timeout=5, max=1000\n' +
'expires: Thu: , 19 Nov 1981 08:52:00 GMT\n');
expect(parsed['date']).toBe('Thu, 04 Aug 2011 20:23:08 GMT');
expect(parsed['content-encoding']).toBe('gzip');
expect(parsed['transfer-encoding']).toBe('chunked');
expect(parsed['keep-alive']).toBe('timeout=5, max=1000');
});
it('should parse lines without space after colon', function() {
expect(parseHeaders('key:value').key).toBe('value');
});
it('should trim the values', function() {
expect(parseHeaders('key: value ').key).toBe('value');
});
it('should allow headers without value', function() {
expect(parseHeaders('key:').key).toBe('');
});
it('should merge headers with same key', function() {
expect(parseHeaders('key: a\nkey:b\n').key).toBe('a, b');
});
it('should normalize keys to lower case', function() {
expect(parseHeaders('KeY: value').key).toBe('value');
});
it('should parse CRLF as delimiter', function() {
// IE does use CRLF
expect(parseHeaders('a: b\r\nc: d\r\n')).toEqual({a: 'b', c: 'd'});
expect(parseHeaders('a: b\r\nc: d\r\n').a).toBe('b');
});
it('should parse tab after semi-colon', function() {
expect(parseHeaders('a:\tbb').a).toBe('bb');
expect(parseHeaders('a: \tbb').a).toBe('bb');
});
});
describe('request headers', function() {
it('should send custom headers', function() {
$http({url: '/url', method: 'GET', headers: {
'Custom': 'header',
'Content-Type': 'application/json'
}});
expect(headers['Custom']).toEqual('header');
expect(headers['Content-Type']).toEqual('application/json');
});
it('should set default headers for GET request', function() {
$http({url: '/url', method: 'GET', headers: {}});
expect(headers['Accept']).toBe('application/json, text/plain, */*');
expect(headers['X-Requested-With']).toBe('XMLHttpRequest');
});
it('should set default headers for POST request', function() {
$http({url: '/url', method: 'POST', headers: {}});
expect(headers['Accept']).toBe('application/json, text/plain, */*');
expect(headers['X-Requested-With']).toBe('XMLHttpRequest');
expect(headers['Content-Type']).toBe('application/json');
});
it('should set default headers for PUT request', function() {
$http({url: '/url', method: 'PUT', headers: {}});
expect(headers['Accept']).toBe('application/json, text/plain, */*');
expect(headers['X-Requested-With']).toBe('XMLHttpRequest');
expect(headers['Content-Type']).toBe('application/json');
});
it('should set default headers for custom HTTP method', function() {
$http({url: '/url', method: 'FOO', headers: {}});
expect(headers['Accept']).toBe('application/json, text/plain, */*');
expect(headers['X-Requested-With']).toBe('XMLHttpRequest');
});
it('should override default headers with custom', function() {
$http({url: '/url', method: 'POST', headers: {
'Accept': 'Rewritten',
'Content-Type': 'Rewritten'
}});
expect(headers['Accept']).toBe('Rewritten');
expect(headers['X-Requested-With']).toBe('XMLHttpRequest');
expect(headers['Content-Type']).toBe('Rewritten');
});
it('should set the XSRF cookie into a XSRF header', function() {
$browser.cookies('XSRF-TOKEN', 'secret');
$http({url: '/url', method: 'GET'});
expect(headers['X-XSRF-TOKEN']).toBe('secret');
$http({url: '/url', method: 'POST', headers: {'S-ome': 'Header'}});
expect(headers['X-XSRF-TOKEN']).toBe('secret');
$http({url: '/url', method: 'PUT', headers: {'Another': 'Header'}});
expect(headers['X-XSRF-TOKEN']).toBe('secret');
$http({url: '/url', method: 'DELETE', headers: {}});
expect(headers['X-XSRF-TOKEN']).toBe('secret');
});
});
describe('short methods', function() {
it('should have .get()', function() {
$http.get('/url');
expect(method).toBe('GET');
expect(url).toBe('/url');
});
it('.get() should allow config param', function() {
$http.get('/url', {headers: {'Custom': 'Header'}});
expect(method).toBe('GET');
expect(url).toBe('/url');
expect(headers['Custom']).toBe('Header');
});
it('should have .delete()', function() {
$http['delete']('/url');
expect(method).toBe('DELETE');
expect(url).toBe('/url');
});
it('.delete() should allow config param', function() {
$http['delete']('/url', {headers: {'Custom': 'Header'}});
expect(method).toBe('DELETE');
expect(url).toBe('/url');
expect(headers['Custom']).toBe('Header');
});
it('should have .head()', function() {
$http.head('/url');
expect(method).toBe('HEAD');
expect(url).toBe('/url');
});
it('.head() should allow config param', function() {
$http.head('/url', {headers: {'Custom': 'Header'}});
expect(method).toBe('HEAD');
expect(url).toBe('/url');
expect(headers['Custom']).toBe('Header');
});
it('should have .patch()', function() {
$http.patch('/url');
expect(method).toBe('PATCH');
expect(url).toBe('/url');
});
it('.patch() should allow config param', function() {
$http.patch('/url', {headers: {'Custom': 'Header'}});
expect(method).toBe('PATCH');
expect(url).toBe('/url');
expect(headers['Custom']).toBe('Header');
});
it('should have .post()', function() {
$http.post('/url', 'some-data');
expect(method).toBe('POST');
expect(url).toBe('/url');
expect(data).toBe('some-data');
});
it('.post() should allow config param', function() {
$http.post('/url', 'some-data', {headers: {'Custom': 'Header'}});
expect(method).toBe('POST');
expect(url).toBe('/url');
expect(data).toBe('some-data');
expect(headers['Custom']).toBe('Header');
});
it('should have .put()', function() {
$http.put('/url', 'some-data');
expect(method).toBe('PUT');
expect(url).toBe('/url');
expect(data).toBe('some-data');
});
it('.put() should allow config param', function() {
$http.put('/url', 'some-data', {headers: {'Custom': 'Header'}});
expect(method).toBe('PUT');
expect(url).toBe('/url');
expect(data).toBe('some-data');
expect(headers['Custom']).toBe('Header');
});
it('should have .jsonp()', function() {
$http.jsonp('/url');
expect(method).toBe('JSONP');
expect(url).toBe('/url');
});
it('.jsonp() should allow config param', function() {
$http.jsonp('/url', {headers: {'Custom': 'Header'}});
expect(method).toBe('JSONP');
expect(url).toBe('/url');
expect(headers['Custom']).toBe('Header');
});
});
describe('future', function() {
describe('abort', function() {
beforeEach(doCommonXhr);
it('should return itself to allow chaining', function() {
expect(future.abort()).toBe(future);
});
it('should allow aborting the request', function() {
future.abort();
expect(rawXhrObject.abort).toHaveBeenCalledOnce();
});
it('should not abort already finished request', function() {
respond(200, 'content');
future.abort();
expect(rawXhrObject.abort).not.toHaveBeenCalled();
});
});
describe('retry', function() {
it('should retry last request with same callbacks', function() {
doCommonXhr('HEAD', '/url-x');
respond(200, '');
$browser.xhr.reset();
onSuccess.reset();
future.retry();
expect($browser.xhr).toHaveBeenCalledOnce();
expect(method).toBe('HEAD');
expect(url).toBe('/url-x');
respond(200, 'body');
expect(onSuccess).toHaveBeenCalledOnce();
});
it('should return itself to allow chaining', function() {
doCommonXhr();
respond(200, '');
expect(future.retry()).toBe(future);
});
it('should throw error when pending request', function() {
doCommonXhr();
expect(future.retry).toThrow('Can not retry request. Abort pending request first.');
});
});
describe('on', function() {
var callback;
beforeEach(function() {
future = $http({method: 'GET', url: '/url'});
callback = jasmine.createSpy('callback');
});
it('should return itself to allow chaining', function() {
expect(future.on('200', noop)).toBe(future);
});
it('should call exact status code callback', function() {
future.on('205', callback);
respond(205, '');
expect(callback).toHaveBeenCalledOnce();
});
it('should match 2xx', function() {
future.on('2xx', callback);
respond(200, '');
respond(201, '');
respond(266, '');
respond(400, '');
respond(300, '');
expect(callback).toHaveBeenCalled();
expect(callback.callCount).toBe(3);
});
it('should match 20x', function() {
future.on('20x', callback);
respond(200, '');
respond(201, '');
respond(205, '');
respond(400, '');
respond(300, '');
respond(210, '');
respond(255, '');
expect(callback).toHaveBeenCalled();
expect(callback.callCount).toBe(3);
});
it('should match 2x1', function() {
future.on('2x1', callback);
respond(201, '');
respond(211, '');
respond(251, '');
respond(400, '');
respond(300, '');
respond(210, '');
respond(255, '');
expect(callback).toHaveBeenCalled();
expect(callback.callCount).toBe(3);
});
it('should match xxx', function() {
future.on('xxx', callback);
respond(201, '');
respond(211, '');
respond(251, '');
respond(404, '');
respond(501, '');
expect(callback).toHaveBeenCalled();
expect(callback.callCount).toBe(5);
});
it('should call all matched callbacks', function() {
var no = jasmine.createSpy('wrong');
future.on('xxx', callback);
future.on('2xx', callback);
future.on('205', callback);
future.on('3xx', no);
future.on('2x1', no);
future.on('4xx', no);
respond(205, '');
expect(callback).toHaveBeenCalled();
expect(callback.callCount).toBe(3);
expect(no).not.toHaveBeenCalled();
});
it('should allow list of status patterns', function() {
future.on('2xx,3xx', callback);
respond(405, '');
expect(callback).not.toHaveBeenCalled();
respond(201);
expect(callback).toHaveBeenCalledOnce();
respond(301);
expect(callback.callCount).toBe(2);
});
it('should preserve the order of listeners', function() {
var log = '';
future.on('2xx', function() {log += '1';});
future.on('201', function() {log += '2';});
future.on('2xx', function() {log += '3';});
respond(201);
expect(log).toBe('123');
});
it('should know "success" alias', function() {
future.on('success', callback);
respond(200, '');
expect(callback).toHaveBeenCalledOnce();
callback.reset();
respond(201, '');
expect(callback).toHaveBeenCalledOnce();
callback.reset();
respond(250, '');
expect(callback).toHaveBeenCalledOnce();
callback.reset();
respond(404, '');
respond(501, '');
expect(callback).not.toHaveBeenCalled();
});
it('should know "error" alias', function() {
future.on('error', callback);
respond(401, '');
expect(callback).toHaveBeenCalledOnce();
callback.reset();
respond(500, '');
expect(callback).toHaveBeenCalledOnce();
callback.reset();
respond(0, '');
expect(callback).toHaveBeenCalledOnce();
callback.reset();
respond(201, '');
respond(200, '');
respond(300, '');
expect(callback).not.toHaveBeenCalled();
});
it('should know "always" alias', function() {
future.on('always', callback);
respond(201, '');
respond(200, '');
respond(300, '');
respond(401, '');
respond(502, '');
respond(0, '');
respond(-1, '');
respond(-2, '');
expect(callback).toHaveBeenCalled();
expect(callback.callCount).toBe(8);
});
it('should call "xxx" when 0 status code', function() {
future.on('xxx', callback);
respond(0, '');
expect(callback).toHaveBeenCalledOnce();
});
it('should not call "2xx" when 0 status code', function() {
future.on('2xx', callback);
respond(0, '');
expect(callback).not.toHaveBeenCalled();
});
it('should normalize internal statuses -1, -2 to 0', function() {
callback.andCallFake(function(response, status) {
expect(status).toBe(0);
});
future.on('xxx', callback);
respond(-1, '');
respond(-2, '');
expect(callback).toHaveBeenCalled();
expect(callback.callCount).toBe(2);
});
it('should match "timeout" when -1 internal status', function() {
future.on('timeout', callback);
respond(-1, '');
expect(callback).toHaveBeenCalledOnce();
});
it('should match "abort" when 0 status', function() {
future.on('abort', callback);
respond(0, '');
expect(callback).toHaveBeenCalledOnce();
});
it('should match "error" when 0, -1, or -2', function() {
future.on('error', callback);
respond(0, '');
respond(-1, '');
respond(-2, '');
expect(callback).toHaveBeenCalled();
expect(callback.callCount).toBe(3);
});
});
});
describe('scope.$apply', function() {
beforeEach(doCommonXhr);
it('should $apply after success callback', function() {
respond(200, '');
expect(scope.$apply).toHaveBeenCalledOnce();
});
it('should $apply after error callback', function() {
respond(404, '');
expect(scope.$apply).toHaveBeenCalledOnce();
});
it('should $apply even if exception thrown during callback', function() {
onSuccess.andThrow('error in callback');
onError.andThrow('error in callback');
respond(200, '');
expect(scope.$apply).toHaveBeenCalledOnce();
scope.$apply.reset();
respond(400, '');
expect(scope.$apply).toHaveBeenCalledOnce();
$exceptionHandler.errors = [];
});
});
describe('transform', function() {
describe('request', function() {
describe('default', function() {
it('should transform object into json', function() {
$http({method: 'POST', url: '/url', data: {one: 'two'}});
expect(data).toBe('{"one":"two"}');
});
it('should ignore strings', function() {
$http({method: 'POST', url: '/url', data: 'string-data'});
expect(data).toBe('string-data');
});
});
});
describe('response', function() {
describe('default', function() {
it('should deserialize json objects', function() {
doCommonXhr();
respond(200, '{"foo":"bar","baz":23}');
expect(onSuccess.mostRecentCall.args[0]).toEqual({foo: 'bar', baz: 23});
});
it('should deserialize json arrays', function() {
doCommonXhr();
respond(200, '[1, "abc", {"foo":"bar"}]');
expect(onSuccess.mostRecentCall.args[0]).toEqual([1, 'abc', {foo: 'bar'}]);
});
it('should deserialize json with security prefix', function() {
doCommonXhr();
respond(200, ')]}\',\n[1, "abc", {"foo":"bar"}]');
expect(onSuccess.mostRecentCall.args[0]).toEqual([1, 'abc', {foo:'bar'}]);
});
});
it('should pipeline more functions', function() {
function first(d) {return d + '1';}
function second(d) {return d + '2';}
onSuccess = jasmine.createSpy('onSuccess');
$http({method: 'POST', url: '/url', data: '0', transformResponse: [first, second]})
.on('200', onSuccess);
respond(200, '0');
expect(onSuccess).toHaveBeenCalledOnce();
expect(onSuccess.mostRecentCall.args[0]).toBe('012');
});
});
});
describe('cache', function() {
function doFirstCacheRequest(method, responseStatus) {
onSuccess = jasmine.createSpy('on200');
$http({method: method || 'get', url: '/url', cache: true});
respond(responseStatus || 200, 'content');
$browser.xhr.reset();
}
it('should cache GET request', function() {
doFirstCacheRequest();
$http({method: 'get', url: '/url', cache: true}).on('200', onSuccess);
$browser.defer.flush();
expect(onSuccess).toHaveBeenCalledOnce();
expect(onSuccess.mostRecentCall.args[0]).toBe('content');
expect($browser.xhr).not.toHaveBeenCalled();
});
it('should always call callback asynchronously', function() {
doFirstCacheRequest();
$http({method: 'get', url: '/url', cache: true}).on('200', onSuccess);
expect(onSuccess).not.toHaveBeenCalled();
});
it('should not cache POST request', function() {
doFirstCacheRequest('post');
$http({method: 'post', url: '/url', cache: true}).on('200', onSuccess);
$browser.defer.flush();
expect(onSuccess).not.toHaveBeenCalled();
expect($browser.xhr).toHaveBeenCalledOnce();
});
it('should not cache PUT request', function() {
doFirstCacheRequest('put');
$http({method: 'put', url: '/url', cache: true}).on('200', onSuccess);
$browser.defer.flush();
expect(onSuccess).not.toHaveBeenCalled();
expect($browser.xhr).toHaveBeenCalledOnce();
});
it('should not cache DELETE request', function() {
doFirstCacheRequest('delete');
$http({method: 'delete', url: '/url', cache: true}).on('200', onSuccess);
$browser.defer.flush();
expect(onSuccess).not.toHaveBeenCalled();
expect($browser.xhr).toHaveBeenCalledOnce();
});
it('should not cache non 2xx responses', function() {
doFirstCacheRequest('get', 404);
$http({method: 'get', url: '/url', cache: true}).on('200', onSuccess);
$browser.defer.flush();
expect(onSuccess).not.toHaveBeenCalled();
expect($browser.xhr).toHaveBeenCalledOnce();
});
it('should cache the headers as well', function() {
doFirstCacheRequest();
onSuccess.andCallFake(function(r, s, headers) {
expect(headers()).toEqual({'content-encoding': 'gzip', 'server': 'Apache'});
expect(headers('server')).toBe('Apache');
});
$http({method: 'get', url: '/url', cache: true}).on('200', onSuccess);
$browser.defer.flush();
expect(onSuccess).toHaveBeenCalledOnce();
});
it('should cache status code as well', function() {
doFirstCacheRequest('get', 201);
onSuccess.andCallFake(function(r, status, h) {
expect(status).toBe(201);
});
$http({method: 'get', url: '/url', cache: true}).on('2xx', onSuccess);
$browser.defer.flush();
expect(onSuccess).toHaveBeenCalledOnce();
});
});
describe('pendingCount', function() {
it('should return number of pending requests', function() {
expect($http.pendingCount()).toBe(0);
$http({method: 'get', url: '/some'});
expect($http.pendingCount()).toBe(1);
respond(200, '');
expect($http.pendingCount()).toBe(0);
});
it('should decrement the counter when request aborted', function() {
future = $http({method: 'get', url: '/x'});
expect($http.pendingCount()).toBe(1);
future.abort();
respond(0, '');
expect($http.pendingCount()).toBe(0);
});
it('should decrement the counter when served from cache', function() {
$http({method: 'get', url: '/cached', cache: true});
respond(200, 'content');
expect($http.pendingCount()).toBe(0);
$http({method: 'get', url: '/cached', cache: true});
expect($http.pendingCount()).toBe(1);
$browser.defer.flush();
expect($http.pendingCount()).toBe(0);
});
it('should decrement the counter before firing callbacks', function() {
$http({method: 'get', url: '/cached'}).on('xxx', function() {
expect($http.pendingCount()).toBe(0);
});
expect($http.pendingCount()).toBe(1);
respond(200, 'content');
});
});
});

View file

@ -1,81 +0,0 @@
'use strict';
describe('$xhr.bulk', function() {
var log;
beforeEach(inject(function($provide) {
$provide.value('$xhr.error', jasmine.createSpy('$xhr.error'));
$provide.factory('$xhrError', ['$xhr.error', identity]);
$provide.factory('$xhrBulk', ['$xhr.bulk', identity]);
log = '';
}));
function callback(code, response) {
expect(code).toEqual(200);
log = log + toJson(response) + ';';
}
it('should collect requests', inject(function($browser, $xhrBulk) {
$xhrBulk.urls["/"] = {match:/.*/};
$xhrBulk('GET', '/req1', null, callback);
$xhrBulk('POST', '/req2', {post:'data'}, callback);
$browser.xhr.expectPOST('/', {
requests:[{method:'GET', url:'/req1', data: null},
{method:'POST', url:'/req2', data:{post:'data'} }]
}).respond([
{status:200, response:'first'},
{status:200, response:'second'}
]);
$xhrBulk.flush(function() { log += 'DONE';});
$browser.xhr.flush();
expect(log).toEqual('"first";"second";DONE');
}));
it('should handle non 200 status code by forwarding to error handler',
inject(function($browser, $xhrBulk, $xhrError) {
$xhrBulk.urls['/'] = {match:/.*/};
$xhrBulk('GET', '/req1', null, callback);
$xhrBulk('POST', '/req2', {post:'data'}, callback);
$browser.xhr.expectPOST('/', {
requests:[{method:'GET', url:'/req1', data: null},
{method:'POST', url:'/req2', data:{post:'data'} }]
}).respond([
{status:404, response:'NotFound'},
{status:200, response:'second'}
]);
$xhrBulk.flush(function() { log += 'DONE';});
$browser.xhr.flush();
expect($xhrError).toHaveBeenCalled();
var cb = $xhrError.mostRecentCall.args[0].success;
expect(typeof cb).toEqual('function');
expect($xhrError).toHaveBeenCalledWith(
{url: '/req1', method: 'GET', data: null, success: cb},
{status: 404, response: 'NotFound'});
expect(log).toEqual('"second";DONE');
}));
it('should handle non 200 status code by calling error callback if provided',
inject(function($browser, $xhrBulk, $xhrError) {
var callback = jasmine.createSpy('error');
$xhrBulk.urls['/'] = {match: /.*/};
$xhrBulk('GET', '/req1', null, noop, callback);
$browser.xhr.expectPOST('/', {
requests:[{method: 'GET', url: '/req1', data: null}]
}).respond([{status: 404, response: 'NotFound'}]);
$xhrBulk.flush();
$browser.xhr.flush();
expect($xhrError).not.toHaveBeenCalled();
expect(callback).toHaveBeenCalledWith(404, 'NotFound');
}));
});

View file

@ -1,175 +0,0 @@
'use strict';
describe('$xhr.cache', function() {
var log;
beforeEach(inject(function($provide) {
$provide.value('$xhr.error', jasmine.createSpy('$xhr.error'));
$provide.factory('$xhrError', ['$xhr.error', identity]);
$provide.factory('$xhrBulk', ['$xhr.bulk', identity]);
$provide.factory('$xhrCache', ['$xhr.cache', identity]);
log = '';
}));
function callback(code, response) {
expect(code).toEqual(200);
log = log + toJson(response) + ';';
}
it('should cache requests', inject(function($browser, $xhrCache) {
$browser.xhr.expectGET('/url').respond('first');
$xhrCache('GET', '/url', null, callback);
$browser.xhr.flush();
$browser.xhr.expectGET('/url').respond('ERROR');
$xhrCache('GET', '/url', null, callback);
$browser.defer.flush();
expect(log).toEqual('"first";"first";');
$xhrCache('GET', '/url', null, callback, false);
$browser.defer.flush();
expect(log).toEqual('"first";"first";"first";');
}));
it('should first return cache request, then return server request', inject(function($browser, $xhrCache) {
$browser.xhr.expectGET('/url').respond('first');
$xhrCache('GET', '/url', null, callback, true);
$browser.xhr.flush();
$browser.xhr.expectGET('/url').respond('ERROR');
$xhrCache('GET', '/url', null, callback, true);
$browser.defer.flush();
expect(log).toEqual('"first";"first";');
$browser.xhr.flush();
expect(log).toEqual('"first";"first";"ERROR";');
}));
it('should serve requests from cache', inject(function($browser, $xhrCache) {
$xhrCache.data.url = {value:'123'};
$xhrCache('GET', 'url', null, callback);
$browser.defer.flush();
expect(log).toEqual('"123";');
$xhrCache('GET', 'url', null, callback, false);
$browser.defer.flush();
expect(log).toEqual('"123";"123";');
}));
it('should keep track of in flight requests and request only once', inject(function($browser, $xhrCache, $xhrBulk) {
$xhrBulk.urls['/bulk'] = {
match:function(url){
return url == '/url';
}
};
$browser.xhr.expectPOST('/bulk', {
requests:[{method:'GET', url:'/url', data: null}]
}).respond([
{status:200, response:'123'}
]);
$xhrCache('GET', '/url', null, callback);
$xhrCache('GET', '/url', null, callback);
$xhrCache.delegate.flush();
$browser.xhr.flush();
expect(log).toEqual('"123";"123";');
}));
it('should clear cache on non GET', inject(function($browser, $xhrCache) {
$browser.xhr.expectPOST('abc', {}).respond({});
$xhrCache.data.url = {value:123};
$xhrCache('POST', 'abc', {});
expect($xhrCache.data.url).toBeUndefined();
}));
it('should call callback asynchronously for both cache hit and cache miss', inject(function($browser, $xhrCache) {
$browser.xhr.expectGET('/url').respond('+');
$xhrCache('GET', '/url', null, callback);
expect(log).toEqual(''); //callback hasn't executed
$browser.xhr.flush();
expect(log).toEqual('"+";'); //callback has executed
$xhrCache('GET', '/url', null, callback);
expect(log).toEqual('"+";'); //callback hasn't executed
$browser.defer.flush();
expect(log).toEqual('"+";"+";'); //callback has executed
}));
it('should call callback synchronously when sync flag is on', inject(function($browser, $xhrCache) {
$browser.xhr.expectGET('/url').respond('+');
$xhrCache('GET', '/url', null, callback, false, true);
expect(log).toEqual(''); //callback hasn't executed
$browser.xhr.flush();
expect(log).toEqual('"+";'); //callback has executed
$xhrCache('GET', '/url', null, callback, false, true);
expect(log).toEqual('"+";"+";'); //callback has executed
$browser.defer.flush();
expect(log).toEqual('"+";"+";'); //callback was not called again any more
}));
it('should call eval after callbacks for both cache hit and cache miss execute',
inject(function($browser, $xhrCache, $rootScope) {
var flushSpy = this.spyOn($rootScope, '$digest').andCallThrough();
$browser.xhr.expectGET('/url').respond('+');
$xhrCache('GET', '/url', null, callback);
expect(flushSpy).not.toHaveBeenCalled();
$browser.xhr.flush();
expect(flushSpy).toHaveBeenCalled();
flushSpy.reset(); //reset the spy
$xhrCache('GET', '/url', null, callback);
expect(flushSpy).not.toHaveBeenCalled();
$browser.defer.flush();
expect(flushSpy).toHaveBeenCalled();
}));
it('should call the error callback on error if provided', inject(function($browser, $xhrCache) {
var errorSpy = jasmine.createSpy('error'),
successSpy = jasmine.createSpy('success');
$browser.xhr.expectGET('/url').respond(500, 'error');
$xhrCache('GET', '/url', null, successSpy, errorSpy, false, true);
$browser.xhr.flush();
expect(errorSpy).toHaveBeenCalledWith(500, 'error');
expect(successSpy).not.toHaveBeenCalled();
errorSpy.reset();
$xhrCache('GET', '/url', successSpy, errorSpy, false, true);
$browser.xhr.flush();
expect(errorSpy).toHaveBeenCalledWith(500, 'error');
expect(successSpy).not.toHaveBeenCalled();
}));
it('should call the $xhr.error on error if error callback not provided',
inject(function($browser, $xhrCache, $xhrError) {
var errorSpy = jasmine.createSpy('error'),
successSpy = jasmine.createSpy('success');
$browser.xhr.expectGET('/url').respond(500, 'error');
$xhrCache('GET', '/url', null, successSpy, false, true);
$browser.xhr.flush();
expect(successSpy).not.toHaveBeenCalled();
expect($xhrError).toHaveBeenCalledWith(
{method: 'GET', url: '/url', data: null, success: successSpy},
{status: 500, body: 'error'});
}));
});

View file

@ -1,29 +0,0 @@
'use strict';
describe('$xhr.error', function() {
var log;
beforeEach(inject(function($provide) {
$provide.value('$xhr.error', jasmine.createSpy('$xhr.error'));
$provide.factory('$xhrError', ['$xhr.error', identity]);
log = '';
}));
function callback(code, response) {
expect(code).toEqual(200);
log = log + toJson(response) + ';';
}
it('should handle non 200 status codes by forwarding to error handler', inject(function($browser, $xhr, $xhrError) {
$browser.xhr.expectPOST('/req', 'MyData').respond(500, 'MyError');
$xhr('POST', '/req', 'MyData', callback);
$browser.xhr.flush();
var cb = $xhrError.mostRecentCall.args[0].success;
expect(typeof cb).toEqual('function');
expect($xhrError).toHaveBeenCalledWith(
{url: '/req', method: 'POST', data: 'MyData', success: cb},
{status: 500, body: 'MyError'});
}));
});

View file

@ -1,271 +0,0 @@
'use strict';
describe('$xhr', function() {
var log;
beforeEach(inject(function($provide) {
log = '';
$provide.value('$xhr.error', jasmine.createSpy('xhr.error'));
$provide.factory('$xhrError', ['$xhr.error', identity]);
}));
function callback(code, response) {
log = log + '{code=' + code + '; response=' + toJson(response) + '}';
}
it('should forward the request to $browser and decode JSON', inject(function($browser, $xhr) {
$browser.xhr.expectGET('/reqGET').respond('first');
$browser.xhr.expectGET('/reqGETjson').respond('["second"]');
$browser.xhr.expectPOST('/reqPOST', {post:'data'}).respond('third');
$xhr('GET', '/reqGET', null, callback);
$xhr('GET', '/reqGETjson', null, callback);
$xhr('POST', '/reqPOST', {post:'data'}, callback);
$browser.xhr.flush();
expect(log).toEqual(
'{code=200; response="third"}' +
'{code=200; response=["second"]}' +
'{code=200; response="first"}');
}));
it('should allow all 2xx requests', inject(function($browser, $xhr) {
$browser.xhr.expectGET('/req1').respond(200, '1');
$xhr('GET', '/req1', null, callback);
$browser.xhr.flush();
$browser.xhr.expectGET('/req2').respond(299, '2');
$xhr('GET', '/req2', null, callback);
$browser.xhr.flush();
expect(log).toEqual(
'{code=200; response="1"}' +
'{code=299; response="2"}');
}));
it('should handle exceptions in callback', inject(function($browser, $xhr, $log) {
$browser.xhr.expectGET('/reqGET').respond('first');
$xhr('GET', '/reqGET', null, function() { throw "MyException"; });
$browser.xhr.flush();
expect($log.error.logs.shift()).toContain('MyException');
}));
it('should automatically deserialize json objects', inject(function($browser, $xhr) {
var response;
$browser.xhr.expectGET('/foo').respond('{"foo":"bar","baz":23}');
$xhr('GET', '/foo', function(code, resp) {
response = resp;
});
$browser.xhr.flush();
expect(response).toEqual({foo:'bar', baz:23});
}));
it('should automatically deserialize json arrays', inject(function($browser, $xhr) {
var response;
$browser.xhr.expectGET('/foo').respond('[1, "abc", {"foo":"bar"}]');
$xhr('GET', '/foo', function(code, resp) {
response = resp;
});
$browser.xhr.flush();
expect(response).toEqual([1, 'abc', {foo:'bar'}]);
}));
it('should automatically deserialize json with security prefix', inject(function($browser, $xhr) {
var response;
$browser.xhr.expectGET('/foo').respond(')]}\',\n[1, "abc", {"foo":"bar"}]');
$xhr('GET', '/foo', function(code, resp) {
response = resp;
});
$browser.xhr.flush();
expect(response).toEqual([1, 'abc', {foo:'bar'}]);
}));
it('should call $xhr.error on error if no error callback provided', inject(function($browser, $xhr, $xhrError) {
var successSpy = jasmine.createSpy('success');
$browser.xhr.expectGET('/url').respond(500, 'error');
$xhr('GET', '/url', null, successSpy);
$browser.xhr.flush();
expect(successSpy).not.toHaveBeenCalled();
expect($xhrError).toHaveBeenCalledWith(
{method: 'GET', url: '/url', data: null, success: successSpy},
{status: 500, body: 'error'}
);
}));
it('should call the error callback on error if provided', inject(function($browser, $xhr) {
var errorSpy = jasmine.createSpy('error'),
successSpy = jasmine.createSpy('success');
$browser.xhr.expectGET('/url').respond(500, 'error');
$xhr('GET', '/url', null, successSpy, errorSpy);
$browser.xhr.flush();
expect(errorSpy).toHaveBeenCalledWith(500, 'error');
expect(successSpy).not.toHaveBeenCalled();
errorSpy.reset();
$xhr('GET', '/url', successSpy, errorSpy);
$browser.xhr.flush();
expect(errorSpy).toHaveBeenCalledWith(500, 'error');
expect(successSpy).not.toHaveBeenCalled();
}));
describe('http headers', function() {
describe('default headers', function() {
it('should set default headers for GET request', inject(function($browser, $xhr) {
var callback = jasmine.createSpy('callback');
$browser.xhr.expectGET('URL', '', {'Accept': 'application/json, text/plain, */*',
'X-Requested-With': 'XMLHttpRequest'}).
respond(234, 'OK');
$xhr('GET', 'URL', callback);
$browser.xhr.flush();
expect(callback).toHaveBeenCalled();
}));
it('should set default headers for POST request', inject(function($browser, $xhr) {
var callback = jasmine.createSpy('callback');
$browser.xhr.expectPOST('URL', 'xx', {'Accept': 'application/json, text/plain, */*',
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/x-www-form-urlencoded'}).
respond(200, 'OK');
$xhr('POST', 'URL', 'xx', callback);
$browser.xhr.flush();
expect(callback).toHaveBeenCalled();
}));
it('should set default headers for custom HTTP method', inject(function($browser, $xhr) {
var callback = jasmine.createSpy('callback');
$browser.xhr.expect('FOO', 'URL', '', {'Accept': 'application/json, text/plain, */*',
'X-Requested-With': 'XMLHttpRequest'}).
respond(200, 'OK');
$xhr('FOO', 'URL', callback);
$browser.xhr.flush();
expect(callback).toHaveBeenCalled();
}));
describe('custom headers', function() {
it('should allow appending a new header to the common defaults', inject(function($browser, $xhr) {
var callback = jasmine.createSpy('callback');
$browser.xhr.expectGET('URL', '', {'Accept': 'application/json, text/plain, */*',
'X-Requested-With': 'XMLHttpRequest',
'Custom-Header': 'value'}).
respond(200, 'OK');
$xhr.defaults.headers.common['Custom-Header'] = 'value';
$xhr('GET', 'URL', callback);
$browser.xhr.flush();
expect(callback).toHaveBeenCalled();
callback.reset();
$browser.xhr.expectPOST('URL', 'xx', {'Accept': 'application/json, text/plain, */*',
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/x-www-form-urlencoded',
'Custom-Header': 'value'}).
respond(200, 'OK');
$xhr('POST', 'URL', 'xx', callback);
$browser.xhr.flush();
expect(callback).toHaveBeenCalled();
}));
it('should allow appending a new header to a method specific defaults', inject(function($browser, $xhr) {
var callback = jasmine.createSpy('callback');
$browser.xhr.expectGET('URL', '', {'Accept': 'application/json, text/plain, */*',
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json'}).
respond(200, 'OK');
$xhr.defaults.headers.get['Content-Type'] = 'application/json';
$xhr('GET', 'URL', callback);
$browser.xhr.flush();
expect(callback).toHaveBeenCalled();
callback.reset();
$browser.xhr.expectPOST('URL', 'x', {'Accept': 'application/json, text/plain, */*',
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/x-www-form-urlencoded'}).
respond(200, 'OK');
$xhr('POST', 'URL', 'x', callback);
$browser.xhr.flush();
expect(callback).toHaveBeenCalled();
}));
it('should support overwriting and deleting default headers', inject(function($browser, $xhr) {
var callback = jasmine.createSpy('callback');
$browser.xhr.expectGET('URL', '', {'Accept': 'application/json, text/plain, */*'}).
respond(200, 'OK');
//delete a default header
delete $xhr.defaults.headers.common['X-Requested-With'];
$xhr('GET', 'URL', callback);
$browser.xhr.flush();
expect(callback).toHaveBeenCalled();
callback.reset();
$browser.xhr.expectPOST('URL', 'xx', {'Accept': 'application/json, text/plain, */*',
'Content-Type': 'application/json'}).
respond(200, 'OK');
//overwrite a default header
$xhr.defaults.headers.post['Content-Type'] = 'application/json';
$xhr('POST', 'URL', 'xx', callback);
$browser.xhr.flush();
expect(callback).toHaveBeenCalled();
}));
});
});
});
describe('xsrf', function() {
it('should copy the XSRF cookie into a XSRF Header', inject(function($browser, $xhr) {
var code, response;
$browser.xhr
.expectPOST('URL', 'DATA', {'X-XSRF-TOKEN': 'secret'})
.respond(234, 'OK');
$browser.cookies('XSRF-TOKEN', 'secret');
$xhr('POST', 'URL', 'DATA', function(c, r){
code = c;
response = r;
});
$browser.xhr.flush();
expect(code).toEqual(234);
expect(response).toEqual('OK');
}));
});
});

View file

@ -1,10 +1,6 @@
'use strict';
describe("widget", function() {
beforeEach(inject(function($provide){
$provide.factory('$xhrCache', ['$xhr.cache', identity]);
}));
describe('ng:switch', inject(function($rootScope, $compile) {
it('should switch on value change', inject(function($rootScope, $compile) {
var element = $compile(
@ -60,26 +56,26 @@ describe("widget", function() {
describe('ng:include', inject(function($rootScope, $compile) {
it('should include on external file', inject(function($rootScope, $compile, $xhrCache) {
it('should include on external file', inject(function($rootScope, $compile, $cacheFactory) {
var element = jqLite('<ng:include src="url" scope="childScope"></ng:include>');
var element = $compile(element)($rootScope);
$rootScope.childScope = $rootScope.$new();
$rootScope.childScope.name = 'misko';
$rootScope.url = 'myUrl';
$xhrCache.data.myUrl = {value:'{{name}}'};
$cacheFactory.get('templates').put('myUrl', '{{name}}');
$rootScope.$digest();
expect(element.text()).toEqual('misko');
}));
it('should remove previously included text if a falsy value is bound to src',
inject(function($rootScope, $compile, $xhrCache) {
inject(function($rootScope, $compile, $cacheFactory) {
var element = jqLite('<ng:include src="url" scope="childScope"></ng:include>');
var element = $compile(element)($rootScope);
$rootScope.childScope = $rootScope.$new();
$rootScope.childScope.name = 'igor';
$rootScope.url = 'myUrl';
$xhrCache.data.myUrl = {value:'{{name}}'};
$cacheFactory.get('templates').put('myUrl', '{{name}}');
$rootScope.$digest();
expect(element.text()).toEqual('igor');
@ -91,11 +87,11 @@ describe("widget", function() {
}));
it('should allow this for scope', inject(function($rootScope, $compile, $xhrCache) {
it('should allow this for scope', inject(function($rootScope, $compile, $cacheFactory) {
var element = jqLite('<ng:include src="url" scope="this"></ng:include>');
var element = $compile(element)($rootScope);
$rootScope.url = 'myUrl';
$xhrCache.data.myUrl = {value:'{{"abc"}}'};
$cacheFactory.get('templates').put('myUrl', '{{"abc"}}');
$rootScope.$digest();
// TODO(misko): because we are using scope==this, the eval gets registered
// during the flush phase and hence does not get called.
@ -108,28 +104,28 @@ describe("widget", function() {
it('should evaluate onload expression when a partial is loaded',
inject(function($rootScope, $compile, $xhrCache) {
inject(function($rootScope, $compile, $cacheFactory) {
var element = jqLite('<ng:include src="url" onload="loaded = true"></ng:include>');
var element = $compile(element)($rootScope);
expect($rootScope.loaded).not.toBeDefined();
$rootScope.url = 'myUrl';
$xhrCache.data.myUrl = {value:'my partial'};
$cacheFactory.get('templates').put('myUrl', 'my partial');
$rootScope.$digest();
expect(element.text()).toEqual('my partial');
expect($rootScope.loaded).toBe(true);
}));
it('should destroy old scope', inject(function($rootScope, $compile, $xhrCache) {
it('should destroy old scope', inject(function($rootScope, $compile, $cacheFactory) {
var element = jqLite('<ng:include src="url"></ng:include>');
var element = $compile(element)($rootScope);
expect($rootScope.$$childHead).toBeFalsy();
$rootScope.url = 'myUrl';
$xhrCache.data.myUrl = {value:'my partial'};
$cacheFactory.get('templates').put('myUrl', 'my partial');
$rootScope.$digest();
expect($rootScope.$$childHead).toBeTruthy();
@ -137,6 +133,55 @@ describe("widget", function() {
$rootScope.$digest();
expect($rootScope.$$childHead).toBeFalsy();
}));
it('should do xhr request and cache it', inject(function($rootScope, $browser, $compile) {
var element = $compile('<ng:include src="url"></ng:include>')($rootScope);
var $browserXhr = $browser.xhr;
$browserXhr.expectGET('myUrl').respond('my partial');
$rootScope.url = 'myUrl';
$rootScope.$digest();
$browserXhr.flush();
expect(element.text()).toEqual('my partial');
$rootScope.url = null;
$rootScope.$digest();
expect(element.text()).toEqual('');
$rootScope.url = 'myUrl';
$rootScope.$digest();
expect(element.text()).toEqual('my partial');
dealoc($rootScope);
}));
it('should clear content when error during xhr request',
inject(function($browser, $compile, $rootScope) {
var element = $compile('<ng:include src="url">content</ng:include>')($rootScope);
var $browserXhr = $browser.xhr;
$browserXhr.expectGET('myUrl').respond(404, '');
$rootScope.url = 'myUrl';
$rootScope.$digest();
$browserXhr.flush();
expect(element.text()).toBe('');
}));
it('should be async even if served from cache', inject(function($rootScope, $compile, $cacheFactory) {
var element = $compile('<ng:include src="url"></ng:include>')($rootScope);
$rootScope.url = 'myUrl';
$cacheFactory.get('templates').put('myUrl', 'my partial');
var called = 0;
// we want to assert only during first watch
$rootScope.$watch(function() {
if (!called++) expect(element.text()).toBe('');
});
$rootScope.$digest();
expect(element.text()).toBe('my partial');
}));
}));
@ -587,6 +632,36 @@ describe("widget", function() {
expect($rootScope.$element.text()).toEqual('2');
}));
it('should clear the content when error during xhr request',
inject(function($route, $location, $rootScope, $browser) {
$route.when('/foo', {controller: noop, template: 'myUrl1'});
$location.path('/foo');
$browser.xhr.expectGET('myUrl1').respond(404, '');
$rootScope.$element.text('content');
$rootScope.$digest();
$browser.xhr.flush();
expect($rootScope.$element.text()).toBe('');
}));
it('should be async even if served from cache',
inject(function($route, $rootScope, $location, $cacheFactory) {
$route.when('/foo', {controller: noop, template: 'myUrl1'});
$cacheFactory.get('templates').put('myUrl1', 'my partial');
$location.path('/foo');
var called = 0;
// we want to assert only during first watch
$rootScope.$watch(function() {
if (!called++) expect(element.text()).toBe('');
});
$rootScope.$digest();
expect(element.text()).toBe('my partial');
}));
});