angular-scroll-animate
Version:
An Angular.js directive which allows you to perform any javascript actions (in the controller, or on the element) when an element is scrolled into, or out of, the users viewport, without any other dependencies.
1,501 lines (1,332 loc) • 82.3 kB
JavaScript
/**
* @license AngularJS v1.4.0
* (c) 2010-2015 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {
'use strict';
/**
* @ngdoc object
* @name angular.mock
* @description
*
* Namespace from 'angular-mocks.js' which contains testing related code.
*/
angular.mock = {};
/**
* ! This is a private undocumented service !
*
* @name $browser
*
* @description
* This service is a mock implementation of {@link ng.$browser}. It provides fake
* implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr,
* cookies, etc...
*
* The api of this service is the same as that of the real {@link ng.$browser $browser}, except
* that there are several helper methods available which can be used in tests.
*/
angular.mock.$BrowserProvider = function() {
this.$get = function() {
return new angular.mock.$Browser();
};
};
angular.mock.$Browser = function() {
var self = this;
this.isMock = true;
self.$$url = "http://server/";
self.$$lastUrl = self.$$url; // used by url polling fn
self.pollFns = [];
// TODO(vojta): remove this temporary api
self.$$completeOutstandingRequest = angular.noop;
self.$$incOutstandingRequestCount = angular.noop;
// register url polling fn
self.onUrlChange = function(listener) {
self.pollFns.push(
function() {
if (self.$$lastUrl !== self.$$url || self.$$state !== self.$$lastState) {
self.$$lastUrl = self.$$url;
self.$$lastState = self.$$state;
listener(self.$$url, self.$$state);
}
}
);
return listener;
};
self.$$applicationDestroyed = angular.noop;
self.$$checkUrlChange = angular.noop;
self.deferredFns = [];
self.deferredNextId = 0;
self.defer = function(fn, delay) {
delay = delay || 0;
self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId});
self.deferredFns.sort(function(a, b) { return a.time - b.time;});
return self.deferredNextId++;
};
/**
* @name $browser#defer.now
*
* @description
* Current milliseconds mock time.
*/
self.defer.now = 0;
self.defer.cancel = function(deferId) {
var fnIndex;
angular.forEach(self.deferredFns, function(fn, index) {
if (fn.id === deferId) fnIndex = index;
});
if (fnIndex !== undefined) {
self.deferredFns.splice(fnIndex, 1);
return true;
}
return false;
};
/**
* @name $browser#defer.flush
*
* @description
* Flushes all pending requests and executes the defer callbacks.
*
* @param {number=} number of milliseconds to flush. See {@link #defer.now}
*/
self.defer.flush = function(delay) {
if (angular.isDefined(delay)) {
self.defer.now += delay;
} else {
if (self.deferredFns.length) {
self.defer.now = self.deferredFns[self.deferredFns.length - 1].time;
} else {
throw new Error('No deferred tasks to be flushed');
}
}
while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) {
self.deferredFns.shift().fn();
}
};
self.$$baseHref = '/';
self.baseHref = function() {
return this.$$baseHref;
};
};
angular.mock.$Browser.prototype = {
/**
* @name $browser#poll
*
* @description
* run all fns in pollFns
*/
poll: function poll() {
angular.forEach(this.pollFns, function(pollFn) {
pollFn();
});
},
url: function(url, replace, state) {
if (angular.isUndefined(state)) {
state = null;
}
if (url) {
this.$$url = url;
// Native pushState serializes & copies the object; simulate it.
this.$$state = angular.copy(state);
return this;
}
return this.$$url;
},
state: function() {
return this.$$state;
},
notifyWhenNoOutstandingRequests: function(fn) {
fn();
}
};
/**
* @ngdoc provider
* @name $exceptionHandlerProvider
*
* @description
* Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors
* passed to the `$exceptionHandler`.
*/
/**
* @ngdoc service
* @name $exceptionHandler
*
* @description
* Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed
* to it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration
* information.
*
*
* ```js
* describe('$exceptionHandlerProvider', function() {
*
* it('should capture log messages and exceptions', function() {
*
* module(function($exceptionHandlerProvider) {
* $exceptionHandlerProvider.mode('log');
* });
*
* inject(function($log, $exceptionHandler, $timeout) {
* $timeout(function() { $log.log(1); });
* $timeout(function() { $log.log(2); throw 'banana peel'; });
* $timeout(function() { $log.log(3); });
* expect($exceptionHandler.errors).toEqual([]);
* expect($log.assertEmpty());
* $timeout.flush();
* expect($exceptionHandler.errors).toEqual(['banana peel']);
* expect($log.log.logs).toEqual([[1], [2], [3]]);
* });
* });
* });
* ```
*/
angular.mock.$ExceptionHandlerProvider = function() {
var handler;
/**
* @ngdoc method
* @name $exceptionHandlerProvider#mode
*
* @description
* Sets the logging mode.
*
* @param {string} mode Mode of operation, defaults to `rethrow`.
*
* - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log`
* mode stores an array of errors in `$exceptionHandler.errors`, to allow later
* assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and
* {@link ngMock.$log#reset reset()}
* - `rethrow`: If any errors are passed to the handler in tests, it typically means that there
* is a bug in the application or test, so this mock will make these tests fail.
* For any implementations that expect exceptions to be thrown, the `rethrow` mode
* will also maintain a log of thrown errors.
*/
this.mode = function(mode) {
switch (mode) {
case 'log':
case 'rethrow':
var errors = [];
handler = function(e) {
if (arguments.length == 1) {
errors.push(e);
} else {
errors.push([].slice.call(arguments, 0));
}
if (mode === "rethrow") {
throw e;
}
};
handler.errors = errors;
break;
default:
throw new Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!");
}
};
this.$get = function() {
return handler;
};
this.mode('rethrow');
};
/**
* @ngdoc service
* @name $log
*
* @description
* Mock implementation of {@link ng.$log} that gathers all logged messages in arrays
* (one array per logging level). These arrays are exposed as `logs` property of each of the
* level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`.
*
*/
angular.mock.$LogProvider = function() {
var debug = true;
function concat(array1, array2, index) {
return array1.concat(Array.prototype.slice.call(array2, index));
}
this.debugEnabled = function(flag) {
if (angular.isDefined(flag)) {
debug = flag;
return this;
} else {
return debug;
}
};
this.$get = function() {
var $log = {
log: function() { $log.log.logs.push(concat([], arguments, 0)); },
warn: function() { $log.warn.logs.push(concat([], arguments, 0)); },
info: function() { $log.info.logs.push(concat([], arguments, 0)); },
error: function() { $log.error.logs.push(concat([], arguments, 0)); },
debug: function() {
if (debug) {
$log.debug.logs.push(concat([], arguments, 0));
}
}
};
/**
* @ngdoc method
* @name $log#reset
*
* @description
* Reset all of the logging arrays to empty.
*/
$log.reset = function() {
/**
* @ngdoc property
* @name $log#log.logs
*
* @description
* Array of messages logged using {@link ng.$log#log `log()`}.
*
* @example
* ```js
* $log.log('Some Log');
* var first = $log.log.logs.unshift();
* ```
*/
$log.log.logs = [];
/**
* @ngdoc property
* @name $log#info.logs
*
* @description
* Array of messages logged using {@link ng.$log#info `info()`}.
*
* @example
* ```js
* $log.info('Some Info');
* var first = $log.info.logs.unshift();
* ```
*/
$log.info.logs = [];
/**
* @ngdoc property
* @name $log#warn.logs
*
* @description
* Array of messages logged using {@link ng.$log#warn `warn()`}.
*
* @example
* ```js
* $log.warn('Some Warning');
* var first = $log.warn.logs.unshift();
* ```
*/
$log.warn.logs = [];
/**
* @ngdoc property
* @name $log#error.logs
*
* @description
* Array of messages logged using {@link ng.$log#error `error()`}.
*
* @example
* ```js
* $log.error('Some Error');
* var first = $log.error.logs.unshift();
* ```
*/
$log.error.logs = [];
/**
* @ngdoc property
* @name $log#debug.logs
*
* @description
* Array of messages logged using {@link ng.$log#debug `debug()`}.
*
* @example
* ```js
* $log.debug('Some Error');
* var first = $log.debug.logs.unshift();
* ```
*/
$log.debug.logs = [];
};
/**
* @ngdoc method
* @name $log#assertEmpty
*
* @description
* Assert that all of the logging methods have no logged messages. If any messages are present,
* an exception is thrown.
*/
$log.assertEmpty = function() {
var errors = [];
angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) {
angular.forEach($log[logLevel].logs, function(log) {
angular.forEach(log, function(logItem) {
errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' +
(logItem.stack || ''));
});
});
});
if (errors.length) {
errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or " +
"an expected log message was not checked and removed:");
errors.push('');
throw new Error(errors.join('\n---------\n'));
}
};
$log.reset();
return $log;
};
};
/**
* @ngdoc service
* @name $interval
*
* @description
* Mock implementation of the $interval service.
*
* Use {@link ngMock.$interval#flush `$interval.flush(millis)`} to
* move forward by `millis` milliseconds and trigger any functions scheduled to run in that
* time.
*
* @param {function()} fn A function that should be called repeatedly.
* @param {number} delay Number of milliseconds between each function call.
* @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat
* indefinitely.
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise
* will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block.
* @param {...*=} Pass additional parameters to the executed function.
* @returns {promise} A promise which will be notified on each iteration.
*/
angular.mock.$IntervalProvider = function() {
this.$get = ['$browser', '$rootScope', '$q', '$$q',
function($browser, $rootScope, $q, $$q) {
var repeatFns = [],
nextRepeatId = 0,
now = 0;
var $interval = function(fn, delay, count, invokeApply) {
var hasParams = arguments.length > 4,
args = hasParams ? Array.prototype.slice.call(arguments, 4) : [],
iteration = 0,
skipApply = (angular.isDefined(invokeApply) && !invokeApply),
deferred = (skipApply ? $$q : $q).defer(),
promise = deferred.promise;
count = (angular.isDefined(count)) ? count : 0;
promise.then(null, null, (!hasParams) ? fn : function() {
fn.apply(null, args);
});
promise.$$intervalId = nextRepeatId;
function tick() {
deferred.notify(iteration++);
if (count > 0 && iteration >= count) {
var fnIndex;
deferred.resolve(iteration);
angular.forEach(repeatFns, function(fn, index) {
if (fn.id === promise.$$intervalId) fnIndex = index;
});
if (fnIndex !== undefined) {
repeatFns.splice(fnIndex, 1);
}
}
if (skipApply) {
$browser.defer.flush();
} else {
$rootScope.$apply();
}
}
repeatFns.push({
nextTime:(now + delay),
delay: delay,
fn: tick,
id: nextRepeatId,
deferred: deferred
});
repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;});
nextRepeatId++;
return promise;
};
/**
* @ngdoc method
* @name $interval#cancel
*
* @description
* Cancels a task associated with the `promise`.
*
* @param {promise} promise A promise from calling the `$interval` function.
* @returns {boolean} Returns `true` if the task was successfully cancelled.
*/
$interval.cancel = function(promise) {
if (!promise) return false;
var fnIndex;
angular.forEach(repeatFns, function(fn, index) {
if (fn.id === promise.$$intervalId) fnIndex = index;
});
if (fnIndex !== undefined) {
repeatFns[fnIndex].deferred.reject('canceled');
repeatFns.splice(fnIndex, 1);
return true;
}
return false;
};
/**
* @ngdoc method
* @name $interval#flush
* @description
*
* Runs interval tasks scheduled to be run in the next `millis` milliseconds.
*
* @param {number=} millis maximum timeout amount to flush up until.
*
* @return {number} The amount of time moved forward.
*/
$interval.flush = function(millis) {
now += millis;
while (repeatFns.length && repeatFns[0].nextTime <= now) {
var task = repeatFns[0];
task.fn();
task.nextTime += task.delay;
repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;});
}
return millis;
};
return $interval;
}];
};
/* jshint -W101 */
/* The R_ISO8061_STR regex is never going to fit into the 100 char limit!
* This directive should go inside the anonymous function but a bug in JSHint means that it would
* not be enacted early enough to prevent the warning.
*/
var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/;
function jsonStringToDate(string) {
var match;
if (match = string.match(R_ISO8061_STR)) {
var date = new Date(0),
tzHour = 0,
tzMin = 0;
if (match[9]) {
tzHour = toInt(match[9] + match[10]);
tzMin = toInt(match[9] + match[11]);
}
date.setUTCFullYear(toInt(match[1]), toInt(match[2]) - 1, toInt(match[3]));
date.setUTCHours(toInt(match[4] || 0) - tzHour,
toInt(match[5] || 0) - tzMin,
toInt(match[6] || 0),
toInt(match[7] || 0));
return date;
}
return string;
}
function toInt(str) {
return parseInt(str, 10);
}
function padNumber(num, digits, trim) {
var neg = '';
if (num < 0) {
neg = '-';
num = -num;
}
num = '' + num;
while (num.length < digits) num = '0' + num;
if (trim) {
num = num.substr(num.length - digits);
}
return neg + num;
}
/**
* @ngdoc type
* @name angular.mock.TzDate
* @description
*
* *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`.
*
* Mock of the Date type which has its timezone specified via constructor arg.
*
* The main purpose is to create Date-like instances with timezone fixed to the specified timezone
* offset, so that we can test code that depends on local timezone settings without dependency on
* the time zone settings of the machine where the code is running.
*
* @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored)
* @param {(number|string)} timestamp Timestamp representing the desired time in *UTC*
*
* @example
* !!!! WARNING !!!!!
* This is not a complete Date object so only methods that were implemented can be called safely.
* To make matters worse, TzDate instances inherit stuff from Date via a prototype.
*
* We do our best to intercept calls to "unimplemented" methods, but since the list of methods is
* incomplete we might be missing some non-standard methods. This can result in errors like:
* "Date.prototype.foo called on incompatible Object".
*
* ```js
* var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
* newYearInBratislava.getTimezoneOffset() => -60;
* newYearInBratislava.getFullYear() => 2010;
* newYearInBratislava.getMonth() => 0;
* newYearInBratislava.getDate() => 1;
* newYearInBratislava.getHours() => 0;
* newYearInBratislava.getMinutes() => 0;
* newYearInBratislava.getSeconds() => 0;
* ```
*
*/
angular.mock.TzDate = function(offset, timestamp) {
var self = new Date(0);
if (angular.isString(timestamp)) {
var tsStr = timestamp;
self.origDate = jsonStringToDate(timestamp);
timestamp = self.origDate.getTime();
if (isNaN(timestamp)) {
throw {
name: "Illegal Argument",
message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string"
};
}
} else {
self.origDate = new Date(timestamp);
}
var localOffset = new Date(timestamp).getTimezoneOffset();
self.offsetDiff = localOffset * 60 * 1000 - offset * 1000 * 60 * 60;
self.date = new Date(timestamp + self.offsetDiff);
self.getTime = function() {
return self.date.getTime() - self.offsetDiff;
};
self.toLocaleDateString = function() {
return self.date.toLocaleDateString();
};
self.getFullYear = function() {
return self.date.getFullYear();
};
self.getMonth = function() {
return self.date.getMonth();
};
self.getDate = function() {
return self.date.getDate();
};
self.getHours = function() {
return self.date.getHours();
};
self.getMinutes = function() {
return self.date.getMinutes();
};
self.getSeconds = function() {
return self.date.getSeconds();
};
self.getMilliseconds = function() {
return self.date.getMilliseconds();
};
self.getTimezoneOffset = function() {
return offset * 60;
};
self.getUTCFullYear = function() {
return self.origDate.getUTCFullYear();
};
self.getUTCMonth = function() {
return self.origDate.getUTCMonth();
};
self.getUTCDate = function() {
return self.origDate.getUTCDate();
};
self.getUTCHours = function() {
return self.origDate.getUTCHours();
};
self.getUTCMinutes = function() {
return self.origDate.getUTCMinutes();
};
self.getUTCSeconds = function() {
return self.origDate.getUTCSeconds();
};
self.getUTCMilliseconds = function() {
return self.origDate.getUTCMilliseconds();
};
self.getDay = function() {
return self.date.getDay();
};
// provide this method only on browsers that already have it
if (self.toISOString) {
self.toISOString = function() {
return padNumber(self.origDate.getUTCFullYear(), 4) + '-' +
padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' +
padNumber(self.origDate.getUTCDate(), 2) + 'T' +
padNumber(self.origDate.getUTCHours(), 2) + ':' +
padNumber(self.origDate.getUTCMinutes(), 2) + ':' +
padNumber(self.origDate.getUTCSeconds(), 2) + '.' +
padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z';
};
}
//hide all methods not implemented in this mock that the Date prototype exposes
var unimplementedMethods = ['getUTCDay',
'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds',
'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear',
'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds',
'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString',
'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf'];
angular.forEach(unimplementedMethods, function(methodName) {
self[methodName] = function() {
throw new Error("Method '" + methodName + "' is not implemented in the TzDate mock");
};
});
return self;
};
//make "tzDateInstance instanceof Date" return true
angular.mock.TzDate.prototype = Date.prototype;
/* jshint +W101 */
angular.mock.animate = angular.module('ngAnimateMock', ['ng'])
.config(['$provide', function($provide) {
var reflowQueue = [];
$provide.value('$$animateReflow', function(fn) {
var index = reflowQueue.length;
reflowQueue.push(fn);
return function cancel() {
reflowQueue.splice(index, 1);
};
});
$provide.decorator('$animate', ['$delegate', '$$asyncCallback', '$timeout', '$browser', '$$rAF',
function($delegate, $$asyncCallback, $timeout, $browser, $$rAF) {
var animate = {
queue: [],
cancel: $delegate.cancel,
enabled: $delegate.enabled,
triggerCallbackEvents: function() {
$$rAF.flush();
$$asyncCallback.flush();
},
triggerCallbackPromise: function() {
$timeout.flush(0);
},
triggerCallbacks: function() {
this.triggerCallbackEvents();
this.triggerCallbackPromise();
},
triggerReflow: function() {
angular.forEach(reflowQueue, function(fn) {
fn();
});
reflowQueue = [];
}
};
angular.forEach(
['animate','enter','leave','move','addClass','removeClass','setClass'], function(method) {
animate[method] = function() {
animate.queue.push({
event: method,
element: arguments[0],
options: arguments[arguments.length - 1],
args: arguments
});
return $delegate[method].apply($delegate, arguments);
};
});
return animate;
}]);
}]);
/**
* @ngdoc function
* @name angular.mock.dump
* @description
*
* *NOTE*: this is not an injectable instance, just a globally available function.
*
* Method for serializing common angular objects (scope, elements, etc..) into strings, useful for
* debugging.
*
* This method is also available on window, where it can be used to display objects on debug
* console.
*
* @param {*} object - any object to turn into string.
* @return {string} a serialized string of the argument
*/
angular.mock.dump = function(object) {
return serialize(object);
function serialize(object) {
var out;
if (angular.isElement(object)) {
object = angular.element(object);
out = angular.element('<div></div>');
angular.forEach(object, function(element) {
out.append(angular.element(element).clone());
});
out = out.html();
} else if (angular.isArray(object)) {
out = [];
angular.forEach(object, function(o) {
out.push(serialize(o));
});
out = '[ ' + out.join(', ') + ' ]';
} else if (angular.isObject(object)) {
if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) {
out = serializeScope(object);
} else if (object instanceof Error) {
out = object.stack || ('' + object.name + ': ' + object.message);
} else {
// TODO(i): this prevents methods being logged,
// we should have a better way to serialize objects
out = angular.toJson(object, true);
}
} else {
out = String(object);
}
return out;
}
function serializeScope(scope, offset) {
offset = offset || ' ';
var log = [offset + 'Scope(' + scope.$id + '): {'];
for (var key in scope) {
if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) {
log.push(' ' + key + ': ' + angular.toJson(scope[key]));
}
}
var child = scope.$$childHead;
while (child) {
log.push(serializeScope(child, offset + ' '));
child = child.$$nextSibling;
}
log.push('}');
return log.join('\n' + offset);
}
};
/**
* @ngdoc service
* @name $httpBackend
* @description
* Fake HTTP backend implementation suitable for unit testing applications that use the
* {@link ng.$http $http service}.
*
* *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less
* development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}.
*
* During unit testing, we want our unit tests to run quickly and have no external dependencies so
* we don’t want to send [XHR](https://developer.mozilla.org/en/xmlhttprequest) or
* [JSONP](http://en.wikipedia.org/wiki/JSONP) requests to a real server. All we really need is
* to verify whether a certain request has been sent or not, or alternatively just let the
* application make requests, respond with pre-trained responses and assert that the end result is
* what we expect it to be.
*
* This mock implementation can be used to respond with static or dynamic responses via the
* `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc).
*
* When an Angular application needs some data from a server, it calls the $http service, which
* sends the request to a real server using $httpBackend service. With dependency injection, it is
* easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify
* the requests and respond with some testing data without sending a request to a real server.
*
* There are two ways to specify what test data should be returned as http responses by the mock
* backend when the code under test makes http requests:
*
* - `$httpBackend.expect` - specifies a request expectation
* - `$httpBackend.when` - specifies a backend definition
*
*
* # Request Expectations vs Backend Definitions
*
* Request expectations provide a way to make assertions about requests made by the application and
* to define responses for those requests. The test will fail if the expected requests are not made
* or they are made in the wrong order.
*
* Backend definitions allow you to define a fake backend for your application which doesn't assert
* if a particular request was made or not, it just returns a trained response if a request is made.
* The test will pass whether or not the request gets made during testing.
*
*
* <table class="table">
* <tr><th width="220px"></th><th>Request expectations</th><th>Backend definitions</th></tr>
* <tr>
* <th>Syntax</th>
* <td>.expect(...).respond(...)</td>
* <td>.when(...).respond(...)</td>
* </tr>
* <tr>
* <th>Typical usage</th>
* <td>strict unit tests</td>
* <td>loose (black-box) unit testing</td>
* </tr>
* <tr>
* <th>Fulfills multiple requests</th>
* <td>NO</td>
* <td>YES</td>
* </tr>
* <tr>
* <th>Order of requests matters</th>
* <td>YES</td>
* <td>NO</td>
* </tr>
* <tr>
* <th>Request required</th>
* <td>YES</td>
* <td>NO</td>
* </tr>
* <tr>
* <th>Response required</th>
* <td>optional (see below)</td>
* <td>YES</td>
* </tr>
* </table>
*
* In cases where both backend definitions and request expectations are specified during unit
* testing, the request expectations are evaluated first.
*
* If a request expectation has no response specified, the algorithm will search your backend
* definitions for an appropriate response.
*
* If a request didn't match any expectation or if the expectation doesn't have the response
* defined, the backend definitions are evaluated in sequential order to see if any of them match
* the request. The response from the first matched definition is returned.
*
*
* # Flushing HTTP requests
*
* The $httpBackend used in production always responds to requests asynchronously. If we preserved
* this behavior in unit testing, we'd have to create async unit tests, which are hard to write,
* to follow and to maintain. But neither can the testing mock respond synchronously; that would
* change the execution of the code under test. For this reason, the mock $httpBackend has a
* `flush()` method, which allows the test to explicitly flush pending requests. This preserves
* the async api of the backend, while allowing the test to execute synchronously.
*
*
* # Unit testing with mock $httpBackend
* The following code shows how to setup and use the mock backend when unit testing a controller.
* First we create the controller under test:
*
```js
// The module code
angular
.module('MyApp', [])
.controller('MyController', MyController);
// The controller code
function MyController($scope, $http) {
var authToken;
$http.get('/auth.py').success(function(data, status, headers) {
authToken = headers('A-Token');
$scope.user = data;
});
$scope.saveMessage = function(message) {
var headers = { 'Authorization': authToken };
$scope.status = 'Saving...';
$http.post('/add-msg.py', message, { headers: headers } ).success(function(response) {
$scope.status = '';
}).error(function() {
$scope.status = 'ERROR!';
});
};
}
```
*
* Now we setup the mock backend and create the test specs:
*
```js
// testing controller
describe('MyController', function() {
var $httpBackend, $rootScope, createController, authRequestHandler;
// Set up the module
beforeEach(module('MyApp'));
beforeEach(inject(function($injector) {
// Set up the mock http service responses
$httpBackend = $injector.get('$httpBackend');
// backend definition common for all tests
authRequestHandler = $httpBackend.when('GET', '/auth.py')
.respond({userId: 'userX'}, {'A-Token': 'xxx'});
// Get hold of a scope (i.e. the root scope)
$rootScope = $injector.get('$rootScope');
// The $controller service is used to create instances of controllers
var $controller = $injector.get('$controller');
createController = function() {
return $controller('MyController', {'$scope' : $rootScope });
};
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should fetch authentication token', function() {
$httpBackend.expectGET('/auth.py');
var controller = createController();
$httpBackend.flush();
});
it('should fail authentication', function() {
// Notice how you can change the response even after it was set
authRequestHandler.respond(401, '');
$httpBackend.expectGET('/auth.py');
var controller = createController();
$httpBackend.flush();
expect($rootScope.status).toBe('Failed...');
});
it('should send msg to server', function() {
var controller = createController();
$httpBackend.flush();
// now you don’t care about the authentication, but
// the controller will still send the request and
// $httpBackend will respond without you having to
// specify the expectation and response for this request
$httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
$rootScope.saveMessage('message content');
expect($rootScope.status).toBe('Saving...');
$httpBackend.flush();
expect($rootScope.status).toBe('');
});
it('should send auth header', function() {
var controller = createController();
$httpBackend.flush();
$httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
// check if the header was send, if it wasn't the expectation won't
// match the request and the test will fail
return headers['Authorization'] == 'xxx';
}).respond(201, '');
$rootScope.saveMessage('whatever');
$httpBackend.flush();
});
});
```
*/
angular.mock.$HttpBackendProvider = function() {
this.$get = ['$rootScope', '$timeout', createHttpBackendMock];
};
/**
* General factory function for $httpBackend mock.
* Returns instance for unit testing (when no arguments specified):
* - passing through is disabled
* - auto flushing is disabled
*
* Returns instance for e2e testing (when `$delegate` and `$browser` specified):
* - passing through (delegating request to real backend) is enabled
* - auto flushing is enabled
*
* @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified)
* @param {Object=} $browser Auto-flushing enabled if specified
* @return {Object} Instance of $httpBackend mock
*/
function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
var definitions = [],
expectations = [],
responses = [],
responsesPush = angular.bind(responses, responses.push),
copy = angular.copy;
function createResponse(status, data, headers, statusText) {
if (angular.isFunction(status)) return status;
return function() {
return angular.isNumber(status)
? [status, data, headers, statusText]
: [200, status, data, headers];
};
}
// TODO(vojta): change params to: method, url, data, headers, callback
function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) {
var xhr = new MockXhr(),
expectation = expectations[0],
wasExpected = false;
function prettyPrint(data) {
return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp)
? data
: angular.toJson(data);
}
function wrapResponse(wrapped) {
if (!$browser && timeout) {
timeout.then ? timeout.then(handleTimeout) : $timeout(handleTimeout, timeout);
}
return handleResponse;
function handleResponse() {
var response = wrapped.response(method, url, data, headers);
xhr.$$respHeaders = response[2];
callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders(),
copy(response[3] || ''));
}
function handleTimeout() {
for (var i = 0, ii = responses.length; i < ii; i++) {
if (responses[i] === handleResponse) {
responses.splice(i, 1);
callback(-1, undefined, '');
break;
}
}
}
}
if (expectation && expectation.match(method, url)) {
if (!expectation.matchData(data)) {
throw new Error('Expected ' + expectation + ' with different data\n' +
'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data);
}
if (!expectation.matchHeaders(headers)) {
throw new Error('Expected ' + expectation + ' with different headers\n' +
'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' +
prettyPrint(headers));
}
expectations.shift();
if (expectation.response) {
responses.push(wrapResponse(expectation));
return;
}
wasExpected = true;
}
var i = -1, definition;
while ((definition = definitions[++i])) {
if (definition.match(method, url, data, headers || {})) {
if (definition.response) {
// if $browser specified, we do auto flush all requests
($browser ? $browser.defer : responsesPush)(wrapResponse(definition));
} else if (definition.passThrough) {
$delegate(method, url, data, callback, headers, timeout, withCredentials);
} else throw new Error('No response defined !');
return;
}
}
throw wasExpected ?
new Error('No response defined !') :
new Error('Unexpected request: ' + method + ' ' + url + '\n' +
(expectation ? 'Expected ' + expectation : 'No more request expected'));
}
/**
* @ngdoc method
* @name $httpBackend#when
* @description
* Creates a new backend definition.
*
* @param {string} method HTTP method.
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current definition.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*
* - respond –
* `{function([status,] data[, headers, statusText])
* | function(function(method, url, data, headers)}`
* – The respond method takes a set of static data to be returned or a function that can
* return an array containing response status (number), response data (string), response
* headers (Object), and the text for the status (string). The respond method returns the
* `requestHandler` object for possible overrides.
*/
$httpBackend.when = function(method, url, data, headers) {
var definition = new MockHttpExpectation(method, url, data, headers),
chain = {
respond: function(status, data, headers, statusText) {
definition.passThrough = undefined;
definition.response = createResponse(status, data, headers, statusText);
return chain;
}
};
if ($browser) {
chain.passThrough = function() {
definition.response = undefined;
definition.passThrough = true;
return chain;
};
}
definitions.push(definition);
return chain;
};
/**
* @ngdoc method
* @name $httpBackend#whenGET
* @description
* Creates a new backend definition for GET requests. For more info see `when()`.
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
/**
* @ngdoc method
* @name $httpBackend#whenHEAD
* @description
* Creates a new backend definition for HEAD requests. For more info see `when()`.
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
/**
* @ngdoc method
* @name $httpBackend#whenDELETE
* @description
* Creates a new backend definition for DELETE requests. For more info see `when()`.
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
/**
* @ngdoc method
* @name $httpBackend#whenPOST
* @description
* Creates a new backend definition for POST requests. For more info see `when()`.
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
/**
* @ngdoc method
* @name $httpBackend#whenPUT
* @description
* Creates a new backend definition for PUT requests. For more info see `when()`.
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(string|RegExp|function(string))=} data HTTP request body or function that receives
* data string and returns true if the data is as expected.
* @param {(Object|function(Object))=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
/**
* @ngdoc method
* @name $httpBackend#whenJSONP
* @description
* Creates a new backend definition for JSONP requests. For more info see `when()`.
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
createShortMethods('when');
/**
* @ngdoc method
* @name $httpBackend#expect
* @description
* Creates a new request expectation.
*
* @param {string} method HTTP method.
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {(Object|function(Object))=} headers HTTP headers or function that receives http header
* object and returns true if the headers match the current expectation.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*
* - respond –
* `{function([status,] data[, headers, statusText])
* | function(function(method, url, data, headers)}`
* – The respond method takes a set of static data to be returned or a function that can
* return an array containing response status (number), response data (string), response
* headers (Object), and the text for the status (string). The respond method returns the
* `requestHandler` object for possible overrides.
*/
$httpBackend.expect = function(method, url, data, headers) {
var expectation = new MockHttpExpectation(method, url, data, headers),
chain = {
respond: function(status, data, headers, statusText) {
expectation.response = createResponse(status, data, headers, statusText);
return chain;
}
};
expectations.push(expectation);
return chain;
};
/**
* @ngdoc method
* @name $httpBackend#expectGET
* @description
* Creates a new request expectation for GET requests. For more info see `expect()`.
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled. See #expect for more info.
*/
/**
* @ngdoc method
* @name $httpBackend#expectHEAD
* @description
* Creates a new request expectation for HEAD requests. For more info see `expect()`.
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
/**
* @ngdoc method
* @name $httpBackend#expectDELETE
* @description
* Creates a new request expectation for DELETE requests. For more info see `expect()`.
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
/**
* @ngdoc method
* @name $httpBackend#expectPOST
* @description
* Creates a new request expectation for POST requests. For more info see `expect()`.
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
/**
* @ngdoc method
* @name $httpBackend#expectPUT
* @description
* Creates a new request expectation for PUT requests. For more info see `expect()`.
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
/**
* @ngdoc method
* @name $httpBackend#expectPATCH
* @description
* Creates a new request expectation for PATCH requests. For more info see `expect()`.
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives a url
* and returns true if the url matches the current definition.
* @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that
* receives data string and returns true if the data is as expected, or Object if request body
* is in JSON format.
* @param {Object=} headers HTTP headers.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
/**
* @ngdoc method
* @name $httpBackend#expectJSONP
* @description
* Creates a new request expectation for JSONP requests. For more info see `expect()`.
*
* @param {string|RegExp|function(string)} url HTTP url or function that receives an url
* and returns true if the url matches the current definition.
* @returns {requestHandler} Returns an object with `respond` method that controls how a matched
* request is handled. You can save this object for later use and invoke `respond` again in
* order to change how a matched request is handled.
*/
createShortMethods('expect');
/**
* @ngdoc method
* @name $httpBackend#flush