mirror of
https://github.com/Hopiu/angular.js.git
synced 2026-03-22 01:10:23 +00:00
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:
parent
497839f583
commit
59adadca08
17 changed files with 1576 additions and 1095 deletions
5
angularFiles.js
vendored
5
angularFiles.js
vendored
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
428
src/service/http.js
Normal 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;
|
||||
};
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}];
|
||||
}
|
||||
|
|
@ -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;
|
||||
}];
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
|
@ -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;
|
||||
}];
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
describe("resource", function() {
|
||||
xdescribe("resource", function() {
|
||||
var resource, CreditCard, callback;
|
||||
|
||||
function nakedExpect(obj) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
983
test/service/httpSpec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
}));
|
||||
});
|
||||
|
|
@ -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'});
|
||||
}));
|
||||
});
|
||||
|
|
@ -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'});
|
||||
}));
|
||||
});
|
||||
|
|
@ -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');
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue