');
+ })
+ );
+
it('should not allow more than one isolate scope creation per element regardless of directive priority', function() {
module(function($compileProvider) {
$compileProvider.directive('highPriorityScope', function() {
@@ -2459,6 +2533,135 @@ describe('$compile', function() {
expect(element.isolateScope()).not.toBe($rootScope);
})
);
+
+ it('should handle "=" bindings with same method names in Object.prototype correctly when not present', inject(
+ function($rootScope, $compile) {
+ var func = function() {
+ element = $compile(
+ '
'
+ )($rootScope);
+ };
+
+ expect(func).not.toThrow();
+ var scope = element.isolateScope();
+ expect(element.find('span').scope()).toBe(scope);
+ expect(scope).not.toBe($rootScope);
+
+ // Not shadowed because optional
+ expect(scope.constructor).toBe($rootScope.constructor);
+ expect(scope.hasOwnProperty('constructor')).toBe(false);
+
+ // Shadowed with undefined because not optional
+ expect(scope.valueOf).toBeUndefined();
+ expect(scope.hasOwnProperty('valueOf')).toBe(true);
+ })
+ );
+
+ it('should handle "=" bindings with same method names in Object.prototype correctly when present', inject(
+ function($rootScope, $compile) {
+ $rootScope.constructor = 'constructor';
+ $rootScope.valueOf = 'valueOf';
+ var func = function() {
+ element = $compile(
+ '
'
+ )($rootScope);
+ };
+
+ expect(func).not.toThrow();
+ var scope = element.isolateScope();
+ expect(element.find('span').scope()).toBe(scope);
+ expect(scope).not.toBe($rootScope);
+ expect(scope.constructor).toBe('constructor');
+ expect(scope.hasOwnProperty('constructor')).toBe(true);
+ expect(scope.valueOf).toBe('valueOf');
+ expect(scope.hasOwnProperty('valueOf')).toBe(true);
+ })
+ );
+
+ it('should handle "@" bindings with same method names in Object.prototype correctly when not present', inject(
+ function($rootScope, $compile) {
+ var func = function() {
+ element = $compile('
')($rootScope);
+ };
+
+ expect(func).not.toThrow();
+ var scope = element.isolateScope();
+ expect(element.find('span').scope()).toBe(scope);
+ expect(scope).not.toBe($rootScope);
+
+ // Does not shadow value because optional
+ expect(scope.constructor).toBe($rootScope.constructor);
+ expect(scope.hasOwnProperty('constructor')).toBe(false);
+
+ // Shadows value because not optional
+ expect(scope.valueOf).toBeUndefined();
+ expect(scope.hasOwnProperty('valueOf')).toBe(true);
+ })
+ );
+
+ it('should handle "@" bindings with same method names in Object.prototype correctly when present', inject(
+ function($rootScope, $compile) {
+ var func = function() {
+ element = $compile(
+ '
'
+ )($rootScope);
+ };
+
+ expect(func).not.toThrow();
+ expect(element.find('span').scope()).toBe(element.isolateScope());
+ expect(element.isolateScope()).not.toBe($rootScope);
+ expect(element.isolateScope()['constructor']).toBe('constructor');
+ expect(element.isolateScope()['valueOf']).toBe('valueOf');
+ })
+ );
+
+ it('should handle "&" bindings with same method names in Object.prototype correctly when not present', inject(
+ function($rootScope, $compile) {
+ var func = function() {
+ element = $compile('
')($rootScope);
+ };
+
+ expect(func).not.toThrow();
+ expect(element.find('span').scope()).toBe(element.isolateScope());
+ expect(element.isolateScope()).not.toBe($rootScope);
+ expect(element.isolateScope()['constructor']).toBe($rootScope.constructor);
+ expect(element.isolateScope()['valueOf']()).toBeUndefined();
+ })
+ );
+
+ it('should handle "&" bindings with same method names in Object.prototype correctly when present', inject(
+ function($rootScope, $compile) {
+ $rootScope.constructor = function() { return 'constructor'; };
+ $rootScope.valueOf = function() { return 'valueOf'; };
+ var func = function() {
+ element = $compile(
+ '
'
+ )($rootScope);
+ };
+
+ expect(func).not.toThrow();
+ expect(element.find('span').scope()).toBe(element.isolateScope());
+ expect(element.isolateScope()).not.toBe($rootScope);
+ expect(element.isolateScope()['constructor']()).toBe('constructor');
+ expect(element.isolateScope()['valueOf']()).toBe('valueOf');
+ })
+ );
+
+ it('should not throw exception when using "watch" as binding in Firefox', inject(
+ function($rootScope, $compile) {
+ $rootScope.watch = 'watch';
+ var func = function() {
+ element = $compile(
+ '
'
+ )($rootScope);
+ };
+
+ expect(func).not.toThrow();
+ expect(element.find('span').scope()).toBe(element.isolateScope());
+ expect(element.isolateScope()).not.toBe($rootScope);
+ expect(element.isolateScope()['watch']).toBe('watch');
+ })
+ );
});
@@ -2501,6 +2704,70 @@ describe('$compile', function() {
);
});
});
+
+ describe('multidir isolated scope error messages', function() {
+ angular.module('fakeIsoledScopeModule', [])
+ .directive('fakeScope', function(log) {
+ return {
+ scope: true,
+ restrict: 'CA',
+ compile: function() {
+ return {pre: function(scope, element) {
+ log(scope.$id);
+ expect(element.data('$scope')).toBe(scope);
+ }};
+ }
+ };
+ })
+ .directive('fakeIScope', function(log) {
+ return {
+ scope: {},
+ restrict: 'CA',
+ compile: function() {
+ return function(scope, element) {
+ iscope = scope;
+ log(scope.$id);
+ expect(element.data('$isolateScopeNoTemplate')).toBe(scope);
+ };
+ }
+ };
+ });
+
+ beforeEach(module('fakeIsoledScopeModule', function() {
+ directive('anonymModuleScopeDirective', function(log) {
+ return {
+ scope: true,
+ restrict: 'CA',
+ compile: function() {
+ return {pre: function(scope, element) {
+ log(scope.$id);
+ expect(element.data('$scope')).toBe(scope);
+ }};
+ }
+ };
+ });
+ }));
+
+ it('should add module name to multidir isolated scope message if directive defined through module', inject(
+ function($rootScope, $compile) {
+ expect(function() {
+ $compile('
');
+ }).toThrowMinErr('$compile', 'multidir',
+ 'Multiple directives [fakeIScope (module: fakeIsoledScopeModule), fakeScope (module: fakeIsoledScopeModule)] ' +
+ 'asking for new/isolated scope on:
');
+ })
+ );
+
+ it('sholdn\'t add module name to multidir isolated scope message if directive is defined directly with $compileProvider', inject(
+ function($rootScope, $compile) {
+ expect(function() {
+ $compile('
');
+ }).toThrowMinErr('$compile', 'multidir',
+ 'Multiple directives [anonymModuleScopeDirective, fakeIScope (module: fakeIsoledScopeModule)] ' +
+ 'asking for new/isolated scope on:
');
+ })
+ );
+ });
});
});
});
@@ -2724,6 +2991,23 @@ describe('$compile', function() {
);
+ it('should call observer only when the attribute value changes', function() {
+ module(function() {
+ directive('observingDirective', function() {
+ return {
+ restrict: 'E',
+ scope: { someAttr: '@' }
+ };
+ });
+ });
+ inject(function($rootScope, $compile) {
+ $compile('
')($rootScope);
+ $rootScope.$digest();
+ expect(observeSpy).not.toHaveBeenCalledWith(undefined);
+ });
+ });
+
+
it('should delegate exceptions to $exceptionHandler', function() {
observeSpy = jasmine.createSpy('$observe attr').andThrow('ERROR');
@@ -2764,6 +3048,25 @@ describe('$compile', function() {
}));
+ it('should handle consecutive text elements as a single text element', inject(function($rootScope, $compile) {
+ // No point it running the test, if there is no MutationObserver
+ if (!window.MutationObserver) return;
+
+ // Create and register the MutationObserver
+ var observer = new window.MutationObserver(noop);
+ observer.observe(document.body, {childList: true, subtree: true});
+
+ // Run the actual test
+ var base = jqLite('
— {{ "This doesn\'t." }}
');
+ element = $compile(base)($rootScope);
+ $rootScope.$digest();
+ expect(element.text()).toBe("— This doesn't.");
+
+ // Unregister the MutationObserver (and hope it doesn't mess up with subsequent tests)
+ observer.disconnect();
+ }));
+
+
it('should support custom start/end interpolation symbols in template and directive template',
function() {
module(function($interpolateProvider, $compileProvider) {
@@ -3307,6 +3610,58 @@ describe('$compile', function() {
}));
+ it('should not overwrite @-bound property each digest when not present', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('testDir', valueFn({
+ scope: {prop: '@'},
+ controller: function($scope) {
+ $scope.prop = $scope.prop || 'default';
+ this.getProp = function() {
+ return $scope.prop;
+ };
+ },
+ controllerAs: 'ctrl',
+ template: '
'
+ }));
+ });
+ inject(function($compile, $rootScope) {
+ element = $compile('
')($rootScope);
+ var scope = element.isolateScope();
+ expect(scope.ctrl.getProp()).toBe('default');
+
+ $rootScope.$digest();
+ expect(scope.ctrl.getProp()).toBe('default');
+ });
+ });
+
+
+ it('should ignore optional "="-bound property if value is the emptry string', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('testDir', valueFn({
+ scope: {prop: '=?'},
+ controller: function($scope) {
+ $scope.prop = $scope.prop || 'default';
+ this.getProp = function() {
+ return $scope.prop;
+ };
+ },
+ controllerAs: 'ctrl',
+ template: '
'
+ }));
+ });
+ inject(function($compile, $rootScope) {
+ element = $compile('
')($rootScope);
+ var scope = element.isolateScope();
+ expect(scope.ctrl.getProp()).toBe('default');
+ $rootScope.$digest();
+ expect(scope.ctrl.getProp()).toBe('default');
+ scope.prop = 'foop';
+ $rootScope.$digest();
+ expect(scope.ctrl.getProp()).toBe('foop');
+ });
+ });
+
+
describe('bind-once', function() {
function countWatches(scope) {
@@ -4168,6 +4523,64 @@ describe('$compile', function() {
childScope.theCtrl.test();
});
});
+
+ describe('should not overwrite @-bound property each digest when not present', function() {
+ it('when creating new scope', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('testDir', valueFn({
+ scope: true,
+ bindToController: {
+ prop: '@'
+ },
+ controller: function() {
+ var self = this;
+ this.prop = this.prop || 'default';
+ this.getProp = function() {
+ return self.prop;
+ };
+ },
+ controllerAs: 'ctrl',
+ template: '
'
+ }));
+ });
+ inject(function($compile, $rootScope) {
+ element = $compile('
')($rootScope);
+ var scope = element.scope();
+ expect(scope.ctrl.getProp()).toBe('default');
+
+ $rootScope.$digest();
+ expect(scope.ctrl.getProp()).toBe('default');
+ });
+ });
+
+ it('when creating isolate scope', function() {
+ module(function($compileProvider) {
+ $compileProvider.directive('testDir', valueFn({
+ scope: {},
+ bindToController: {
+ prop: '@'
+ },
+ controller: function() {
+ var self = this;
+ this.prop = this.prop || 'default';
+ this.getProp = function() {
+ return self.prop;
+ };
+ },
+ controllerAs: 'ctrl',
+ template: '
'
+ }));
+ });
+ inject(function($compile, $rootScope) {
+ element = $compile('
')($rootScope);
+ var scope = element.isolateScope();
+ expect(scope.ctrl.getProp()).toBe('default');
+
+ $rootScope.$digest();
+ expect(scope.ctrl.getProp()).toBe('default');
+ });
+ });
+ });
});
@@ -4209,6 +4622,177 @@ describe('$compile', function() {
});
+ it('should respect explicit return value from controller', function() {
+ var expectedController;
+ module(function() {
+ directive('logControllerProp', function(log) {
+ return {
+ controller: function($scope) {
+ this.foo = 'baz'; // value should not be used.
+ return expectedController = {foo: 'bar'};
+ },
+ link: function(scope, element, attrs, controller) {
+ expect(expectedController).toBeDefined();
+ expect(controller).toBe(expectedController);
+ expect(controller.foo).toBe('bar');
+ log('done');
+ }
+ };
+ });
+ });
+ inject(function(log, $compile, $rootScope) {
+ element = $compile('
')($rootScope);
+ expect(log).toEqual('done');
+ expect(element.data('$logControllerPropController')).toBe(expectedController);
+ });
+ });
+
+
+ it('should get explicit return value of required parent controller', function() {
+ var expectedController;
+ module(function() {
+ directive('nested', function(log) {
+ return {
+ require: '^^?nested',
+ controller: function() {
+ if (!expectedController) expectedController = {foo: 'bar'};
+ return expectedController;
+ },
+ link: function(scope, element, attrs, controller) {
+ if (element.parent().length) {
+ expect(expectedController).toBeDefined();
+ expect(controller).toBe(expectedController);
+ expect(controller.foo).toBe('bar');
+ log('done');
+ }
+ }
+ };
+ });
+ });
+ inject(function(log, $compile, $rootScope) {
+ element = $compile('
')($rootScope);
+ expect(log).toEqual('done');
+ expect(element.data('$nestedController')).toBe(expectedController);
+ });
+ });
+
+
+ it('should respect explicit controller return value when using controllerAs', function() {
+ module(function() {
+ directive('main', function() {
+ return {
+ templateUrl: 'main.html',
+ scope: {},
+ controller: function() {
+ this.name = 'lucas';
+ return {name: 'george'};
+ },
+ controllerAs: 'mainCtrl'
+ };
+ });
+ });
+ inject(function($templateCache, $compile, $rootScope) {
+ $templateCache.put('main.html', '
template:{{mainCtrl.name}} ');
+ element = $compile('
')($rootScope);
+ $rootScope.$apply();
+ expect(element.text()).toBe('template:george');
+ });
+ });
+
+
+ it('transcluded children should receive explicit return value of parent controller', function() {
+ var expectedController;
+ module(function() {
+ directive('nester', valueFn({
+ transclude: true,
+ controller: function($transclude) {
+ this.foo = 'baz';
+ return expectedController = {transclude:$transclude, foo: 'bar'};
+ },
+ link: function(scope, el, attr, ctrl) {
+ ctrl.transclude(cloneAttach);
+ function cloneAttach(clone) {
+ el.append(clone);
+ }
+ }
+ }));
+ directive('nested', function(log) {
+ return {
+ require: '^^nester',
+ link: function(scope, element, attrs, controller) {
+ expect(controller).toBeDefined();
+ expect(controller).toBe(expectedController);
+ log('done');
+ }
+ };
+ });
+ });
+ inject(function(log, $compile) {
+ element = $compile('
')($rootScope);
+ $rootScope.$apply();
+ expect(log.toString()).toBe('done');
+ expect(element.data('$nesterController')).toBe(expectedController);
+ });
+ });
+
+
+ it('explicit controller return values are ignored if they are primitives', function() {
+ module(function() {
+ directive('logControllerProp', function(log) {
+ return {
+ controller: function($scope) {
+ this.foo = 'baz'; // value *will* be used.
+ return 'bar';
+ },
+ link: function(scope, element, attrs, controller) {
+ log(controller.foo);
+ }
+ };
+ });
+ });
+ inject(function(log, $compile, $rootScope) {
+ element = $compile('
')($rootScope);
+ expect(log).toEqual('baz');
+ expect(element.data('$logControllerPropController').foo).toEqual('baz');
+ });
+ });
+
+
+ it('should correctly assign controller return values for multiple directives', function() {
+ var directiveController, otherDirectiveController;
+ module(function() {
+
+ directive('myDirective', function(log) {
+ return {
+ scope: true,
+ controller: function($scope) {
+ return directiveController = {
+ foo: 'bar'
+ };
+ }
+ };
+ });
+
+ directive('myOtherDirective', function(log) {
+ return {
+ controller: function($scope) {
+ return otherDirectiveController = {
+ baz: 'luh'
+ };
+ }
+ };
+ });
+
+ });
+
+ inject(function(log, $compile, $rootScope) {
+ element = $compile('
')($rootScope);
+ expect(element.data('$myDirectiveController')).toBe(directiveController);
+ expect(element.data('$myOtherDirectiveController')).toBe(otherDirectiveController);
+ });
+ });
+
+
it('should get required parent controller', function() {
module(function() {
directive('nested', function(log) {
@@ -5209,8 +5793,13 @@ describe('$compile', function() {
expect(privateData.events.click).toBeDefined();
expect(privateData.events.click[0]).toBeDefined();
+ //Ensure the angular $destroy event is still sent
+ var destroyCount = 0;
+ element.find("div").on("$destroy", function() { destroyCount++; });
+
$rootScope.$apply('xs = null');
+ expect(destroyCount).toBe(2);
expect(firstRepeatedElem.data('$scope')).not.toBeDefined();
privateData = jQuery._data(firstRepeatedElem[0]);
expect(privateData && privateData.events).not.toBeDefined();
@@ -5231,9 +5820,7 @@ describe('$compile', function() {
testCleanup();
- // The initial ng-repeat div is dumped after parsing hence we expect cleanData
- // count to be one larger than size of the iterated array.
- expect(cleanedCount).toBe(xs.length + 1);
+ expect(cleanedCount).toBe(xs.length);
// Restore the previous jQuery.cleanData.
jQuery.cleanData = currentCleanData;
diff --git a/test/ng/cookieReaderSpec.js b/test/ng/cookieReaderSpec.js
new file mode 100644
index 000000000000..e8756c4b5235
--- /dev/null
+++ b/test/ng/cookieReaderSpec.js
@@ -0,0 +1,103 @@
+'use strict';
+
+describe('$$cookieReader', function() {
+ var $$cookieReader;
+
+ function deleteAllCookies() {
+ var cookies = document.cookie.split(";");
+ var path = location.pathname;
+
+ for (var i = 0; i < cookies.length; i++) {
+ var cookie = cookies[i];
+ var eqPos = cookie.indexOf("=");
+ var name = eqPos > -1 ? cookie.substr(0, eqPos) : cookie;
+ var parts = path.split('/');
+ while (parts.length) {
+ document.cookie = name + "=;path=" + (parts.join('/') || '/') + ";expires=Thu, 01 Jan 1970 00:00:00 GMT";
+ parts.pop();
+ }
+ }
+ }
+
+ beforeEach(function() {
+ deleteAllCookies();
+ expect(document.cookie).toEqual('');
+
+ inject(function(_$$cookieReader_) {
+ $$cookieReader = _$$cookieReader_;
+ });
+ });
+
+
+ afterEach(function() {
+ deleteAllCookies();
+ expect(document.cookie).toEqual('');
+ });
+
+
+ describe('get via $$cookieReader()[cookieName]', function() {
+
+ it('should return undefined for nonexistent cookie', function() {
+ expect($$cookieReader().nonexistent).not.toBeDefined();
+ });
+
+
+ it('should return a value for an existing cookie', function() {
+ document.cookie = "foo=bar=baz;path=/";
+ expect($$cookieReader().foo).toEqual('bar=baz');
+ });
+
+ it('should return the the first value provided for a cookie', function() {
+ // For a cookie that has different values that differ by path, the
+ // value for the most specific path appears first. $$cookieReader()
+ // should provide that value for the cookie.
+ document.cookie = 'foo="first"; foo="second"';
+ expect($$cookieReader()['foo']).toBe('"first"');
+ });
+
+ it('should decode cookie values that were encoded by puts', function() {
+ document.cookie = "cookie2%3Dbar%3Bbaz=val%3Due;path=/";
+ expect($$cookieReader()['cookie2=bar;baz']).toEqual('val=ue');
+ });
+
+
+ it('should preserve leading & trailing spaces in names and values', function() {
+ document.cookie = '%20cookie%20name%20=%20cookie%20value%20';
+ expect($$cookieReader()[' cookie name ']).toEqual(' cookie value ');
+ expect($$cookieReader()['cookie name']).not.toBeDefined();
+ });
+
+ it('should decode special characters in cookie values', function() {
+ document.cookie = 'cookie_name=cookie_value_%E2%82%AC';
+ expect($$cookieReader()['cookie_name']).toEqual('cookie_value_€');
+ });
+
+ it('should not decode cookie values that do not appear to be encoded', function() {
+ // see #9211 - sometimes cookies contain a value that causes decodeURIComponent to throw
+ document.cookie = 'cookie_name=cookie_value_%XX';
+ expect($$cookieReader()['cookie_name']).toEqual('cookie_value_%XX');
+ });
+ });
+
+
+ describe('getAll via $$cookieReader()', function() {
+
+ it('should return cookies as hash', function() {
+ document.cookie = "foo1=bar1;path=/";
+ document.cookie = "foo2=bar2;path=/";
+ expect($$cookieReader()).toEqual({'foo1':'bar1', 'foo2':'bar2'});
+ });
+
+
+ it('should return empty hash if no cookies exist', function() {
+ expect($$cookieReader()).toEqual({});
+ });
+ });
+
+
+ it('should initialize cookie cache with existing cookies', function() {
+ document.cookie = "existingCookie=existingValue;path=/";
+ expect($$cookieReader()).toEqual({'existingCookie':'existingValue'});
+ });
+
+});
diff --git a/test/ng/directive/formSpec.js b/test/ng/directive/formSpec.js
index d69fa707b6ad..27d18f3032e0 100644
--- a/test/ng/directive/formSpec.js
+++ b/test/ng/directive/formSpec.js
@@ -802,6 +802,7 @@ describe('form', function() {
scope.$digest();
expect(form).toBePristine();
scope.$digest();
+
expect(formCtrl.$pristine).toBe(true);
expect(formCtrl.$dirty).toBe(false);
expect(nestedForm).toBePristine();
@@ -881,20 +882,38 @@ describe('form', function() {
it('should rename forms with no parent when interpolated name changes', function() {
var element = $compile('
')(scope);
- var element2 = $compile('
')(scope);
+ var element2 = $compile('
')(scope);
scope.nameID = "A";
scope.$digest();
var form = element.controller('form');
var form2 = element2.controller('form');
+ expect(scope.nameA).toBe(form);
+ expect(scope.ngformA).toBe(form2);
expect(form.$name).toBe('nameA');
- expect(form2.$name).toBe('nameA');
+ expect(form2.$name).toBe('ngformA');
scope.nameID = "B";
scope.$digest();
+ expect(scope.nameA).toBeUndefined();
+ expect(scope.ngformA).toBeUndefined();
+ expect(scope.nameB).toBe(form);
+ expect(scope.ngformB).toBe(form2);
expect(form.$name).toBe('nameB');
- expect(form2.$name).toBe('nameB');
+ expect(form2.$name).toBe('ngformB');
});
+ it('should rename forms with an initially blank name', function() {
+ var element = $compile('
')(scope);
+ scope.$digest();
+ var form = element.controller('form');
+ expect(scope['']).toBe(form);
+ expect(form.$name).toBe('');
+ scope.name = 'foo';
+ scope.$digest();
+ expect(scope.foo).toBe(form);
+ expect(form.$name).toBe('foo');
+ expect(scope.foo).toBe(form);
+ });
describe('$setSubmitted', function() {
beforeEach(function() {
diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js
index ad98d52abdf9..3c92625a1b43 100644
--- a/test/ng/directive/inputSpec.js
+++ b/test/ng/directive/inputSpec.js
@@ -551,12 +551,8 @@ describe('input', function() {
expect(inputElm.val()).toBe('2013-01');
- try {
- //set to text for browsers with datetime-local validation.
- inputElm[0].setAttribute('type', 'text');
- } catch (e) {
- //for IE8
- }
+ //set to text for browsers with datetime-local validation.
+ inputElm[0].setAttribute('type', 'text');
helper.changeInputValueTo('stuff');
expect(inputElm.val()).toBe('stuff');
@@ -613,6 +609,19 @@ describe('input', function() {
});
+ it('should use any timezone if specified in the options', function() {
+ var inputElm = helper.compileInput('
');
+
+ helper.changeInputValueTo('2013-07');
+ expect(+$rootScope.value).toBe(Date.UTC(2013, 5, 30, 19, 0, 0));
+
+ $rootScope.$apply(function() {
+ $rootScope.value = new Date(Date.UTC(2014, 5, 30, 19, 0, 0));
+ });
+ expect(inputElm.val()).toBe('2014-07');
+ });
+
+
it('should label parse errors as `month`', function() {
var inputElm = helper.compileInput('
', {
valid: false,
@@ -636,6 +645,17 @@ describe('input', function() {
expect(inputElm.val()).toBe('2013-12');
});
+ it('should only change the month of a bound date in any timezone', function() {
+ var inputElm = helper.compileInput('
');
+
+ $rootScope.$apply(function() {
+ $rootScope.value = new Date(Date.UTC(2013, 6, 31, 20, 0, 0));
+ });
+ helper.changeInputValueTo('2013-09');
+ expect(+$rootScope.value).toBe(Date.UTC(2013, 7, 31, 20, 0, 0));
+ expect(inputElm.val()).toBe('2013-09');
+ });
+
describe('min', function() {
var inputElm;
beforeEach(function() {
@@ -752,12 +772,8 @@ describe('input', function() {
expect(inputElm.val()).toBe('2013-W02');
- try {
- //set to text for browsers with datetime-local validation.
- inputElm[0].setAttribute('type', 'text');
- } catch (e) {
- //for IE8
- }
+ //set to text for browsers with datetime-local validation.
+ inputElm[0].setAttribute('type', 'text');
helper.changeInputValueTo('stuff');
expect(inputElm.val()).toBe('stuff');
@@ -814,6 +830,19 @@ describe('input', function() {
});
+ it('should use any timezone if specified in the options', function() {
+ var inputElm = helper.compileInput('
');
+
+ helper.changeInputValueTo('2013-W03');
+ expect(+$rootScope.value).toBe(Date.UTC(2013, 0, 16, 19, 0, 0));
+
+ $rootScope.$apply(function() {
+ $rootScope.value = new Date(Date.UTC(2014, 0, 16, 19, 0, 0));
+ });
+ expect(inputElm.val()).toBe('2014-W03');
+ });
+
+
it('should label parse errors as `week`', function() {
var inputElm = helper.compileInput('
', {
valid: false,
@@ -928,12 +957,8 @@ describe('input', function() {
expect(inputElm.val()).toBe('2009-01-06T16:25:00.000');
- try {
- //set to text for browsers with datetime-local validation.
- inputElm[0].setAttribute('type', 'text');
- } catch (e) {
- //for IE8
- }
+ //set to text for browsers with datetime-local validation.
+ inputElm[0].setAttribute('type', 'text');
helper.changeInputValueTo('stuff');
expect(inputElm.val()).toBe('stuff');
@@ -990,6 +1015,30 @@ describe('input', function() {
});
+ it('should use any timezone if specified in the options', function() {
+ var inputElm = helper.compileInput('
');
+
+ helper.changeInputValueTo('2000-01-01T06:02');
+ expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 1, 2, 0));
+
+ $rootScope.$apply(function() {
+ $rootScope.value = new Date(Date.UTC(2001, 0, 1, 1, 2, 0));
+ });
+ expect(inputElm.val()).toBe('2001-01-01T06:02:00.000');
+ });
+
+
+ it('should fallback to default timezone in case an unknown timezone was passed', function() {
+ var inputElm = helper.compileInput(
+ '
' +
+ '
');
+
+ helper.changeGivenInputTo(inputElm.eq(0), '2000-01-01T06:02');
+ helper.changeGivenInputTo(inputElm.eq(1), '2000-01-01T06:02');
+ expect($rootScope.value1).toEqual($rootScope.value2);
+ });
+
+
it('should allow to specify the milliseconds', function() {
var inputElm = helper.compileInput('
');
@@ -1216,12 +1265,8 @@ describe('input', function() {
expect(inputElm.val()).toBe('16:25:00.000');
- try {
- //set to text for browsers with time validation.
- inputElm[0].setAttribute('type', 'text');
- } catch (e) {
- //for IE8
- }
+ //set to text for browsers with time validation.
+ inputElm[0].setAttribute('type', 'text');
helper.changeInputValueTo('stuff');
expect(inputElm.val()).toBe('stuff');
@@ -1278,6 +1323,19 @@ describe('input', function() {
});
+ it('should use any timezone if specified in the options', function() {
+ var inputElm = helper.compileInput('
');
+
+ helper.changeInputValueTo('23:02:00');
+ expect(+$rootScope.value).toBe(Date.UTC(1970, 0, 1, 18, 2, 0));
+
+ $rootScope.$apply(function() {
+ $rootScope.value = new Date(Date.UTC(1971, 0, 1, 18, 2, 0));
+ });
+ expect(inputElm.val()).toBe('23:02:00.000');
+ });
+
+
it('should allow to specify the milliseconds', function() {
var inputElm = helper.compileInput('
');
@@ -1497,12 +1555,8 @@ describe('input', function() {
expect(inputElm.val()).toBe('2014-09-14');
- try {
- //set to text for browsers with date validation.
- inputElm[0].setAttribute('type', 'text');
- } catch (e) {
- //for IE8
- }
+ //set to text for browsers with date validation.
+ inputElm[0].setAttribute('type', 'text');
helper.changeInputValueTo('1-2-3');
expect(inputElm.val()).toBe('1-2-3');
@@ -1559,6 +1613,19 @@ describe('input', function() {
});
+ it('should use any timezone if specified in the options', function() {
+ var inputElm = helper.compileInput('
');
+
+ helper.changeInputValueTo('2000-01-01');
+ expect(+$rootScope.value).toBe(Date.UTC(1999, 11, 31, 19, 0, 0));
+
+ $rootScope.$apply(function() {
+ $rootScope.value = new Date(Date.UTC(2000, 11, 31, 19, 0, 0));
+ });
+ expect(inputElm.val()).toBe('2001-01-01');
+ });
+
+
it('should label parse errors as `date`', function() {
var inputElm = helper.compileInput('
', {
valid: false,
@@ -1756,12 +1823,10 @@ describe('input', function() {
$rootScope.$apply('age = 123');
expect(inputElm.val()).toBe('123');
- try {
- // to allow non-number values, we have to change type so that
- // the browser which have number validation will not interfere with
- // this test. IE8 won't allow it hence the catch.
- inputElm[0].setAttribute('type', 'text');
- } catch (e) {}
+ // to allow non-number values, we have to change type so that
+ // the browser which have number validation will not interfere with
+ // this test.
+ inputElm[0].setAttribute('type', 'text');
helper.changeInputValueTo('123X');
expect(inputElm.val()).toBe('123X');
@@ -1852,6 +1917,71 @@ describe('input', function() {
});
+ it('should parse exponential notation', function() {
+ var inputElm = helper.compileInput('
');
+
+ // #.###e+##
+ $rootScope.form.alias.$setViewValue("1.23214124123412412e+26");
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(1.23214124123412412e+26);
+
+ // #.###e##
+ $rootScope.form.alias.$setViewValue("1.23214124123412412e26");
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(1.23214124123412412e26);
+
+ // #.###e-##
+ $rootScope.form.alias.$setViewValue("1.23214124123412412e-26");
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(1.23214124123412412e-26);
+
+ // ####e+##
+ $rootScope.form.alias.$setViewValue("123214124123412412e+26");
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(123214124123412412e26);
+
+ // ####e##
+ $rootScope.form.alias.$setViewValue("123214124123412412e26");
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(123214124123412412e26);
+
+ // ####e-##
+ $rootScope.form.alias.$setViewValue("123214124123412412e-26");
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(123214124123412412e-26);
+
+ // #.###E+##
+ $rootScope.form.alias.$setViewValue("1.23214124123412412E+26");
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(1.23214124123412412e+26);
+
+ // #.###E##
+ $rootScope.form.alias.$setViewValue("1.23214124123412412E26");
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(1.23214124123412412e26);
+
+ // #.###E-##
+ $rootScope.form.alias.$setViewValue("1.23214124123412412E-26");
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(1.23214124123412412e-26);
+
+ // ####E+##
+ $rootScope.form.alias.$setViewValue("123214124123412412E+26");
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(123214124123412412e26);
+
+ // ####E##
+ $rootScope.form.alias.$setViewValue("123214124123412412E26");
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(123214124123412412e26);
+
+ // ####E-##
+ $rootScope.form.alias.$setViewValue("123214124123412412E-26");
+ expect(inputElm).toBeValid();
+ expect($rootScope.value).toBe(123214124123412412e-26);
+ });
+
+
describe('min', function() {
it('should validate', function() {
diff --git a/test/ng/directive/ngClassSpec.js b/test/ng/directive/ngClassSpec.js
index 2b174dcc2a91..28a49a6bb364 100644
--- a/test/ng/directive/ngClassSpec.js
+++ b/test/ng/directive/ngClassSpec.js
@@ -33,6 +33,32 @@ describe('ngClass', function() {
}));
+ it('should add new and remove old classes with same names as Object.prototype properties dynamically', inject(function($rootScope, $compile) {
+ /* jshint -W001 */
+ element = $compile('
')($rootScope);
+ $rootScope.dynClass = { watch: true, hasOwnProperty: true, isPrototypeOf: true };
+ $rootScope.$digest();
+ expect(element.hasClass('existing')).toBe(true);
+ expect(element.hasClass('watch')).toBe(true);
+ expect(element.hasClass('hasOwnProperty')).toBe(true);
+ expect(element.hasClass('isPrototypeOf')).toBe(true);
+
+ $rootScope.dynClass.watch = false;
+ $rootScope.$digest();
+ expect(element.hasClass('existing')).toBe(true);
+ expect(element.hasClass('watch')).toBe(false);
+ expect(element.hasClass('hasOwnProperty')).toBe(true);
+ expect(element.hasClass('isPrototypeOf')).toBe(true);
+
+ delete $rootScope.dynClass;
+ $rootScope.$digest();
+ expect(element.hasClass('existing')).toBe(true);
+ expect(element.hasClass('watch')).toBe(false);
+ expect(element.hasClass('hasOwnProperty')).toBe(false);
+ expect(element.hasClass('isPrototypeOf')).toBe(false);
+ }));
+
+
it('should support adding multiple classes via an array', inject(function($rootScope, $compile) {
element = $compile('
')($rootScope);
$rootScope.$digest();
@@ -63,6 +89,17 @@ describe('ngClass', function() {
expect(element.hasClass('AnotB')).toBeFalsy();
}));
+ it('should support adding multiple classes via an array mixed with conditionally via a map', inject(function($rootScope, $compile) {
+ element = $compile('
')($rootScope);
+ $rootScope.$digest();
+ expect(element.hasClass('existing')).toBeTruthy();
+ expect(element.hasClass('A')).toBeTruthy();
+ expect(element.hasClass('B')).toBeFalsy();
+ $rootScope.condition = true;
+ $rootScope.$digest();
+ expect(element.hasClass('B')).toBeTruthy();
+
+ }));
it('should remove classes when the referenced object is the same but its property is changed',
inject(function($rootScope, $compile) {
@@ -414,14 +451,13 @@ describe('ngClass animations', function() {
});
});
- it("should consider the ngClass expression evaluation before performing an animation", function() {
+ it("should combine the ngClass evaluation with the enter animation", function() {
//mocks are not used since the enter delegation method is called before addClass and
//it makes it impossible to test to see that addClass is called first
module('ngAnimate');
module('ngAnimateMock');
- var digestQueue = [];
module(function($animateProvider) {
$animateProvider.register('.crazy', function() {
return {
@@ -431,29 +467,13 @@ describe('ngClass animations', function() {
}
};
});
-
- return function($rootScope) {
- var before = $rootScope.$$postDigest;
- $rootScope.$$postDigest = function() {
- var args = arguments;
- digestQueue.push(function() {
- before.apply($rootScope, args);
- });
- };
- };
});
- inject(function($compile, $rootScope, $browser, $rootElement, $animate, $timeout, $document) {
-
- // Animations need to digest twice in order to be enabled regardless if there are no template HTTP requests.
- $rootScope.$digest();
- digestQueue.shift()();
-
- $rootScope.$digest();
- digestQueue.shift()();
+ inject(function($compile, $rootScope, $browser, $rootElement, $animate, $timeout, $$body, $$rAF) {
+ $animate.enabled(true);
$rootScope.val = 'crazy';
element = angular.element('
');
- jqLite($document[0].body).append($rootElement);
+ $$body.append($rootElement);
$compile(element)($rootScope);
@@ -467,25 +487,13 @@ describe('ngClass animations', function() {
expect(element.hasClass('crazy')).toBe(false);
expect(enterComplete).toBe(false);
- expect(digestQueue.length).toBe(1);
$rootScope.$digest();
-
- $timeout.flush();
-
- expect(element.hasClass('crazy')).toBe(true);
- expect(enterComplete).toBe(false);
-
- digestQueue.shift()(); //enter
- expect(digestQueue.length).toBe(0);
-
- //we don't normally need this, but since the timing between digests
- //is spaced-out then it is required so that the original digestion
- //is kicked into gear
+ $$rAF.flush();
$rootScope.$digest();
- $animate.triggerCallbacks();
- expect(element.data('state')).toBe('crazy-enter');
+ expect(element.hasClass('crazy')).toBe(true);
expect(enterComplete).toBe(true);
+ expect(element.data('state')).toBe('crazy-enter');
});
});
diff --git a/test/ng/directive/ngIncludeSpec.js b/test/ng/directive/ngIncludeSpec.js
index b1ec3e735e1f..9cecde9825a8 100644
--- a/test/ng/directive/ngIncludeSpec.js
+++ b/test/ng/directive/ngIncludeSpec.js
@@ -354,11 +354,7 @@ describe('ngInclude', function() {
expect(window._ngIncludeCausesScriptToRun).toBe(true);
- // IE8 doesn't like deleting properties of window
- window._ngIncludeCausesScriptToRun = undefined;
- try {
- delete window._ngIncludeCausesScriptToRun;
- } catch (e) {}
+ delete window._ngIncludeCausesScriptToRun;
}));
diff --git a/test/ng/directive/ngModelSpec.js b/test/ng/directive/ngModelSpec.js
index 583414deaa9c..7f4bfc205dfb 100644
--- a/test/ng/directive/ngModelSpec.js
+++ b/test/ng/directive/ngModelSpec.js
@@ -578,6 +578,29 @@ describe('ngModel', function() {
dealoc(form);
}));
+
+
+ it('should set NaN as the $modelValue when an asyncValidator is present',
+ inject(function($q) {
+
+ ctrl.$asyncValidators.test = function() {
+ return $q(function(resolve, reject) {
+ resolve();
+ });
+ };
+
+ scope.$apply('value = 10');
+ expect(ctrl.$modelValue).toBe(10);
+
+ expect(function() {
+ scope.$apply(function() {
+ scope.value = NaN;
+ });
+ }).not.toThrow();
+
+ expect(ctrl.$modelValue).toBeNaN();
+
+ }));
});
@@ -1068,6 +1091,31 @@ describe('ngModel', function() {
}));
+ it('should be possible to extend Object prototype and still be able to do form validation',
+ inject(function($compile, $rootScope) {
+ Object.prototype.someThing = function() {};
+ var element = $compile('
')($rootScope);
+ var inputElm = element.find('input');
+
+ var formCtrl = $rootScope.myForm;
+ var usernameCtrl = formCtrl.username;
+
+ $rootScope.$digest();
+ expect(usernameCtrl.$invalid).toBe(true);
+ expect(formCtrl.$invalid).toBe(true);
+
+ usernameCtrl.$setViewValue('valid-username');
+ $rootScope.$digest();
+
+ expect(usernameCtrl.$invalid).toBe(false);
+ expect(formCtrl.$invalid).toBe(false);
+ delete Object.prototype.someThing;
+
+ dealoc(element);
+ }));
+
it('should re-evaluate the form validity state once the asynchronous promise has been delivered',
inject(function($compile, $rootScope, $q) {
@@ -1133,17 +1181,17 @@ describe('ngModel', function() {
it('should minimize janky setting of classes during $validate() and ngModelWatch', inject(function($animate, $compile, $rootScope) {
- var addClass = $animate.$$addClassImmediately;
- var removeClass = $animate.$$removeClassImmediately;
+ var addClass = $animate.addClass;
+ var removeClass = $animate.removeClass;
var addClassCallCount = 0;
var removeClassCallCount = 0;
var input;
- $animate.$$addClassImmediately = function(element, className) {
+ $animate.addClass = function(element, className) {
if (input && element[0] === input[0]) ++addClassCallCount;
return addClass.call($animate, element, className);
};
- $animate.$$removeClassImmediately = function(element, className) {
+ $animate.removeClass = function(element, className) {
if (input && element[0] === input[0]) ++removeClassCallCount;
return removeClass.call($animate, element, className);
};
@@ -1221,6 +1269,96 @@ describe('ngModel', function() {
expect(ctrl.$validators.mock).toHaveBeenCalledWith('a', 'ab');
expect(ctrl.$validators.mock.calls.length).toEqual(2);
});
+
+ it('should validate correctly when $parser name equals $validator key', function() {
+
+ ctrl.$validators.parserOrValidator = function(value) {
+ switch (value) {
+ case 'allInvalid':
+ case 'parseValid-validatorsInvalid':
+ case 'stillParseValid-validatorsInvalid':
+ return false;
+ default:
+ return true;
+ }
+ };
+
+ ctrl.$validators.validator = function(value) {
+ switch (value) {
+ case 'allInvalid':
+ case 'parseValid-validatorsInvalid':
+ case 'stillParseValid-validatorsInvalid':
+ return false;
+ default:
+ return true;
+ }
+ };
+
+ ctrl.$$parserName = 'parserOrValidator';
+ ctrl.$parsers.push(function(value) {
+ switch (value) {
+ case 'allInvalid':
+ case 'stillAllInvalid':
+ case 'parseInvalid-validatorsValid':
+ case 'stillParseInvalid-validatorsValid':
+ return undefined;
+ default:
+ return value;
+ }
+ });
+
+ //Parser and validators are invalid
+ scope.$apply('value = "allInvalid"');
+ expect(scope.value).toBe('allInvalid');
+ expect(ctrl.$error).toEqual({parserOrValidator: true, validator: true});
+
+ ctrl.$validate();
+ expect(scope.value).toEqual('allInvalid');
+ expect(ctrl.$error).toEqual({parserOrValidator: true, validator: true});
+
+ ctrl.$setViewValue('stillAllInvalid');
+ expect(scope.value).toBeUndefined();
+ expect(ctrl.$error).toEqual({parserOrValidator: true});
+
+ ctrl.$validate();
+ expect(scope.value).toBeUndefined();
+ expect(ctrl.$error).toEqual({parserOrValidator: true});
+
+ //Parser is valid, validators are invalid
+ scope.$apply('value = "parseValid-validatorsInvalid"');
+ expect(scope.value).toBe('parseValid-validatorsInvalid');
+ expect(ctrl.$error).toEqual({parserOrValidator: true, validator: true});
+
+ ctrl.$validate();
+ expect(scope.value).toBe('parseValid-validatorsInvalid');
+ expect(ctrl.$error).toEqual({parserOrValidator: true, validator: true});
+
+ ctrl.$setViewValue('stillParseValid-validatorsInvalid');
+ expect(scope.value).toBeUndefined();
+ expect(ctrl.$error).toEqual({parserOrValidator: true, validator: true});
+
+ ctrl.$validate();
+ expect(scope.value).toBeUndefined();
+ expect(ctrl.$error).toEqual({parserOrValidator: true, validator: true});
+
+ //Parser is invalid, validators are valid
+ scope.$apply('value = "parseInvalid-validatorsValid"');
+ expect(scope.value).toBe('parseInvalid-validatorsValid');
+ expect(ctrl.$error).toEqual({});
+
+ ctrl.$validate();
+ expect(scope.value).toBe('parseInvalid-validatorsValid');
+ expect(ctrl.$error).toEqual({});
+
+ ctrl.$setViewValue('stillParseInvalid-validatorsValid');
+ expect(scope.value).toBeUndefined();
+ expect(ctrl.$error).toEqual({parserOrValidator: true});
+
+ ctrl.$validate();
+ expect(scope.value).toBeUndefined();
+ expect(ctrl.$error).toEqual({parserOrValidator: true});
+ });
+
});
});
diff --git a/test/ng/directive/ngOptionsSpec.js b/test/ng/directive/ngOptionsSpec.js
index 21f890fc5406..5498a61d9d12 100644
--- a/test/ng/directive/ngOptionsSpec.js
+++ b/test/ng/directive/ngOptionsSpec.js
@@ -151,11 +151,18 @@ describe('ngOptions', function() {
it('should throw when not formated "? for ? in ?"', function() {
expect(function() {
- compile('
')(scope);
+ compile('
');
}).toThrowMinErr('ngOptions', 'iexp', /Expected expression in form of/);
});
+ it('should have optional dependency on ngModel', function() {
+ expect(function() {
+ compile('
');
+ }).not.toThrow();
+ });
+
+
it('should render a list', function() {
createSingleSelect();
@@ -173,6 +180,47 @@ describe('ngOptions', function() {
});
+ it('should not include properties with non-numeric keys in array-like collections when using array syntax', function() {
+ createSelect({
+ 'ng-model':'selected',
+ 'ng-options':'value for value in values'
+ });
+
+ scope.$apply(function() {
+ scope.values = { 0: 'X', 1: 'Y', 2: 'Z', 'a': 'A', length: 3};
+ scope.selected = scope.values[1];
+ });
+
+ var options = element.find('option');
+ expect(options.length).toEqual(3);
+ expect(options.eq(0)).toEqualOption('X');
+ expect(options.eq(1)).toEqualOption('Y');
+ expect(options.eq(2)).toEqualOption('Z');
+
+ });
+
+
+ it('should include properties with non-numeric keys in array-like collections when using object syntax', function() {
+ createSelect({
+ 'ng-model':'selected',
+ 'ng-options':'value for (key, value) in values'
+ });
+
+ scope.$apply(function() {
+ scope.values = { 0: 'X', 1: 'Y', 2: 'Z', 'a': 'A', length: 3};
+ scope.selected = scope.values[1];
+ });
+
+ var options = element.find('option');
+ expect(options.length).toEqual(5);
+ expect(options.eq(0)).toEqualOption('X');
+ expect(options.eq(1)).toEqualOption('Y');
+ expect(options.eq(2)).toEqualOption('Z');
+ expect(options.eq(3)).toEqualOption('A');
+ expect(options.eq(4)).toEqualOption(3);
+ });
+
+
it('should render an object', function() {
createSelect({
'ng-model': 'selected',
@@ -355,6 +403,7 @@ describe('ngOptions', function() {
expect(options.eq(2)).toEqualOption(scope.values[2], 'D');
});
+
it('should preserve pre-existing empty option', function() {
createSingleSelect(true);
@@ -399,6 +448,43 @@ describe('ngOptions', function() {
});
+ it('should not watch non-numeric array properties', function() {
+ createSelect({
+ 'ng-options': 'value as createLabel(value) for value in array',
+ 'ng-model': 'selected'
+ });
+ scope.createLabel = jasmine.createSpy('createLabel').andCallFake(function(value) { return value; });
+ scope.array = ['a', 'b', 'c'];
+ scope.array.$$private = 'do not watch';
+ scope.array.$property = 'do not watch';
+ scope.array.other = 'do not watch';
+ scope.array.fn = function() {};
+ scope.selected = 'b';
+ scope.$digest();
+
+ expect(scope.createLabel).toHaveBeenCalledWith('a');
+ expect(scope.createLabel).toHaveBeenCalledWith('b');
+ expect(scope.createLabel).toHaveBeenCalledWith('c');
+ expect(scope.createLabel).not.toHaveBeenCalledWith('do not watch');
+ expect(scope.createLabel).not.toHaveBeenCalledWith(jasmine.any(Function));
+ });
+
+
+ it('should not watch object properties that start with $ or $$', function() {
+ createSelect({
+ 'ng-options': 'key as createLabel(key) for (key, value) in object',
+ 'ng-model': 'selected'
+ });
+ scope.createLabel = jasmine.createSpy('createLabel').andCallFake(function(value) { return value; });
+ scope.object = {'regularProperty': 'visible', '$$private': 'invisible', '$property': 'invisible'};
+ scope.selected = 'regularProperty';
+ scope.$digest();
+
+ expect(scope.createLabel).toHaveBeenCalledWith('regularProperty');
+ expect(scope.createLabel).not.toHaveBeenCalledWith('$$private');
+ expect(scope.createLabel).not.toHaveBeenCalledWith('$property');
+ });
+
it('should allow expressions over multiple lines', function() {
scope.isNotFoo = function(item) {
return item.name !== 'Foo';
@@ -472,6 +558,30 @@ describe('ngOptions', function() {
});
+ it('should update the label if only the property has changed', function() {
+ // ng-options="value.name for value in values"
+ // ng-model="selected"
+ createSingleSelect();
+
+ scope.$apply(function() {
+ scope.values = [{name: 'A'}, {name: 'B'}, {name: 'C'}];
+ scope.selected = scope.values[0];
+ });
+
+ var options = element.find('option');
+ expect(options.eq(0).prop('label')).toEqual('A');
+ expect(options.eq(1).prop('label')).toEqual('B');
+ expect(options.eq(2).prop('label')).toEqual('C');
+
+
+ scope.$apply('values[0].name = "X"');
+
+ options = element.find('option');
+ expect(options.eq(0).prop('label')).toEqual('X');
+
+ });
+
+
// bug fix #9714
it('should select the matching option when the options are updated', function() {
@@ -533,6 +643,199 @@ describe('ngOptions', function() {
});
+ describe('disableWhen expression', function() {
+
+ describe('on single select', function() {
+
+ it('should disable options', function() {
+
+ scope.selected = '';
+ scope.options = [
+ { name: 'white', value: '#FFFFFF' },
+ { name: 'one', value: 1, unavailable: true },
+ { name: 'notTrue', value: false },
+ { name: 'thirty', value: 30, unavailable: false }
+ ];
+ createSelect({
+ 'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
+ 'ng-model': 'selected'
+ });
+ var options = element.find('option');
+
+ expect(options.length).toEqual(5);
+ expect(options.eq(1).prop('disabled')).toEqual(false);
+ expect(options.eq(2).prop('disabled')).toEqual(true);
+ expect(options.eq(3).prop('disabled')).toEqual(false);
+ expect(options.eq(4).prop('disabled')).toEqual(false);
+ });
+
+
+ it('should not select disabled options when model changes', function() {
+ scope.options = [
+ { name: 'white', value: '#FFFFFF' },
+ { name: 'one', value: 1, unavailable: true },
+ { name: 'notTrue', value: false },
+ { name: 'thirty', value: 30, unavailable: false }
+ ];
+ createSelect({
+ 'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
+ 'ng-model': 'selected'
+ });
+
+ // Initially the model is set to an enabled option
+ scope.$apply('selected = 30');
+ var options = element.find('option');
+ expect(options.eq(3).prop('selected')).toEqual(true);
+
+ // Now set the model to a disabled option
+ scope.$apply('selected = 1');
+ options = element.find('option');
+
+ expect(element.val()).toEqualUnknownValue('?');
+ expect(options.length).toEqual(5);
+ expect(options.eq(0).prop('selected')).toEqual(true);
+ expect(options.eq(2).prop('selected')).toEqual(false);
+ expect(options.eq(4).prop('selected')).toEqual(false);
+ });
+
+
+ it('should select options in model when they become enabled', function() {
+ scope.options = [
+ { name: 'white', value: '#FFFFFF' },
+ { name: 'one', value: 1, unavailable: true },
+ { name: 'notTrue', value: false },
+ { name: 'thirty', value: 30, unavailable: false }
+ ];
+ createSelect({
+ 'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
+ 'ng-model': 'selected'
+ });
+
+ // Set the model to a disabled option
+ scope.$apply('selected = 1');
+ var options = element.find('option');
+
+ expect(element.val()).toEqualUnknownValue('?');
+ expect(options.length).toEqual(5);
+ expect(options.eq(0).prop('selected')).toEqual(true);
+ expect(options.eq(2).prop('selected')).toEqual(false);
+ expect(options.eq(4).prop('selected')).toEqual(false);
+
+ // Now enable that option
+ scope.$apply(function() {
+ scope.options[1].unavailable = false;
+ });
+
+ expect(element).toEqualSelectValue(1);
+ options = element.find('option');
+ expect(options.length).toEqual(4);
+ expect(options.eq(1).prop('selected')).toEqual(true);
+ expect(options.eq(3).prop('selected')).toEqual(false);
+ });
+ });
+
+
+ describe('on multi select', function() {
+
+ it('should disable options', function() {
+
+ scope.selected = [];
+ scope.options = [
+ { name: 'a', value: 0 },
+ { name: 'b', value: 1, unavailable: true },
+ { name: 'c', value: 2 },
+ { name: 'd', value: 3, unavailable: false }
+ ];
+ createSelect({
+ 'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
+ 'multiple': true,
+ 'ng-model': 'selected'
+ });
+ var options = element.find('option');
+
+ expect(options.eq(0).prop('disabled')).toEqual(false);
+ expect(options.eq(1).prop('disabled')).toEqual(true);
+ expect(options.eq(2).prop('disabled')).toEqual(false);
+ expect(options.eq(3).prop('disabled')).toEqual(false);
+ });
+
+
+ it('should not select disabled options when model changes', function() {
+ scope.options = [
+ { name: 'a', value: 0 },
+ { name: 'b', value: 1, unavailable: true },
+ { name: 'c', value: 2 },
+ { name: 'd', value: 3, unavailable: false }
+ ];
+ createSelect({
+ 'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
+ 'multiple': true,
+ 'ng-model': 'selected'
+ });
+
+ // Initially the model is set to an enabled option
+ scope.$apply('selected = [3]');
+ var options = element.find('option');
+ expect(options.eq(0).prop('selected')).toEqual(false);
+ expect(options.eq(1).prop('selected')).toEqual(false);
+ expect(options.eq(2).prop('selected')).toEqual(false);
+ expect(options.eq(3).prop('selected')).toEqual(true);
+
+ // Now add a disabled option
+ scope.$apply('selected = [1,3]');
+ options = element.find('option');
+ expect(options.eq(0).prop('selected')).toEqual(false);
+ expect(options.eq(1).prop('selected')).toEqual(false);
+ expect(options.eq(2).prop('selected')).toEqual(false);
+ expect(options.eq(3).prop('selected')).toEqual(true);
+
+ // Now only select the disabled option
+ scope.$apply('selected = [1]');
+ expect(options.eq(0).prop('selected')).toEqual(false);
+ expect(options.eq(1).prop('selected')).toEqual(false);
+ expect(options.eq(2).prop('selected')).toEqual(false);
+ expect(options.eq(3).prop('selected')).toEqual(false);
+ });
+
+
+ it('should select options in model when they become enabled', function() {
+ scope.options = [
+ { name: 'a', value: 0 },
+ { name: 'b', value: 1, unavailable: true },
+ { name: 'c', value: 2 },
+ { name: 'd', value: 3, unavailable: false }
+ ];
+ createSelect({
+ 'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
+ 'multiple': true,
+ 'ng-model': 'selected'
+ });
+
+ // Set the model to a disabled option
+ scope.$apply('selected = [1]');
+ var options = element.find('option');
+
+ expect(options.eq(0).prop('selected')).toEqual(false);
+ expect(options.eq(1).prop('selected')).toEqual(false);
+ expect(options.eq(2).prop('selected')).toEqual(false);
+ expect(options.eq(3).prop('selected')).toEqual(false);
+
+ // Now enable that option
+ scope.$apply(function() {
+ scope.options[1].unavailable = false;
+ });
+
+ expect(element).toEqualSelectValue([1], true);
+ options = element.find('option');
+ expect(options.eq(0).prop('selected')).toEqual(false);
+ expect(options.eq(1).prop('selected')).toEqual(true);
+ expect(options.eq(2).prop('selected')).toEqual(false);
+ expect(options.eq(3).prop('selected')).toEqual(false);
+ });
+ });
+ });
+
+
describe('selectAs expression', function() {
beforeEach(function() {
scope.arr = [{id: 10, label: 'ten'}, {id:20, label: 'twenty'}];
@@ -575,6 +878,54 @@ describe('ngOptions', function() {
});
+ it('should re-render if an item in an array source is added/removed', function() {
+ createSelect({
+ 'ng-model': 'selected',
+ 'multiple': true,
+ 'ng-options': 'item.id as item.label for item in arr'
+ });
+
+ scope.$apply(function() {
+ scope.selected = [10];
+ });
+ expect(element).toEqualSelectValue([10], true);
+
+ scope.$apply(function() {
+ scope.selected.push(20);
+ });
+ expect(element).toEqualSelectValue([10, 20], true);
+
+
+ scope.$apply(function() {
+ scope.selected.shift();
+ });
+ expect(element).toEqualSelectValue([20], true);
+ });
+
+
+ it('should handle a options containing circular references', function() {
+ scope.arr[0].ref = scope.arr[0];
+ scope.selected = [scope.arr[0]];
+ createSelect({
+ 'ng-model': 'selected',
+ 'multiple': true,
+ 'ng-options': 'item as item.label for item in arr'
+ });
+ expect(element).toEqualSelectValue([scope.arr[0]], true);
+
+ scope.$apply(function() {
+ scope.selected.push(scope.arr[1]);
+ });
+ expect(element).toEqualSelectValue([scope.arr[0], scope.arr[1]], true);
+
+
+ scope.$apply(function() {
+ scope.selected.pop();
+ });
+ expect(element).toEqualSelectValue([scope.arr[0]], true);
+ });
+
+
it('should support single select with object source', function() {
createSelect({
'ng-model': 'selected',
@@ -655,6 +1006,86 @@ describe('ngOptions', function() {
expect(options.eq(2)).toEqualTrackedOption(20, 'twenty');
});
+
+ it('should update the selected option even if only the tracked property on the selected object changes (single)', function() {
+ createSelect({
+ 'ng-model': 'selected',
+ 'ng-options': 'item.label for item in arr track by item.id'
+ });
+
+ scope.$apply(function() {
+ scope.selected = {id: 10, label: 'ten'};
+ });
+
+ expect(element.val()).toEqual('10');
+
+ // Update the properties on the selected object, rather than replacing the whole object
+ scope.$apply(function() {
+ scope.selected.id = 20;
+ scope.selected.label = 'new twenty';
+ });
+
+ // The value of the select should change since the id property changed
+ expect(element.val()).toEqual('20');
+
+ // But the label of the selected option does not change
+ var option = element.find('option').eq(1);
+ expect(option.prop('selected')).toEqual(true);
+ expect(option.text()).toEqual('twenty'); // not 'new twenty'
+ });
+
+
+ it('should update the selected options even if only the tracked properties on the objects in the ' +
+ 'selected collection change (multi)', function() {
+ createSelect({
+ 'ng-model': 'selected',
+ 'multiple': true,
+ 'ng-options': 'item.label for item in arr track by item.id'
+ });
+
+ scope.$apply(function() {
+ scope.selected = [{id: 10, label: 'ten'}];
+ });
+
+ expect(element.val()).toEqual(['10']);
+
+ // Update the tracked property on the object in the selected array, rather than replacing the whole object
+ scope.$apply(function() {
+ scope.selected[0].id = 20;
+ });
+
+ // The value of the select should change since the id property changed
+ expect(element.val()).toEqual(['20']);
+
+ // But the label of the selected option does not change
+ var option = element.find('option').eq(1);
+ expect(option.prop('selected')).toEqual(true);
+ expect(option.text()).toEqual('twenty'); // not 'new twenty'
+ });
+
+
+ it('should prevent changes to the selected object from modifying the options objects (single)', function() {
+
+ createSelect({
+ 'ng-model': 'selected',
+ 'ng-options': 'item.label for item in arr track by item.id'
+ });
+
+ element.val('10');
+ browserTrigger(element, 'change');
+
+ expect(scope.selected).toEqual(scope.arr[0]);
+
+ scope.$apply(function() {
+ scope.selected.id = 20;
+ });
+
+ expect(scope.selected).not.toEqual(scope.arr[0]);
+ expect(element.val()).toEqual('20');
+ expect(scope.arr).toEqual([{id: 10, label: 'ten'}, {id:20, label: 'twenty'}]);
+ });
+
+
it('should preserve value even when reference has changed (single&array)', function() {
createSelect({
'ng-model': 'selected',
@@ -717,7 +1148,7 @@ describe('ngOptions', function() {
expect(element.val()).toBe('10');
setSelectValue(element, 1);
- expect(scope.selected).toBe(scope.obj['2']);
+ expect(scope.selected).toEqual(scope.obj['2']);
});
@@ -759,6 +1190,104 @@ describe('ngOptions', function() {
});
}).not.toThrow();
});
+
+ it('should re-render if the tracked property of the model is changed when using trackBy', function() {
+
+ createSelect({
+ 'ng-model': 'selected',
+ 'ng-options': 'item for item in arr track by item.id'
+ });
+
+ scope.$apply(function() {
+ scope.selected = {id: 10, label: 'ten'};
+ });
+
+ spyOn(element.controller('ngModel'), '$render');
+
+ scope.$apply(function() {
+ scope.arr[0].id = 20;
+ });
+
+ // update render due to equality watch
+ expect(element.controller('ngModel').$render).toHaveBeenCalled();
+
+ });
+
+ it('should not set view value again if the tracked property of the model has not changed when using trackBy', function() {
+
+ createSelect({
+ 'ng-model': 'selected',
+ 'ng-options': 'item for item in arr track by item.id'
+ });
+
+ scope.$apply(function() {
+ scope.selected = {id: 10, label: 'ten'};
+ });
+
+ spyOn(element.controller('ngModel'), '$setViewValue');
+
+ scope.$apply(function() {
+ scope.arr[0] = {id: 10, label: 'ten'};
+ });
+
+ expect(element.controller('ngModel').$setViewValue).not.toHaveBeenCalled();
+ });
+
+ it('should not re-render if a property of the model is changed when not using trackBy', function() {
+
+ createSelect({
+ 'ng-model': 'selected',
+ 'ng-options': 'item for item in arr'
+ });
+
+ scope.$apply(function() {
+ scope.selected = scope.arr[0];
+ });
+
+ spyOn(element.controller('ngModel'), '$render');
+
+ scope.$apply(function() {
+ scope.selected.label = 'changed';
+ });
+
+ // no render update as no equality watch
+ expect(element.controller('ngModel').$render).not.toHaveBeenCalled();
+ });
+
+
+ it('should handle options containing circular references (single)', function() {
+ scope.arr[0].ref = scope.arr[0];
+ createSelect({
+ 'ng-model': 'selected',
+ 'ng-options': 'item for item in arr track by item.id'
+ });
+
+ expect(function() {
+ scope.$apply(function() {
+ scope.selected = scope.arr[0];
+ });
+ }).not.toThrow();
+ });
+
+
+ it('should handle options containing circular references (multiple)', function() {
+ scope.arr[0].ref = scope.arr[0];
+ createSelect({
+ 'ng-model': 'selected',
+ 'multiple': true,
+ 'ng-options': 'item for item in arr track by item.id'
+ });
+
+ expect(function() {
+ scope.$apply(function() {
+ scope.selected = [scope.arr[0]];
+ });
+
+ scope.$apply(function() {
+ scope.selected.push(scope.arr[1]);
+ });
+ }).not.toThrow();
+ });
});
@@ -796,7 +1325,7 @@ describe('ngOptions', function() {
element.val('10');
browserTrigger(element, 'change');
- expect(scope.selected).toBe(scope.arr[0].subItem);
+ expect(scope.selected).toEqual(scope.arr[0].subItem);
// Now reload the array
scope.$apply(function() {
@@ -1164,6 +1693,31 @@ describe('ngOptions', function() {
expect(element).toEqualSelectValue(scope.selected);
});
+ it('should bind to object disabled', function() {
+ scope.selected = 30;
+ scope.options = [
+ { name: 'white', value: '#FFFFFF' },
+ { name: 'one', value: 1, unavailable: true },
+ { name: 'notTrue', value: false },
+ { name: 'thirty', value: 30, unavailable: false }
+ ];
+ createSelect({
+ 'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
+ 'ng-model': 'selected'
+ });
+
+ var options = element.find('option');
+
+ expect(scope.options[1].unavailable).toEqual(true);
+ expect(options.eq(1).prop('disabled')).toEqual(true);
+
+ scope.$apply(function() {
+ scope.options[1].unavailable = false;
+ });
+
+ expect(scope.options[1].unavailable).toEqual(false);
+ expect(options.eq(1).prop('disabled')).toEqual(false);
+ });
it('should insert a blank option if bound to null', function() {
createSingleSelect();
@@ -1350,7 +1904,7 @@ describe('ngOptions', function() {
scope.values.pop();
});
- expect(element.val()).toEqualUnknownValue();
+ expect(element.val()).toEqual('');
expect(scope.selected).toEqual(null);
// Check after model change
@@ -1364,7 +1918,7 @@ describe('ngOptions', function() {
scope.values.pop();
});
- expect(element.val()).toEqualUnknownValue();
+ expect(element.val()).toEqual('');
expect(scope.selected).toEqual(null);
});
@@ -1653,6 +2207,37 @@ describe('ngOptions', function() {
expect(element.find('option')[1].selected).toBeTruthy();
});
+ it('should not write disabled selections from model', function() {
+ scope.selected = [30];
+ scope.options = [
+ { name: 'white', value: '#FFFFFF' },
+ { name: 'one', value: 1, unavailable: true },
+ { name: 'notTrue', value: false },
+ { name: 'thirty', value: 30, unavailable: false }
+ ];
+ createSelect({
+ 'ng-options': 'o.value as o.name disable when o.unavailable for o in options',
+ 'ng-model': 'selected',
+ 'multiple': true
+ });
+
+ var options = element.find('option');
+
+ expect(options.eq(0).prop('selected')).toEqual(false);
+ expect(options.eq(1).prop('selected')).toEqual(false);
+ expect(options.eq(2).prop('selected')).toEqual(false);
+ expect(options.eq(3).prop('selected')).toEqual(true);
+
+ scope.$apply(function() {
+ scope.selected.push(1);
+ });
+
+ expect(options.eq(0).prop('selected')).toEqual(false);
+ expect(options.eq(1).prop('selected')).toEqual(false);
+ expect(options.eq(2).prop('selected')).toEqual(false);
+ expect(options.eq(3).prop('selected')).toEqual(true);
+ });
+
it('should update model on change', function() {
createMultiSelect();
diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js
index 1205a8701b9f..0c7070fb5e6d 100644
--- a/test/ng/directive/ngRepeatSpec.js
+++ b/test/ng/directive/ngRepeatSpec.js
@@ -1084,7 +1084,7 @@ describe('ngRepeat', function() {
beforeEach(function() {
element = $compile(
'
' +
- '{{key}}:{{val}}|> ' +
+ '{{item}} ' +
' ')(scope);
a = {};
b = {};
@@ -1462,7 +1462,7 @@ describe('ngRepeat animations', function() {
}));
it('should not change the position of the block that is being animated away via a leave animation',
- inject(function($compile, $rootScope, $animate, $document, $window, $sniffer, $timeout) {
+ inject(function($compile, $rootScope, $animate, $document, $window, $sniffer, $timeout, $$rAF) {
if (!$sniffer.transitions) return;
var item;
@@ -1487,10 +1487,9 @@ describe('ngRepeat animations', function() {
$rootScope.$digest();
expect(element.text()).toBe('123'); // the original order should be preserved
- $animate.triggerReflow();
+ $$rAF.flush();
$timeout.flush(1500); // 1s * 1.5 closing buffer
expect(element.text()).toBe('13');
-
} finally {
ss.destroy();
}
diff --git a/test/ng/directive/ngStyleSpec.js b/test/ng/directive/ngStyleSpec.js
index 89056f539326..4d1ef2069bea 100644
--- a/test/ng/directive/ngStyleSpec.js
+++ b/test/ng/directive/ngStyleSpec.js
@@ -23,6 +23,15 @@ describe('ngStyle', function() {
}));
+ it('should support lazy one-time binding for object literals', inject(function($rootScope, $compile) {
+ element = $compile('
')($rootScope);
+ $rootScope.$digest();
+ expect(parseInt(element.css('height') + 0)).toEqual(0); // height could be '' or '0px'
+ $rootScope.$apply('heightStr = "40px"');
+ expect(element.css('height')).toBe('40px');
+ }));
+
+
describe('preserving styles set before and after compilation', function() {
var scope, preCompStyle, preCompVal, postCompStyle, postCompVal, element;
diff --git a/test/ng/directive/selectSpec.js b/test/ng/directive/selectSpec.js
index 725c87ddec97..3aca6176ec97 100644
--- a/test/ng/directive/selectSpec.js
+++ b/test/ng/directive/selectSpec.js
@@ -200,6 +200,70 @@ describe('select', function() {
describe('empty option', function() {
+ it('should allow empty option to be added and removed dynamically', function() {
+
+ scope.dynamicOptions = [];
+ scope.robot = '';
+ compile('
' +
+ '{{opt.display}} ' +
+ '');
+ expect(element).toEqualSelect(['? string: ?']);
+
+
+ scope.dynamicOptions = [
+ { val: '', display: '--select--' },
+ { val: 'x', display: 'robot x' },
+ { val: 'y', display: 'robot y' }
+ ];
+ scope.$digest();
+ expect(element).toEqualSelect([''], 'x', 'y');
+
+
+ scope.robot = 'x';
+ scope.$digest();
+ expect(element).toEqualSelect('', ['x'], 'y');
+
+
+ scope.dynamicOptions.shift();
+ scope.$digest();
+ expect(element).toEqualSelect(['x'], 'y');
+
+
+ scope.robot = undefined;
+ scope.$digest();
+ expect(element).toEqualSelect([unknownValue(undefined)], 'x', 'y');
+ });
+
+
+ it('should cope with a dynamic empty option added to a static empty option', function() {
+ scope.dynamicOptions = [];
+ scope.robot = 'x';
+ compile('' +
+ '--static-select-- ' +
+ '{{opt.display}} ' +
+ '');
+ scope.$digest();
+ expect(element).toEqualSelect([unknownValue('x')], '');
+
+ scope.robot = undefined;
+ scope.$digest();
+ expect(element.find('option').eq(0).prop('selected')).toBe(true);
+ expect(element.find('option').eq(0).text()).toBe('--static-select--');
+
+ scope.dynamicOptions = [
+ { val: '', display: '--dynamic-select--' },
+ { val: 'x', display: 'robot x' },
+ { val: 'y', display: 'robot y' }
+ ];
+ scope.$digest();
+ expect(element).toEqualSelect([''], '', 'x', 'y');
+
+
+ scope.dynamicOptions = [];
+ scope.$digest();
+ expect(element).toEqualSelect(['']);
+ });
+
it('should select the empty option when model is undefined', function() {
compile('' +
'--select-- ' +
@@ -237,6 +301,23 @@ describe('select', function() {
});
+ it('should remove unknown option when model is undefined', function() {
+ scope.robot = 'other';
+ compile('' +
+ '--select-- ' +
+ 'robot x ' +
+ 'robot y ' +
+ ' ');
+
+ expect(element).toEqualSelect([unknownValue('other')], '', 'x', 'y');
+
+ scope.robot = undefined;
+ scope.$digest();
+
+ expect(element).toEqualSelect([''], 'x', 'y');
+ });
+
+
describe('interactions with repeated options', function() {
it('should select empty option when model is undefined', function() {
@@ -323,7 +404,7 @@ describe('select', function() {
scope.$apply(function() {
scope.robot = null;
});
- expect(element).toEqualSelect([unknownValue(null)], '', 'c3p0', 'r2d2');
+ expect(element).toEqualSelect([''], 'c3p0', 'r2d2');
scope.$apply(function() {
scope.robot = 'r2d2';
@@ -824,6 +905,60 @@ describe('select', function() {
expect(element).toBeDirty();
});
+
+ describe('calls to $render', function() {
+
+ var ngModelCtrl;
+
+ beforeEach(function() {
+ compile(
+ '' +
+ 'A ' +
+ 'B ' +
+ ' ');
+
+ ngModelCtrl = element.controller('ngModel');
+ spyOn(ngModelCtrl, '$render').andCallThrough();
+ });
+
+
+ it('should call $render once when the reference to the viewValue changes', function() {
+ scope.$apply(function() {
+ scope.selection = ['A'];
+ });
+ expect(ngModelCtrl.$render.calls.length).toBe(1);
+
+ scope.$apply(function() {
+ scope.selection = ['A', 'B'];
+ });
+ expect(ngModelCtrl.$render.calls.length).toBe(2);
+
+ scope.$apply(function() {
+ scope.selection = [];
+ });
+ expect(ngModelCtrl.$render.calls.length).toBe(3);
+ });
+
+
+ it('should call $render once when the viewValue deep-changes', function() {
+ scope.$apply(function() {
+ scope.selection = ['A'];
+ });
+ expect(ngModelCtrl.$render.calls.length).toBe(1);
+
+ scope.$apply(function() {
+ scope.selection.push('B');
+ });
+ expect(ngModelCtrl.$render.calls.length).toBe(2);
+
+ scope.$apply(function() {
+ scope.selection.length = 0;
+ });
+ expect(ngModelCtrl.$render.calls.length).toBe(3);
+ });
+
+ });
+
});
diff --git a/test/ng/directive/styleSpec.js b/test/ng/directive/styleSpec.js
index 36f15cde1ebb..b42844be21c7 100644
--- a/test/ng/directive/styleSpec.js
+++ b/test/ng/directive/styleSpec.js
@@ -14,8 +14,7 @@ describe('style', function() {
$compile(element)($rootScope);
$rootScope.$digest();
- // read innerHTML and trim to pass on IE8
- expect(trim(element[0].innerHTML)).toBe('.header{font-size:1.5em; h3{font-size:1.5em}}');
+ expect(element[0].innerHTML).toBe('.header{font-size:1.5em; h3{font-size:1.5em}}');
}));
@@ -24,15 +23,13 @@ describe('style', function() {
$compile(element)($rootScope);
$rootScope.$digest();
- // read innerHTML and trim to pass on IE8
- expect(trim(element[0].innerHTML)).toBe('.some-container{ width: px; }');
+ expect(element[0].innerHTML).toBe('.some-container{ width: px; }');
$rootScope.$apply(function() {
$rootScope.elementWidth = 200;
});
- // read innerHTML and trim to pass on IE8
- expect(trim(element[0].innerHTML)).toBe('.some-container{ width: 200px; }');
+ expect(element[0].innerHTML).toBe('.some-container{ width: 200px; }');
}));
@@ -41,15 +38,13 @@ describe('style', function() {
$compile(element)($rootScope);
$rootScope.$digest();
- // read innerHTML and trim to pass on IE8
- expect(trim(element[0].innerHTML)).toBe('.header{ h3 { font-size: em }}');
+ expect(element[0].innerHTML).toBe('.header{ h3 { font-size: em }}');
$rootScope.$apply(function() {
$rootScope.fontSize = 1.5;
});
- // read innerHTML and trim to pass on IE8
- expect(trim(element[0].innerHTML)).toBe('.header{ h3 { font-size: 1.5em }}');
+ expect(element[0].innerHTML).toBe('.header{ h3 { font-size: 1.5em }}');
}));
@@ -58,16 +53,14 @@ describe('style', function() {
$compile(element)($rootScope);
$rootScope.$digest();
- // read innerHTML and trim to pass on IE8
- expect(trim(element[0].innerHTML)).toBe('.header{ h3 { font-size: }}');
+ expect(element[0].innerHTML).toBe('.header{ h3 { font-size: }}');
$rootScope.$apply(function() {
$rootScope.fontSize = 1.5;
$rootScope.unit = 'em';
});
- // read innerHTML and trim to pass on IE8
- expect(trim(element[0].innerHTML)).toBe('.header{ h3 { font-size: 1.5em }}');
+ expect(element[0].innerHTML).toBe('.header{ h3 { font-size: 1.5em }}');
}));
diff --git a/test/ng/documentSpec.js b/test/ng/documentSpec.js
index 064904a26db0..3fbca1d7a048 100644
--- a/test/ng/documentSpec.js
+++ b/test/ng/documentSpec.js
@@ -6,4 +6,24 @@ describe('$document', function() {
it("should inject $document", inject(function($document) {
expect($document).toEqual(jqLite(document));
}));
+
+
+ it('should be able to mock $document object', function() {
+ module({$document: {}});
+ inject(function($httpBackend, $http) {
+ $httpBackend.expectGET('/dummy').respond('dummy');
+ $http.get('/dummy');
+ $httpBackend.flush();
+ });
+ });
+
+
+ it('should be able to mock $document array', function() {
+ module({$document: [{}]});
+ inject(function($httpBackend, $http) {
+ $httpBackend.expectGET('/dummy').respond('dummy');
+ $http.get('/dummy');
+ $httpBackend.flush();
+ });
+ });
});
diff --git a/test/ng/filter/filterSpec.js b/test/ng/filter/filterSpec.js
index 0318ae57bcf6..25928d89b6c4 100644
--- a/test/ng/filter/filterSpec.js
+++ b/test/ng/filter/filterSpec.js
@@ -48,6 +48,15 @@ describe('Filter: filter', function() {
});
+ it('should ignore undefined properties of the expression object', function() {
+ var items = [{name: 'a'}, {name: 'abc'}];
+ expect(filter(items, {name: undefined})).toEqual([{name: 'a'}, {name: 'abc'}]);
+
+ items = [{first: 'misko'}, {deep: {first: 'misko'}}, {deep: {last: 'hevery'}}];
+ expect(filter(items, {deep: {first: undefined}})).toEqual([{deep: {first: 'misko'}}, {deep: {last: 'hevery'}}]);
+ });
+
+
it('should take function as predicate', function() {
var items = [{name: 'a'}, {name: 'abc', done: true}];
expect(filter(items, function(i) {return i.done;}).length).toBe(1);
@@ -416,6 +425,23 @@ describe('Filter: filter', function() {
toThrowMinErr('filter', 'notarray', 'Expected array but received: {"toString":null,"valueOf":null}');
});
+ it('should not throw an error if used with an array like object', function() {
+ function getArguments() {
+ return arguments;
+ }
+ var argsObj = getArguments({name: 'Misko'}, {name: 'Igor'}, {name: 'Brad'});
+
+ var nodeList = jqLite("Misko Igor Brad
")[0].childNodes;
+ function nodeFilterPredicate(node) {
+ return node.innerHTML.indexOf("I") !== -1;
+ }
+
+ expect(filter(argsObj, 'i').length).toBe(2);
+ expect(filter('abc','b').length).toBe(1);
+ expect(filter(nodeList, nodeFilterPredicate).length).toBe(1);
+
+ });
+
it('should return undefined when the array is undefined', function() {
expect(filter(undefined, {})).toBeUndefined();
@@ -428,15 +454,128 @@ describe('Filter: filter', function() {
});
+ it('should not throw an error if property is null when comparing object', function() {
+ var items = [
+ { office:1, people: {name:'john'}},
+ { office:2, people: {name:'jane'}},
+ { office:3, people: null}
+ ];
+ var f = { };
+ expect(filter(items, f).length).toBe(3);
+
+ f = { people:null };
+ expect(filter(items, f).length).toBe(1);
+
+ f = { people: {}};
+ expect(filter(items, f).length).toBe(2);
+
+ f = { people:{ name: '' }};
+ expect(filter(items, f).length).toBe(2);
+
+ f = { people:{ name:'john' }};
+ expect(filter(items, f).length).toBe(1);
+
+ f = { people:{ name:'j' }};
+ expect(filter(items, f).length).toBe(2);
+ });
+
+
+ it('should match `null` against `null` only', function() {
+ var items = [
+ {value: null},
+ {value: undefined},
+ {value: true},
+ {value: false},
+ {value: NaN},
+ {value: 42},
+ {value: 'null'},
+ {value: 'test'},
+ {value: {}},
+ {value: new Date()}
+ ];
+ var flt;
+
+ flt = null;
+ expect(filter(items, flt).length).toBe(1);
+ expect(filter(items, flt)[0]).toBe(items[0]);
+
+ flt = {value: null};
+ expect(filter(items, flt).length).toBe(1);
+ expect(filter(items, flt)[0]).toBe(items[0]);
+
+ flt = {value: undefined};
+ expect(filter(items, flt).length).toBe(items.length);
+
+ flt = {value: NaN};
+ expect(includes(filter(items, flt), items[0])).toBeFalsy();
+
+ flt = {value: false};
+ expect(includes(filter(items, flt), items[0])).toBeFalsy();
+
+ flt = '';
+ expect(includes(filter(items, flt), items[0])).toBeFalsy();
+
+ flt = {value: 'null'};
+ expect(includes(filter(items, flt), items[0])).toBeFalsy();
+ });
+
+
describe('should support comparator', function() {
- it('not consider `object === "[object Object]"` in non-strict comparison', function() {
+ it('not convert `null` or `undefined` to string in non-strict comparison', function() {
+ var items = [
+ {value: null},
+ {value: undefined}
+ ];
+ var flt = {value: 'u'};
+
+ expect(filter(items, flt).length).toBe(0);
+ });
+
+
+ it('not consider objects without a custom `toString` in non-strict comparison', function() {
var items = [{test: {}}];
var expr = '[object';
expect(filter(items, expr).length).toBe(0);
});
+ it('should consider objects with custom `toString()` in non-strict comparison', function() {
+ var obj = new Date(1970, 0);
+ var items = [{test: obj}];
+ expect(filter(items, '1970').length).toBe(1);
+ expect(filter(items, 1970).length).toBe(1);
+
+ obj = {
+ toString: function() { return 'custom'; }
+ };
+ items = [{test: obj}];
+ expect(filter(items, 'custom').length).toBe(1);
+ });
+
+
+ it('should cope with objects that have no `toString()` in non-strict comparison', function() {
+ var obj = Object.create(null);
+ var items = [{test: obj}];
+ expect(function() {
+ filter(items, 'foo');
+ }).not.toThrow();
+ expect(filter(items, 'foo').length).toBe(0);
+ });
+
+
+ it('should cope with objects where `toString` is not a function in non-strict comparison', function() {
+ var obj = {
+ toString: 'moo'
+ };
+ var items = [{test: obj}];
+ expect(function() {
+ filter(items, 'foo');
+ }).not.toThrow();
+ expect(filter(items, 'foo').length).toBe(0);
+ });
+
+
it('as equality when true', function() {
var items = ['misko', 'adam', 'adamson'];
var expr = 'adam';
diff --git a/test/ng/filter/filtersSpec.js b/test/ng/filter/filtersSpec.js
index bd101b432882..52ad6fbd6a4d 100644
--- a/test/ng/filter/filtersSpec.js
+++ b/test/ng/filter/filtersSpec.js
@@ -101,7 +101,7 @@ describe('filters', function() {
it('should do basic currency filtering', function() {
expect(currency(0)).toEqual('$0.00');
- expect(currency(-999)).toEqual('($999.00)');
+ expect(currency(-999)).toEqual('-$999.00');
expect(currency(1234.5678, "USD$")).toEqual('USD$1,234.57');
expect(currency(1234.5678, "USD$", 0)).toEqual('USD$1,235');
});
@@ -298,6 +298,18 @@ describe('filters', function() {
expect(date(earlyDate, "MMMM dd, y")).
toEqual('September 03, 1');
+
+ expect(date(noon, "MMMM dd, y G")).
+ toEqual('September 03, 2010 AD');
+
+ expect(date(noon, "MMMM dd, y GG")).
+ toEqual('September 03, 2010 AD');
+
+ expect(date(noon, "MMMM dd, y GGG")).
+ toEqual('September 03, 2010 AD');
+
+ expect(date(noon, "MMMM dd, y GGGG")).
+ toEqual('September 03, 2010 Anno Domini');
});
it('should accept negative numbers as strings', function() {
@@ -464,8 +476,8 @@ describe('filters', function() {
});
it('should fallback to default timezone in case an unknown timezone was passed', function() {
- var value = new angular.mock.TzDate(-2, '2003-09-10T01:02:04.000Z');
- expect(date(value, 'yyyy-MM-dd HH-mm-ssZ', 'WTF')).toEqual('2003-09-10 03-02-04+0200');
+ var value = new Date(2003, 8, 10, 3, 2, 4);
+ expect(date(value, 'yyyy-MM-dd HH-mm-ssZ', 'WTF')).toEqual(date(value, 'yyyy-MM-dd HH-mm-ssZ'));
});
});
});
diff --git a/test/ng/filter/limitToSpec.js b/test/ng/filter/limitToSpec.js
index a15fbbc58556..343290a55c5d 100644
--- a/test/ng/filter/limitToSpec.js
+++ b/test/ng/filter/limitToSpec.js
@@ -23,6 +23,19 @@ describe('Filter: limitTo', function() {
expect(limitTo(number, '3')).toEqual("100");
});
+ it('should return the first X items beginning from index Y when X and Y are positive', function() {
+ expect(limitTo(items, 3, '3')).toEqual(['d', 'e', 'f']);
+ expect(limitTo(items, '3', 3)).toEqual(['d', 'e', 'f']);
+ expect(limitTo(str, 3, 3)).toEqual("wxy");
+ expect(limitTo(str, '3', '3')).toEqual("wxy");
+ });
+
+ it('should return the first X items beginning from index Y when X is positive and Y is negative', function() {
+ expect(limitTo(items, 3, '-3')).toEqual(['f', 'g', 'h']);
+ expect(limitTo(items, '3', -3)).toEqual(['f', 'g', 'h']);
+ expect(limitTo(str, 3, -3)).toEqual("xyz");
+ expect(limitTo(str, '3', '-3')).toEqual("xyz");
+ });
it('should return the last X items when X is negative', function() {
expect(limitTo(items, -3)).toEqual(['f', 'g', 'h']);
@@ -33,6 +46,19 @@ describe('Filter: limitTo', function() {
expect(limitTo(number, '-3')).toEqual("045");
});
+ it('should return the last X items until index Y when X and Y are negative', function() {
+ expect(limitTo(items, -3, '-3')).toEqual(['c', 'd', 'e']);
+ expect(limitTo(items, '-3', -3)).toEqual(['c', 'd', 'e']);
+ expect(limitTo(str, -3, -3)).toEqual("uvw");
+ expect(limitTo(str, '-3', '-3')).toEqual("uvw");
+ });
+
+ it('should return the last X items until index Y when X is negative and Y is positive', function() {
+ expect(limitTo(items, -3, '4')).toEqual(['b', 'c', 'd']);
+ expect(limitTo(items, '-3', 4)).toEqual(['b', 'c', 'd']);
+ expect(limitTo(str, -3, 4)).toEqual("uvw");
+ expect(limitTo(str, '-3', '4')).toEqual("uvw");
+ });
it('should return an empty array when X = 0', function() {
expect(limitTo(items, 0)).toEqual([]);
@@ -60,6 +86,18 @@ describe('Filter: limitTo', function() {
expect(limitTo(str, undefined)).toEqual(str);
});
+ it('should take 0 as beginning index value when Y cannot be parsed', function() {
+ expect(limitTo(items, 3, 'bogus')).toEqual(limitTo(items, 3, 0));
+ expect(limitTo(items, -3, 'null')).toEqual(limitTo(items, -3));
+ expect(limitTo(items, '3', 'undefined')).toEqual(limitTo(items, '3', 0));
+ expect(limitTo(items, '-3', null)).toEqual(limitTo(items, '-3'));
+ expect(limitTo(items, 3, undefined)).toEqual(limitTo(items, 3, 0));
+ expect(limitTo(str, 3, 'bogus')).toEqual(limitTo(str, 3));
+ expect(limitTo(str, -3, 'null')).toEqual(limitTo(str, -3, 0));
+ expect(limitTo(str, '3', 'undefined')).toEqual(limitTo(str, '3'));
+ expect(limitTo(str, '-3', null)).toEqual(limitTo(str, '-3', 0));
+ expect(limitTo(str, 3, undefined)).toEqual(limitTo(str, 3));
+ });
it('should return input if not String or Array or Number', function() {
expect(limitTo(null, 1)).toEqual(null);
@@ -99,4 +137,32 @@ describe('Filter: limitTo', function() {
expect(limitTo(str, -Infinity)).toEqual(str);
expect(limitTo(str, '-Infinity')).toEqual(str);
});
+
+ it('should return an empty array if Y exceeds input length', function() {
+ expect(limitTo(items, '3', 12)).toEqual([]);
+ expect(limitTo(items, 4, '-12')).toEqual([]);
+ expect(limitTo(items, -3, '12')).toEqual([]);
+ expect(limitTo(items, '-4', -12)).toEqual([]);
+ });
+
+ it('should return an empty string if Y exceeds input length', function() {
+ expect(limitTo(str, '3', 12)).toEqual("");
+ expect(limitTo(str, 4, '-12')).toEqual("");
+ expect(limitTo(str, -3, '12')).toEqual("");
+ expect(limitTo(str, '-4', -12)).toEqual("");
+ });
+
+ it('should return the entire string beginning from Y if X is positive and X+Y exceeds input length', function() {
+ expect(limitTo(items, 7, 3)).toEqual(['d', 'e', 'f', 'g', 'h']);
+ expect(limitTo(items, 7, -3)).toEqual(['f', 'g', 'h']);
+ expect(limitTo(str, 6, 3)).toEqual("wxyz");
+ expect(limitTo(str, 6, -3)).toEqual("xyz");
+ });
+
+ it('should return the entire string until index Y if X is negative and X+Y exceeds input length', function() {
+ expect(limitTo(items, -7, 3)).toEqual(['a', 'b', 'c']);
+ expect(limitTo(items, -7, -3)).toEqual(['a', 'b', 'c', 'd', 'e']);
+ expect(limitTo(str, -6, 3)).toEqual("tuv");
+ expect(limitTo(str, -6, -3)).toEqual("tuvw");
+ });
});
diff --git a/test/ng/filter/orderBySpec.js b/test/ng/filter/orderBySpec.js
index 17cd6b47dbb4..9edec1532cd3 100644
--- a/test/ng/filter/orderBySpec.js
+++ b/test/ng/filter/orderBySpec.js
@@ -23,7 +23,7 @@ describe('Filter: orderBy', function() {
});
- it('shouldSortArrayInReverse', function() {
+ it('should reverse collection if `reverseOrder` param is truthy', function() {
expect(orderBy([{a:15}, {a:2}], 'a', true)).toEqualData([{a:15}, {a:2}]);
expect(orderBy([{a:15}, {a:2}], 'a', "T")).toEqualData([{a:15}, {a:2}]);
expect(orderBy([{a:15}, {a:2}], 'a', "reverse")).toEqualData([{a:15}, {a:2}]);
@@ -116,7 +116,7 @@ describe('Filter: orderBy', function() {
});
- it('should not reverse array of objects with no predicate', function() {
+ it('should not reverse array of objects with no predicate and reverse is not `true`', function() {
var array = [
{ id: 2 },
{ id: 1 },
@@ -126,6 +126,39 @@ describe('Filter: orderBy', function() {
expect(orderBy(array)).toEqualData(array);
});
+ it('should reverse array of objects with no predicate and reverse is `true`', function() {
+ var array = [
+ { id: 2 },
+ { id: 1 },
+ { id: 4 },
+ { id: 3 }
+ ];
+ var reversedArray = [
+ { id: 3 },
+ { id: 4 },
+ { id: 1 },
+ { id: 2 }
+ ];
+ expect(orderBy(array, '', true)).toEqualData(reversedArray);
+ });
+
+
+ it('should reverse array of objects with predicate of "-"', function() {
+ var array = [
+ { id: 2 },
+ { id: 1 },
+ { id: 4 },
+ { id: 3 }
+ ];
+ var reversedArray = [
+ { id: 3 },
+ { id: 4 },
+ { id: 1 },
+ { id: 2 }
+ ];
+ expect(orderBy(array, '-')).toEqualData(reversedArray);
+ });
+
it('should not reverse array of objects with null prototype and no predicate', function() {
var array = [2,1,4,3].map(function(id) {
@@ -151,6 +184,16 @@ describe('Filter: orderBy', function() {
null
]);
});
+
+
+ it('should sort array of arrays as Array.prototype.sort', function() {
+ expect(orderBy([['one'], ['two'], ['three']])).toEqualData([['one'], ['three'], ['two']]);
+ });
+
+
+ it('should sort mixed array of objects and values in a stable way', function() {
+ expect(orderBy([{foo: 2}, {foo: {}}, {foo: 3}, {foo: 4}], 'foo')).toEqualData([{foo: 2}, {foo: 3}, {foo: 4}, {foo: {}}]);
+ });
});
diff --git a/test/ng/forceReflowSpec.js b/test/ng/forceReflowSpec.js
new file mode 100644
index 000000000000..6a1885791635
--- /dev/null
+++ b/test/ng/forceReflowSpec.js
@@ -0,0 +1,52 @@
+'use strict';
+
+describe('$$forceReflow', function() {
+ it('should issue a reflow by touching the `document.body.client` when no param is provided', function() {
+ module(function($provide) {
+ var doc = jqLite('
');
+ doc[0].body = {};
+ doc[0].body.offsetWidth = 10;
+ $provide.value('$document', doc);
+ });
+ inject(function($$forceReflow) {
+ var value = $$forceReflow();
+ expect(value).toBe(11);
+ });
+ });
+
+ it('should issue a reflow by touching the `domNode.offsetWidth` when a domNode param is provided',
+ inject(function($$forceReflow) {
+
+ var elm = {};
+ elm.offsetWidth = 100;
+ expect($$forceReflow(elm)).toBe(101);
+ }));
+
+ it('should issue a reflow by touching the `jqLiteNode[0].offsetWidth` when a jqLite node param is provided',
+ inject(function($$forceReflow) {
+
+ var elm = {};
+ elm.offsetWidth = 200;
+ elm = jqLite(elm);
+ expect($$forceReflow(elm)).toBe(201);
+ }));
+
+ describe('$animate with ngAnimateMock', function() {
+ beforeEach(module('ngAnimateMock'));
+
+ it('should keep track of how many reflows have been issued',
+ inject(function($$forceReflow, $animate) {
+
+ var elm = {};
+ elm.offsetWidth = 10;
+
+ expect($animate.reflows).toBe(0);
+
+ $$forceReflow(elm);
+ $$forceReflow(elm);
+ $$forceReflow(elm);
+
+ expect($animate.reflows).toBe(3);
+ }));
+ });
+});
diff --git a/test/ng/httpBackendSpec.js b/test/ng/httpBackendSpec.js
index 12ceed5beb01..bd7f4b8f1859 100644
--- a/test/ng/httpBackendSpec.js
+++ b/test/ng/httpBackendSpec.js
@@ -50,6 +50,23 @@ describe('$httpBackend', function() {
expect(xhr.$$data).toBe(null);
});
+ it('should pass the correct falsy value to send if falsy body is set (excluding NaN)', function() {
+ var values = [false, 0, "", null, undefined];
+ angular.forEach(values, function(value) {
+ $backend('GET', '/some-url', value, noop);
+ xhr = MockXhr.$$lastInstance;
+
+ expect(xhr.$$data).toBe(value);
+ });
+ });
+
+ it('should pass NaN to send if NaN body is set', function() {
+ $backend('GET', '/some-url', NaN, noop);
+ xhr = MockXhr.$$lastInstance;
+
+ expect(isNaN(xhr.$$data)).toEqual(true);
+ });
+
it('should call completion function with xhr.statusText if present', function() {
callback.andCallFake(function(status, response, headers, statusText) {
expect(statusText).toBe('OK');
@@ -235,7 +252,7 @@ describe('$httpBackend', function() {
it('should read responseText if response was not defined', function() {
- // old browsers like IE8, don't support responseType, so they always respond with responseText
+ // old browsers like IE9, don't support responseType, so they always respond with responseText
$backend('GET', '/whatever', null, callback, {}, null, null, 'blob');
diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js
index e3096edbb1b4..70e0d71cbae3 100644
--- a/test/ng/httpSpec.js
+++ b/test/ng/httpSpec.js
@@ -2,12 +2,24 @@
describe('$http', function() {
- var callback;
+ var callback, mockedCookies;
+ var customParamSerializer = function(params) {
+ return Object.keys(params).join('_');
+ };
beforeEach(function() {
callback = jasmine.createSpy('done');
+ mockedCookies = {};
+ module({
+ $$cookieReader: function() {
+ return mockedCookies;
+ }
+ });
});
+ beforeEach(module({
+ customParamSerializer: customParamSerializer
+ }));
beforeEach(module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
}));
@@ -218,13 +230,16 @@ describe('$http', function() {
});
});
inject(function($http, $httpBackend, $rootScope) {
- var config = { method: 'get', url: '/url', headers: { foo: 'bar'} };
+ var config = { headers: { foo: 'bar'} };
+ var configCopy = angular.copy(config);
$httpBackend.expect('GET', '/intercepted').respond('');
- $http.get('/url');
+ $http.get('/url', config);
+ $rootScope.$apply();
+ expect(config).toEqual(configCopy);
+ $httpBackend.expect('POST', '/intercepted').respond('');
+ $http.post('/url', {bar: 'baz'}, config);
$rootScope.$apply();
- expect(config.method).toEqual('get');
- expect(config.url).toEqual('/url');
- expect(config.headers.foo).toEqual('bar');
+ expect(config).toEqual(configCopy);
});
});
@@ -348,6 +363,20 @@ describe('$http', function() {
$httpBackend.expect('GET', '/url?date=2014-07-15T17:30:00.000Z').respond('');
$http({url: '/url', params: {date:new Date('2014-07-15T17:30:00.000Z')}, method: 'GET'});
});
+
+
+ describe('custom params serialization', function() {
+
+ it('should allow specifying custom paramSerializer as function', function() {
+ $httpBackend.expect('GET', '/url?foo_bar').respond('');
+ $http({url: '/url', params: {foo: 'fooVal', bar: 'barVal'}, paramSerializer: customParamSerializer});
+ });
+
+ it('should allow specifying custom paramSerializer as function from DI', function() {
+ $httpBackend.expect('GET', '/url?foo_bar').respond('');
+ $http({url: '/url', params: {foo: 'fooVal', bar: 'barVal'}, paramSerializer: 'customParamSerializer'});
+ });
+ });
});
@@ -428,6 +457,34 @@ describe('$http', function() {
var httpPromise = $http({url: '/url', method: 'GET'});
expect(httpPromise.success(callback)).toBe(httpPromise);
});
+
+
+ it('should error if the callback is not a function', function() {
+ expect(function() {
+ $http({url: '/url', method: 'GET'}).success();
+ }).toThrowMinErr('ng', 'areq');
+
+ expect(function() {
+ $http({url: '/url', method: 'GET'}).success(undefined);
+ }).toThrowMinErr('ng', 'areq');
+
+ expect(function() {
+ $http({url: '/url', method: 'GET'}).success(null);
+ }).toThrowMinErr('ng', 'areq');
+
+
+ expect(function() {
+ $http({url: '/url', method: 'GET'}).success({});
+ }).toThrowMinErr('ng', 'areq');
+
+ expect(function() {
+ $http({url: '/url', method: 'GET'}).success([]);
+ }).toThrowMinErr('ng', 'areq');
+
+ expect(function() {
+ $http({url: '/url', method: 'GET'}).success('error');
+ }).toThrowMinErr('ng', 'areq');
+ });
});
@@ -452,6 +509,34 @@ describe('$http', function() {
var httpPromise = $http({url: '/url', method: 'GET'});
expect(httpPromise.error(callback)).toBe(httpPromise);
});
+
+
+ it('should error if the callback is not a function', function() {
+ expect(function() {
+ $http({url: '/url', method: 'GET'}).error();
+ }).toThrowMinErr('ng', 'areq');
+
+ expect(function() {
+ $http({url: '/url', method: 'GET'}).error(undefined);
+ }).toThrowMinErr('ng', 'areq');
+
+ expect(function() {
+ $http({url: '/url', method: 'GET'}).error(null);
+ }).toThrowMinErr('ng', 'areq');
+
+
+ expect(function() {
+ $http({url: '/url', method: 'GET'}).error({});
+ }).toThrowMinErr('ng', 'areq');
+
+ expect(function() {
+ $http({url: '/url', method: 'GET'}).error([]);
+ }).toThrowMinErr('ng', 'areq');
+
+ expect(function() {
+ $http({url: '/url', method: 'GET'}).error('error');
+ }).toThrowMinErr('ng', 'areq');
+ });
});
});
@@ -691,7 +776,7 @@ describe('$http', function() {
});
it('should not set XSRF cookie for cross-domain requests', inject(function($browser) {
- $browser.cookies('XSRF-TOKEN', 'secret');
+ mockedCookies['XSRF-TOKEN'] = 'secret';
$browser.url('http://host.com/base');
$httpBackend.expect('GET', 'http://www.test.com/url', undefined, function(headers) {
return headers['X-XSRF-TOKEN'] === undefined;
@@ -733,15 +818,15 @@ describe('$http', function() {
$httpBackend.flush();
});
- it('should set the XSRF cookie into a XSRF header', inject(function($browser) {
+ it('should set the XSRF cookie into a XSRF header', inject(function() {
function checkXSRF(secret, header) {
return function(headers) {
return headers[header || 'X-XSRF-TOKEN'] == secret;
};
}
- $browser.cookies('XSRF-TOKEN', 'secret');
- $browser.cookies('aCookie', 'secret2');
+ mockedCookies['XSRF-TOKEN'] = 'secret';
+ mockedCookies['aCookie'] = 'secret2';
$httpBackend.expect('GET', '/url', undefined, checkXSRF('secret')).respond('');
$httpBackend.expect('POST', '/url', undefined, checkXSRF('secret')).respond('');
$httpBackend.expect('PUT', '/url', undefined, checkXSRF('secret')).respond('');
@@ -809,23 +894,18 @@ describe('$http', function() {
expect(config.foo).toBeUndefined();
});
- it('should check the cache before checking the XSRF cookie', inject(function($browser, $cacheFactory) {
- var testCache = $cacheFactory('testCache'),
- executionOrder = [];
+ it('should check the cache before checking the XSRF cookie', inject(function($cacheFactory) {
+ var testCache = $cacheFactory('testCache');
- spyOn($browser, 'cookies').andCallFake(function() {
- executionOrder.push('cookies');
- return {'XSRF-TOKEN':'foo'};
- });
spyOn(testCache, 'get').andCallFake(function() {
- executionOrder.push('cache');
+ mockedCookies['XSRF-TOKEN'] = 'foo';
});
- $httpBackend.expect('GET', '/url', undefined).respond('');
+ $httpBackend.expect('GET', '/url', undefined, function(headers) {
+ return headers['X-XSRF-TOKEN'] === 'foo';
+ }).respond('');
$http({url: '/url', method: 'GET', cache: testCache});
$httpBackend.flush();
-
- expect(executionOrder).toEqual(['cache', 'cookies']);
}));
});
@@ -1038,6 +1118,35 @@ describe('$http', function() {
expect(callback).toHaveBeenCalledOnce();
});
+ it('should have access to request headers with mixed case', function() {
+ $httpBackend.expect('POST', '/url', 'header1').respond(200);
+ $http.post('/url', 'req', {
+ headers: {H1: 'header1'},
+ transformRequest: function(data, headers) {
+ return headers('H1');
+ }
+ }).success(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+ it('should not allow modifications to headers in a transform functions', function() {
+ var config = {
+ headers: {'Accept': 'bar'},
+ transformRequest: function(data, headers) {
+ angular.extend(headers(), {
+ 'Accept': 'foo'
+ });
+ }
+ };
+
+ $httpBackend.expect('GET', '/url', undefined, {Accept: 'bar'}).respond(200);
+ $http.get('/url', config).success(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ });
it('should pipeline more functions', function() {
function first(d, h) {return d + '-first' + ':' + h('h1');}
@@ -1719,11 +1828,16 @@ describe('$http', function() {
$httpBackend.flush();
});
- it('should have separate opbjects for defaults PUT and POST', function() {
+ it('should have separate objects for defaults PUT and POST', function() {
expect($http.defaults.headers.post).not.toBe($http.defaults.headers.put);
expect($http.defaults.headers.post).not.toBe($http.defaults.headers.patch);
expect($http.defaults.headers.put).not.toBe($http.defaults.headers.patch);
});
+
+ it('should expose default param serializer at runtime', function() {
+ var paramSerializer = $http.defaults.paramSerializer;
+ expect(paramSerializer({foo: 'foo', bar: ['bar', 'baz']})).toEqual('bar=bar&bar=baz&foo=foo');
+ });
});
});
@@ -1860,3 +1974,93 @@ describe('$http with $applyAsync', function() {
expect(log).toEqual(['response 1', 'response 2', 'response 3']);
});
});
+
+describe('$http without useLegacyPromiseExtensions', function() {
+ var $httpBackend, $http;
+ beforeEach(module(function($httpProvider) {
+ $httpProvider.useLegacyPromiseExtensions(false);
+ }, provideLog));
+
+ beforeEach(inject(['$httpBackend', '$http', '$rootScope', function($hb, $h, $rs) {
+ $httpBackend = $hb;
+ $http = $h;
+ }]));
+
+ it('should throw when the success or error methods are called if useLegacyPromiseExtensions is false', function() {
+ $httpBackend.expect('GET', '/url').respond('');
+ var promise = $http({url: '/url'});
+
+ function callSucess() {
+ promise.success();
+ }
+
+ function callError() {
+ promise.error();
+ }
+
+ expect(callSucess).toThrowMinErr(
+ '$http', 'legacy', 'The method `success` on the promise returned from `$http` has been disabled.');
+ expect(callError).toThrowMinErr(
+ '$http', 'legacy', 'The method `error` on the promise returned from `$http` has been disabled.');
+ });
+});
+
+describe('$http param serializers', function() {
+
+ var defSer, jqrSer;
+ beforeEach(inject(function($httpParamSerializer, $httpParamSerializerJQLike) {
+ defSer = $httpParamSerializer;
+ jqrSer = $httpParamSerializerJQLike;
+ }));
+
+ describe('common functionality', function() {
+
+ it('should return empty string for null or undefined params', function() {
+ expect(defSer(undefined)).toEqual('');
+ expect(jqrSer(undefined)).toEqual('');
+ expect(defSer(null)).toEqual('');
+ expect(jqrSer(null)).toEqual('');
+ });
+
+ it('should serialize objects', function() {
+ expect(defSer({foo: 'foov', bar: 'barv'})).toEqual('bar=barv&foo=foov');
+ expect(jqrSer({foo: 'foov', bar: 'barv'})).toEqual('bar=barv&foo=foov');
+ expect(defSer({someDate: new Date('2014-07-15T17:30:00.000Z')})).toEqual('someDate=2014-07-15T17:30:00.000Z');
+ expect(jqrSer({someDate: new Date('2014-07-15T17:30:00.000Z')})).toEqual('someDate=2014-07-15T17:30:00.000Z');
+ });
+
+ });
+
+ describe('default array serialization', function() {
+
+ it('should serialize arrays by repeating param name', function() {
+ expect(defSer({a: 'b', foo: ['bar', 'baz']})).toEqual('a=b&foo=bar&foo=baz');
+ });
+ });
+
+ describe('jquery array and objects serialization', function() {
+
+ it('should serialize arrays by repeating param name with [] suffix', function() {
+ expect(jqrSer({a: 'b', foo: ['bar', 'baz']})).toEqual('a=b&foo%5B%5D=bar&foo%5B%5D=baz');
+ expect(decodeURIComponent(jqrSer({a: 'b', foo: ['bar', 'baz']}))).toEqual('a=b&foo[]=bar&foo[]=baz');
+ });
+
+ it('should serialize objects by repeating param name with [key] suffix', function() {
+ expect(jqrSer({a: 'b', foo: {'bar': 'barv', 'baz': 'bazv'}})).toEqual('a=b&foo%5Bbar%5D=barv&foo%5Bbaz%5D=bazv');
+ //a=b&foo[bar]=barv&foo[baz]=bazv
+ });
+
+ it('should serialize nested objects by repeating param name with [key] suffix', function() {
+ expect(jqrSer({a: ['b', {c: 'd'}], e: {f: 'g', 'h': ['i', 'j']}})).toEqual(
+ 'a%5B%5D=b&a%5B1%5D%5Bc%5D=d&e%5Bf%5D=g&e%5Bh%5D%5B%5D=i&e%5Bh%5D%5B%5D=j');
+ //a[]=b&a[1][c]=d&e[f]=g&e[h][]=i&e[h][]=j
+ });
+
+ it('should serialize objects inside array elements using their index', function() {
+ expect(jqrSer({a: ['b', 'c'], d: [{e: 'f', g: 'h'}, 'i', {j: 'k'}]})).toEqual(
+ 'a%5B%5D=b&a%5B%5D=c&d%5B0%5D%5Be%5D=f&d%5B0%5D%5Bg%5D=h&d%5B%5D=i&d%5B2%5D%5Bj%5D=k');
+ //a[]=b&a[]=c&d[0][e]=f&d[0][g]=h&d[]=i&d[2][j]=k
+ });
+ });
+
+});
diff --git a/test/ng/interpolateSpec.js b/test/ng/interpolateSpec.js
index 232244ede492..dfab860b1e3b 100644
--- a/test/ng/interpolateSpec.js
+++ b/test/ng/interpolateSpec.js
@@ -125,6 +125,28 @@ describe('$interpolate', function() {
expect($rootScope.$countWatchers()).toBe(0);
}));
+
+ it('should stop watching strings with no expressions after first execution',
+ inject(function($interpolate, $rootScope) {
+ var spy = jasmine.createSpy();
+ $rootScope.$watch($interpolate('foo'), spy);
+ $rootScope.$digest();
+ expect($rootScope.$countWatchers()).toBe(0);
+ expect(spy).toHaveBeenCalledWith('foo', 'foo', $rootScope);
+ expect(spy.calls.length).toBe(1);
+ })
+ );
+
+ it('should stop watching strings with only constant expressions after first execution',
+ inject(function($interpolate, $rootScope) {
+ var spy = jasmine.createSpy();
+ $rootScope.$watch($interpolate('foo {{42}}'), spy);
+ $rootScope.$digest();
+ expect($rootScope.$countWatchers()).toBe(0);
+ expect(spy).toHaveBeenCalledWith('foo 42', 'foo 42', $rootScope);
+ expect(spy.calls.length).toBe(1);
+ })
+ );
});
describe('interpolation escaping', function() {
diff --git a/test/ng/intervalSpec.js b/test/ng/intervalSpec.js
index 41cddded3307..2bc2de321a18 100644
--- a/test/ng/intervalSpec.js
+++ b/test/ng/intervalSpec.js
@@ -142,6 +142,31 @@ describe('$interval', function() {
}));
+ it('should allow you to specify a number of arguments', inject(function($interval, $window) {
+ var task1 = jasmine.createSpy('task1'),
+ task2 = jasmine.createSpy('task2'),
+ task3 = jasmine.createSpy('task3');
+ $interval(task1, 1000, 2, true, 'Task1');
+ $interval(task2, 1000, 2, true, 'Task2');
+ $interval(task3, 1000, 2, true, 'I', 'am', 'a', 'Task3', 'spy');
+
+ $window.flush(1000);
+ expect(task1).toHaveBeenCalledWith('Task1');
+ expect(task2).toHaveBeenCalledWith('Task2');
+ expect(task3).toHaveBeenCalledWith('I', 'am', 'a', 'Task3', 'spy');
+
+ task1.reset();
+ task2.reset();
+ task3.reset();
+
+ $window.flush(1000);
+ expect(task1).toHaveBeenCalledWith('Task1');
+ expect(task2).toHaveBeenCalledWith('Task2');
+ expect(task3).toHaveBeenCalledWith('I', 'am', 'a', 'Task3', 'spy');
+
+ }));
+
+
it('should return a promise which will be updated with the count on each iteration',
inject(function($interval, $window) {
var log = [],
diff --git a/test/ng/localeSpec.js b/test/ng/localeSpec.js
index 1da9cf611062..a811de51fbad 100644
--- a/test/ng/localeSpec.js
+++ b/test/ng/localeSpec.js
@@ -3,7 +3,10 @@
describe('$locale', function() {
/* global $LocaleProvider: false */
- var $locale = new $LocaleProvider().$get();
+ var $locale;
+ beforeEach(inject(function(_$locale_) {
+ $locale = _$locale_;
+ }));
it('should have locale id set to en-us', function() {
expect($locale.id).toBe('en-us');
diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js
index de0561d0c16f..1efe3396762f 100644
--- a/test/ng/locationSpec.js
+++ b/test/ng/locationSpec.js
@@ -2,8 +2,8 @@
'use strict';
describe('$location', function() {
- var url;
+ // Mock out the $log function - see testabilityPatch.js
beforeEach(module(provideLog));
afterEach(function() {
@@ -46,325 +46,303 @@ describe('$location', function() {
it('should not include the drive name in path() on WIN', function() {
//See issue #4680 for details
- url = new LocationHashbangUrl('file:///base', '#!');
- url.$$parse('file:///base#!/foo?a=b&c#hash');
+ var locationUrl = new LocationHashbangUrl('file:///base', 'file:///', '#!');
+ locationUrl.$$parse('file:///base#!/foo?a=b&c#hash');
- expect(url.path()).toBe('/foo');
+ expect(locationUrl.path()).toBe('/foo');
});
it('should include the drive name if it was provided in the input url', function() {
- url = new LocationHashbangUrl('file:///base', '#!');
- url.$$parse('file:///base#!/C:/foo?a=b&c#hash');
+ var locationUrl = new LocationHashbangUrl('file:///base', 'file:///', '#!');
+ locationUrl.$$parse('file:///base#!/C:/foo?a=b&c#hash');
- expect(url.path()).toBe('/C:/foo');
+ expect(locationUrl.path()).toBe('/C:/foo');
});
});
- it('should not infinitely digest when using a semicolon in initial path', function() {
- module(function($windowProvider, $locationProvider, $browserProvider, $documentProvider) {
- $locationProvider.html5Mode(true);
- $windowProvider.$get = function() {
- var win = {};
- angular.extend(win, window);
- win.addEventListener = angular.noop;
- win.removeEventListener = angular.noop;
- win.history = {
- replaceState: angular.noop,
- pushState: angular.noop
- };
- win.location = {
- href: 'http://localhost:9876/;jsessionid=foo',
- replace: function(val) {
- win.location.href = val;
- }
- };
- return win;
- };
- var baseElement = jqLite(' ');
- $documentProvider.$get = function() {
- return {
- 0: window.document,
- find: jasmine.createSpy('find').andReturn(baseElement)
- };
- };
- $browserProvider.$get = function($document, $window) {
- var sniffer = {history: true};
- var logs = {log:[], warn:[], info:[], error:[]};
- var fakeLog = {log: function() { logs.log.push(slice.call(arguments)); },
- warn: function() { logs.warn.push(slice.call(arguments)); },
- info: function() { logs.info.push(slice.call(arguments)); },
- error: function() { logs.error.push(slice.call(arguments)); }};
-
- /* global Browser: false */
- var b = new Browser($window, $document, fakeLog, sniffer);
- b.pollFns = [];
- return b;
- };
- });
- var self = this;
- inject(function($location, $browser, $rootScope) {
- expect(function() {
- $rootScope.$digest();
- }).not.toThrow();
- });
- });
-
describe('NewUrl', function() {
- beforeEach(function() {
- url = new LocationHtml5Url('http://www.domain.com:9877/');
- url.$$parse('http://www.domain.com:9877/path/b?search=a&b=c&d#hash');
- });
-
+ function createLocationHtml5Url() {
+ var locationUrl = new LocationHtml5Url('http://www.domain.com:9877/', 'http://www.domain.com:9877/');
+ locationUrl.$$parse('http://www.domain.com:9877/path/b?search=a&b=c&d#hash');
+ return locationUrl;
+ }
it('should provide common getters', function() {
- expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#hash');
- expect(url.protocol()).toBe('http');
- expect(url.host()).toBe('www.domain.com');
- expect(url.port()).toBe(9877);
- expect(url.path()).toBe('/path/b');
- expect(url.search()).toEqual({search: 'a', b: 'c', d: true});
- expect(url.hash()).toBe('hash');
- expect(url.url()).toBe('/path/b?search=a&b=c&d#hash');
+ var locationUrl = createLocationHtml5Url();
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#hash');
+ expect(locationUrl.protocol()).toBe('http');
+ expect(locationUrl.host()).toBe('www.domain.com');
+ expect(locationUrl.port()).toBe(9877);
+ expect(locationUrl.path()).toBe('/path/b');
+ expect(locationUrl.search()).toEqual({search: 'a', b: 'c', d: true});
+ expect(locationUrl.hash()).toBe('hash');
+ expect(locationUrl.url()).toBe('/path/b?search=a&b=c&d#hash');
});
it('path() should change path', function() {
- url.path('/new/path');
- expect(url.path()).toBe('/new/path');
- expect(url.absUrl()).toBe('http://www.domain.com:9877/new/path?search=a&b=c&d#hash');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.path('/new/path');
+ expect(locationUrl.path()).toBe('/new/path');
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/new/path?search=a&b=c&d#hash');
});
it('path() should not break on numeric values', function() {
- url.path(1);
- expect(url.path()).toBe('/1');
- expect(url.absUrl()).toBe('http://www.domain.com:9877/1?search=a&b=c&d#hash');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.path(1);
+ expect(locationUrl.path()).toBe('/1');
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/1?search=a&b=c&d#hash');
});
it('path() should allow using 0 as path', function() {
- url.path(0);
- expect(url.path()).toBe('/0');
- expect(url.absUrl()).toBe('http://www.domain.com:9877/0?search=a&b=c&d#hash');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.path(0);
+ expect(locationUrl.path()).toBe('/0');
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/0?search=a&b=c&d#hash');
});
it('path() should set to empty path on null value', function() {
- url.path('/foo');
- expect(url.path()).toBe('/foo');
- url.path(null);
- expect(url.path()).toBe('/');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.path('/foo');
+ expect(locationUrl.path()).toBe('/foo');
+ locationUrl.path(null);
+ expect(locationUrl.path()).toBe('/');
});
it('search() should accept string', function() {
- url.search('x=y&c');
- expect(url.search()).toEqual({x: 'y', c: true});
- expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?x=y&c#hash');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.search('x=y&c');
+ expect(locationUrl.search()).toEqual({x: 'y', c: true});
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/path/b?x=y&c#hash');
});
it('search() should accept object', function() {
- url.search({one: 1, two: true});
- expect(url.search()).toEqual({one: 1, two: true});
- expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?one=1&two#hash');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.search({one: 1, two: true});
+ expect(locationUrl.search()).toEqual({one: 1, two: true});
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/path/b?one=1&two#hash');
});
it('search() should copy object', function() {
+ var locationUrl = createLocationHtml5Url();
var obj = {one: 1, two: true, three: null};
- url.search(obj);
+ locationUrl.search(obj);
expect(obj).toEqual({one: 1, two: true, three: null});
obj.one = 'changed';
- expect(url.search()).toEqual({one: 1, two: true});
- expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?one=1&two#hash');
+ expect(locationUrl.search()).toEqual({one: 1, two: true});
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/path/b?one=1&two#hash');
});
it('search() should change single parameter', function() {
- url.search({id: 'old', preserved: true});
- url.search('id', 'new');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.search({id: 'old', preserved: true});
+ locationUrl.search('id', 'new');
- expect(url.search()).toEqual({id: 'new', preserved: true});
+ expect(locationUrl.search()).toEqual({id: 'new', preserved: true});
});
it('search() should remove single parameter', function() {
- url.search({id: 'old', preserved: true});
- url.search('id', null);
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.search({id: 'old', preserved: true});
+ locationUrl.search('id', null);
- expect(url.search()).toEqual({preserved: true});
+ expect(locationUrl.search()).toEqual({preserved: true});
});
it('search() should remove multiple parameters', function() {
- url.search({one: 1, two: true});
- expect(url.search()).toEqual({one: 1, two: true});
- url.search({one: null, two: null});
- expect(url.search()).toEqual({});
- expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b#hash');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.search({one: 1, two: true});
+ expect(locationUrl.search()).toEqual({one: 1, two: true});
+ locationUrl.search({one: null, two: null});
+ expect(locationUrl.search()).toEqual({});
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/path/b#hash');
});
it('search() should accept numeric keys', function() {
- url.search({1: 'one', 2: 'two'});
- expect(url.search()).toEqual({'1': 'one', '2': 'two'});
- expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?1=one&2=two#hash');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.search({1: 'one', 2: 'two'});
+ expect(locationUrl.search()).toEqual({'1': 'one', '2': 'two'});
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/path/b?1=one&2=two#hash');
});
it('search() should handle multiple value', function() {
- url.search('a&b');
- expect(url.search()).toEqual({a: true, b: true});
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.search('a&b');
+ expect(locationUrl.search()).toEqual({a: true, b: true});
- url.search('a', null);
+ locationUrl.search('a', null);
- expect(url.search()).toEqual({b: true});
+ expect(locationUrl.search()).toEqual({b: true});
- url.search('b', undefined);
- expect(url.search()).toEqual({});
+ locationUrl.search('b', undefined);
+ expect(locationUrl.search()).toEqual({});
});
it('search() should handle single value', function() {
- url.search('ignore');
- expect(url.search()).toEqual({ignore: true});
- url.search(1);
- expect(url.search()).toEqual({1: true});
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.search('ignore');
+ expect(locationUrl.search()).toEqual({ignore: true});
+ locationUrl.search(1);
+ expect(locationUrl.search()).toEqual({1: true});
});
it('search() should throw error an incorrect argument', function() {
+ var locationUrl = createLocationHtml5Url();
expect(function() {
- url.search(null);
+ locationUrl.search(null);
}).toThrowMinErr('$location', 'isrcharg', 'The first argument of the `$location#search()` call must be a string or an object.');
expect(function() {
- url.search(undefined);
+ locationUrl.search(undefined);
}).toThrowMinErr('$location', 'isrcharg', 'The first argument of the `$location#search()` call must be a string or an object.');
});
it('hash() should change hash fragment', function() {
- url.hash('new-hash');
- expect(url.hash()).toBe('new-hash');
- expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#new-hash');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.hash('new-hash');
+ expect(locationUrl.hash()).toBe('new-hash');
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#new-hash');
});
it('hash() should accept numeric parameter', function() {
- url.hash(5);
- expect(url.hash()).toBe('5');
- expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#5');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.hash(5);
+ expect(locationUrl.hash()).toBe('5');
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#5');
});
it('hash() should allow using 0', function() {
- url.hash(0);
- expect(url.hash()).toBe('0');
- expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#0');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.hash(0);
+ expect(locationUrl.hash()).toBe('0');
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#0');
});
it('hash() should accept null parameter', function() {
- url.hash(null);
- expect(url.hash()).toBe('');
- expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.hash(null);
+ expect(locationUrl.hash()).toBe('');
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d');
});
it('url() should change the path, search and hash', function() {
- url.url('/some/path?a=b&c=d#hhh');
- expect(url.url()).toBe('/some/path?a=b&c=d#hhh');
- expect(url.absUrl()).toBe('http://www.domain.com:9877/some/path?a=b&c=d#hhh');
- expect(url.path()).toBe('/some/path');
- expect(url.search()).toEqual({a: 'b', c: 'd'});
- expect(url.hash()).toBe('hhh');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.url('/some/path?a=b&c=d#hhh');
+ expect(locationUrl.url()).toBe('/some/path?a=b&c=d#hhh');
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/some/path?a=b&c=d#hhh');
+ expect(locationUrl.path()).toBe('/some/path');
+ expect(locationUrl.search()).toEqual({a: 'b', c: 'd'});
+ expect(locationUrl.hash()).toBe('hhh');
});
it('url() should change only hash when no search and path specified', function() {
- url.url('#some-hash');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.url('#some-hash');
- expect(url.hash()).toBe('some-hash');
- expect(url.url()).toBe('/path/b?search=a&b=c&d#some-hash');
- expect(url.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#some-hash');
+ expect(locationUrl.hash()).toBe('some-hash');
+ expect(locationUrl.url()).toBe('/path/b?search=a&b=c&d#some-hash');
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/path/b?search=a&b=c&d#some-hash');
});
it('url() should change only search and hash when no path specified', function() {
- url.url('?a=b');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.url('?a=b');
- expect(url.search()).toEqual({a: 'b'});
- expect(url.hash()).toBe('');
- expect(url.path()).toBe('/path/b');
+ expect(locationUrl.search()).toEqual({a: 'b'});
+ expect(locationUrl.hash()).toBe('');
+ expect(locationUrl.path()).toBe('/path/b');
});
it('url() should reset search and hash when only path specified', function() {
- url.url('/new/path');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.url('/new/path');
- expect(url.path()).toBe('/new/path');
- expect(url.search()).toEqual({});
- expect(url.hash()).toBe('');
+ expect(locationUrl.path()).toBe('/new/path');
+ expect(locationUrl.search()).toEqual({});
+ expect(locationUrl.hash()).toBe('');
});
it('url() should change path when empty string specified', function() {
- url.url('');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.url('');
- expect(url.path()).toBe('/');
- expect(url.search()).toEqual({});
- expect(url.hash()).toBe('');
+ expect(locationUrl.path()).toBe('/');
+ expect(locationUrl.search()).toEqual({});
+ expect(locationUrl.hash()).toBe('');
});
it('replace should set $$replace flag and return itself', function() {
- expect(url.$$replace).toBe(false);
+ var locationUrl = createLocationHtml5Url();
+ expect(locationUrl.$$replace).toBe(false);
- url.replace();
- expect(url.$$replace).toBe(true);
- expect(url.replace()).toBe(url);
+ locationUrl.replace();
+ expect(locationUrl.$$replace).toBe(true);
+ expect(locationUrl.replace()).toBe(locationUrl);
});
it('should parse new url', function() {
- url = new LocationHtml5Url('http://host.com/');
- url.$$parse('http://host.com/base');
- expect(url.path()).toBe('/base');
+ var locationUrl = new LocationHtml5Url('http://host.com/', 'http://host.com/');
+ locationUrl.$$parse('http://host.com/base');
+ expect(locationUrl.path()).toBe('/base');
- url = new LocationHtml5Url('http://host.com/');
- url.$$parse('http://host.com/base#');
- expect(url.path()).toBe('/base');
+ locationUrl = new LocationHtml5Url('http://host.com/', 'http://host.com/');
+ locationUrl.$$parse('http://host.com/base#');
+ expect(locationUrl.path()).toBe('/base');
});
it('should prefix path with forward-slash', function() {
- url = new LocationHtml5Url('http://server/');
- url.path('b');
+ var locationUrl = new LocationHtml5Url('http://server/', 'http://server/') ;
+ locationUrl.path('b');
- expect(url.path()).toBe('/b');
- expect(url.absUrl()).toBe('http://server/b');
+ expect(locationUrl.path()).toBe('/b');
+ expect(locationUrl.absUrl()).toBe('http://server/b');
});
it('should set path to forward-slash when empty', function() {
- url = new LocationHtml5Url('http://server/');
- url.$$parse('http://server/');
- expect(url.path()).toBe('/');
- expect(url.absUrl()).toBe('http://server/');
+ var locationUrl = new LocationHtml5Url('http://server/', 'http://server/') ;
+ locationUrl.$$parse('http://server/');
+ expect(locationUrl.path()).toBe('/');
+ expect(locationUrl.absUrl()).toBe('http://server/');
});
it('setters should return Url object to allow chaining', function() {
- expect(url.path('/any')).toBe(url);
- expect(url.search('')).toBe(url);
- expect(url.hash('aaa')).toBe(url);
- expect(url.url('/some')).toBe(url);
+ var locationUrl = createLocationHtml5Url();
+ expect(locationUrl.path('/any')).toBe(locationUrl);
+ expect(locationUrl.search('')).toBe(locationUrl);
+ expect(locationUrl.hash('aaa')).toBe(locationUrl);
+ expect(locationUrl.url('/some')).toBe(locationUrl);
});
it('should not preserve old properties when parsing new url', function() {
- url.$$parse('http://www.domain.com:9877/a');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.$$parse('http://www.domain.com:9877/a');
- expect(url.path()).toBe('/a');
- expect(url.search()).toEqual({});
- expect(url.hash()).toBe('');
- expect(url.absUrl()).toBe('http://www.domain.com:9877/a');
+ expect(locationUrl.path()).toBe('/a');
+ expect(locationUrl.search()).toEqual({});
+ expect(locationUrl.hash()).toBe('');
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/a');
});
it('should not rewrite when hashbang url is not given', function() {
@@ -378,60 +356,64 @@ describe('$location', function() {
});
it('should prepend path with basePath', function() {
- url = new LocationHtml5Url('http://server/base/');
- url.$$parse('http://server/base/abc?a');
- expect(url.path()).toBe('/abc');
- expect(url.search()).toEqual({a: true});
+ var locationUrl = new LocationHtml5Url('http://server/base/', 'http://server/base/') ;
+ locationUrl.$$parse('http://server/base/abc?a');
+ expect(locationUrl.path()).toBe('/abc');
+ expect(locationUrl.search()).toEqual({a: true});
- url.path('/new/path');
- expect(url.absUrl()).toBe('http://server/base/new/path?a');
+ locationUrl.path('/new/path');
+ expect(locationUrl.absUrl()).toBe('http://server/base/new/path?a');
});
it('should throw error when invalid server url given', function() {
- url = new LocationHtml5Url('http://server.org/base/abc', '/base');
+ var locationUrl = new LocationHtml5Url('http://server.org/base/abc', 'http://server.org/base/', '/base');
expect(function() {
- url.$$parse('http://other.server.org/path#/path');
+ locationUrl.$$parse('http://other.server.org/path#/path');
}).toThrowMinErr('$location', 'ipthprfx', 'Invalid url "http://other.server.org/path#/path", missing path prefix "http://server.org/base/".');
});
it('should throw error when invalid base url given', function() {
- url = new LocationHtml5Url('http://server.org/base/abc', '/base');
+ var locationUrl = new LocationHtml5Url('http://server.org/base/abc', 'http://server.org/base/', '/base');
expect(function() {
- url.$$parse('http://server.org/path#/path');
+ locationUrl.$$parse('http://server.org/path#/path');
}).toThrowMinErr('$location', 'ipthprfx', 'Invalid url "http://server.org/path#/path", missing path prefix "http://server.org/base/".');
});
describe('state', function() {
it('should set $$state and return itself', function() {
- expect(url.$$state).toEqual(null);
+ var locationUrl = createLocationHtml5Url();
+ expect(locationUrl.$$state).toEqual(null);
- var returned = url.state({a: 2});
- expect(url.$$state).toEqual({a: 2});
- expect(returned).toBe(url);
+ var returned = locationUrl.state({a: 2});
+ expect(locationUrl.$$state).toEqual({a: 2});
+ expect(returned).toBe(locationUrl);
});
it('should set state', function() {
- url.state({a: 2});
- expect(url.state()).toEqual({a: 2});
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.state({a: 2});
+ expect(locationUrl.state()).toEqual({a: 2});
});
it('should allow to set both URL and state', function() {
- url.url('/foo').state({a: 2});
- expect(url.url()).toEqual('/foo');
- expect(url.state()).toEqual({a: 2});
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.url('/foo').state({a: 2});
+ expect(locationUrl.url()).toEqual('/foo');
+ expect(locationUrl.state()).toEqual({a: 2});
});
it('should allow to mix state and various URL functions', function() {
- url.path('/foo').hash('abcd').state({a: 2}).search('bar', 'baz');
- expect(url.path()).toEqual('/foo');
- expect(url.state()).toEqual({a: 2});
- expect(url.search() && url.search().bar).toBe('baz');
- expect(url.hash()).toEqual('abcd');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.path('/foo').hash('abcd').state({a: 2}).search('bar', 'baz');
+ expect(locationUrl.path()).toEqual('/foo');
+ expect(locationUrl.state()).toEqual({a: 2});
+ expect(locationUrl.search() && locationUrl.search().bar).toBe('baz');
+ expect(locationUrl.hash()).toEqual('abcd');
});
});
@@ -439,43 +421,46 @@ describe('$location', function() {
describe('encoding', function() {
it('should encode special characters', function() {
- url.path('/a <>#');
- url.search({'i j': '<>#'});
- url.hash('<>#');
-
- expect(url.path()).toBe('/a <>#');
- expect(url.search()).toEqual({'i j': '<>#'});
- expect(url.hash()).toBe('<>#');
- expect(url.absUrl()).toBe('http://www.domain.com:9877/a%20%3C%3E%23?i%20j=%3C%3E%23#%3C%3E%23');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.path('/a <>#');
+ locationUrl.search({'i j': '<>#'});
+ locationUrl.hash('<>#');
+
+ expect(locationUrl.path()).toBe('/a <>#');
+ expect(locationUrl.search()).toEqual({'i j': '<>#'});
+ expect(locationUrl.hash()).toBe('<>#');
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/a%20%3C%3E%23?i%20j=%3C%3E%23#%3C%3E%23');
});
it('should not encode !$:@', function() {
- url.path('/!$:@');
- url.search('');
- url.hash('!$:@');
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.path('/!$:@');
+ locationUrl.search('');
+ locationUrl.hash('!$:@');
- expect(url.absUrl()).toBe('http://www.domain.com:9877/!$:@#!$:@');
+ expect(locationUrl.absUrl()).toBe('http://www.domain.com:9877/!$:@#!$:@');
});
it('should decode special characters', function() {
- url = new LocationHtml5Url('http://host.com/');
- url.$$parse('http://host.com/a%20%3C%3E%23?i%20j=%3C%3E%23#x%20%3C%3E%23');
- expect(url.path()).toBe('/a <>#');
- expect(url.search()).toEqual({'i j': '<>#'});
- expect(url.hash()).toBe('x <>#');
+ var locationUrl = new LocationHtml5Url('http://host.com/', 'http://host.com/');
+ locationUrl.$$parse('http://host.com/a%20%3C%3E%23?i%20j=%3C%3E%23#x%20%3C%3E%23');
+ expect(locationUrl.path()).toBe('/a <>#');
+ expect(locationUrl.search()).toEqual({'i j': '<>#'});
+ expect(locationUrl.hash()).toBe('x <>#');
});
it('should decode pluses as spaces in urls', function() {
- url = new LocationHtml5Url('http://host.com/');
- url.$$parse('http://host.com/?a+b=c+d');
- expect(url.search()).toEqual({'a b':'c d'});
+ var locationUrl = new LocationHtml5Url('http://host.com/', 'http://host.com/');
+ locationUrl.$$parse('http://host.com/?a+b=c+d');
+ expect(locationUrl.search()).toEqual({'a b':'c d'});
});
it('should retain pluses when setting search queries', function() {
- url.search({'a+b':'c+d'});
- expect(url.search()).toEqual({'a+b':'c+d'});
+ var locationUrl = createLocationHtml5Url();
+ locationUrl.search({'a+b':'c+d'});
+ expect(locationUrl.search()).toEqual({'a+b':'c+d'});
});
});
@@ -484,159 +469,167 @@ describe('$location', function() {
describe('HashbangUrl', function() {
- beforeEach(function() {
- url = new LocationHashbangUrl('http://www.server.org:1234/base', '#!');
- url.$$parse('http://www.server.org:1234/base#!/path?a=b&c#hash');
- });
+ function createHashbangUrl() {
+ var locationUrl = new LocationHashbangUrl('http://www.server.org:1234/base', 'http://www.server.org:1234/', '#!');
+ locationUrl.$$parse('http://www.server.org:1234/base#!/path?a=b&c#hash');
+ return locationUrl;
+ }
it('should parse hashbang url into path and search', function() {
- expect(url.protocol()).toBe('http');
- expect(url.host()).toBe('www.server.org');
- expect(url.port()).toBe(1234);
- expect(url.path()).toBe('/path');
- expect(url.search()).toEqual({a: 'b', c: true});
- expect(url.hash()).toBe('hash');
+ var locationUrl = createHashbangUrl();
+ expect(locationUrl.protocol()).toBe('http');
+ expect(locationUrl.host()).toBe('www.server.org');
+ expect(locationUrl.port()).toBe(1234);
+ expect(locationUrl.path()).toBe('/path');
+ expect(locationUrl.search()).toEqual({a: 'b', c: true});
+ expect(locationUrl.hash()).toBe('hash');
});
it('absUrl() should return hashbang url', function() {
- expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/path?a=b&c#hash');
+ var locationUrl = createHashbangUrl();
+ expect(locationUrl.absUrl()).toBe('http://www.server.org:1234/base#!/path?a=b&c#hash');
- url.path('/new/path');
- url.search({one: 1});
- url.hash('hhh');
- expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/new/path?one=1#hhh');
+ locationUrl.path('/new/path');
+ locationUrl.search({one: 1});
+ locationUrl.hash('hhh');
+ expect(locationUrl.absUrl()).toBe('http://www.server.org:1234/base#!/new/path?one=1#hhh');
});
it('should preserve query params in base', function() {
- url = new LocationHashbangUrl('http://www.server.org:1234/base?base=param', '#');
- url.$$parse('http://www.server.org:1234/base?base=param#/path?a=b&c#hash');
- expect(url.absUrl()).toBe('http://www.server.org:1234/base?base=param#/path?a=b&c#hash');
+ var locationUrl = new LocationHashbangUrl('http://www.server.org:1234/base?base=param', 'http://www.server.org:1234/', '#');
+ locationUrl.$$parse('http://www.server.org:1234/base?base=param#/path?a=b&c#hash');
+ expect(locationUrl.absUrl()).toBe('http://www.server.org:1234/base?base=param#/path?a=b&c#hash');
- url.path('/new/path');
- url.search({one: 1});
- url.hash('hhh');
- expect(url.absUrl()).toBe('http://www.server.org:1234/base?base=param#/new/path?one=1#hhh');
+ locationUrl.path('/new/path');
+ locationUrl.search({one: 1});
+ locationUrl.hash('hhh');
+ expect(locationUrl.absUrl()).toBe('http://www.server.org:1234/base?base=param#/new/path?one=1#hhh');
});
it('should prefix path with forward-slash', function() {
- url = new LocationHashbangUrl('http://host.com/base', '#');
- url.$$parse('http://host.com/base#path');
- expect(url.path()).toBe('/path');
- expect(url.absUrl()).toBe('http://host.com/base#/path');
+ var locationUrl = new LocationHashbangUrl('http://host.com/base', 'http://host.com/', '#');
+ locationUrl.$$parse('http://host.com/base#path');
+ expect(locationUrl.path()).toBe('/path');
+ expect(locationUrl.absUrl()).toBe('http://host.com/base#/path');
- url.path('wrong');
- expect(url.path()).toBe('/wrong');
- expect(url.absUrl()).toBe('http://host.com/base#/wrong');
+ locationUrl.path('wrong');
+ expect(locationUrl.path()).toBe('/wrong');
+ expect(locationUrl.absUrl()).toBe('http://host.com/base#/wrong');
});
it('should set path to forward-slash when empty', function() {
- url = new LocationHashbangUrl('http://server/base', '#!');
- url.$$parse('http://server/base');
- url.path('aaa');
+ var locationUrl = new LocationHashbangUrl('http://server/base', 'http://server/', '#!');
+ locationUrl.$$parse('http://server/base');
+ locationUrl.path('aaa');
- expect(url.path()).toBe('/aaa');
- expect(url.absUrl()).toBe('http://server/base#!/aaa');
+ expect(locationUrl.path()).toBe('/aaa');
+ expect(locationUrl.absUrl()).toBe('http://server/base#!/aaa');
});
it('should not preserve old properties when parsing new url', function() {
- url.$$parse('http://www.server.org:1234/base#!/');
+ var locationUrl = createHashbangUrl();
+ locationUrl.$$parse('http://www.server.org:1234/base#!/');
- expect(url.path()).toBe('/');
- expect(url.search()).toEqual({});
- expect(url.hash()).toBe('');
- expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/');
+ expect(locationUrl.path()).toBe('/');
+ expect(locationUrl.search()).toEqual({});
+ expect(locationUrl.hash()).toBe('');
+ expect(locationUrl.absUrl()).toBe('http://www.server.org:1234/base#!/');
});
it('should insert default hashbang if a hash is given with no hashbang prefix', function() {
+ var locationUrl = createHashbangUrl();
- url.$$parse('http://www.server.org:1234/base#/path');
- expect(url.absUrl()).toBe('http://www.server.org:1234/base#!#%2Fpath');
- expect(url.hash()).toBe('/path');
- expect(url.path()).toBe('');
+ locationUrl.$$parse('http://www.server.org:1234/base#/path');
+ expect(locationUrl.absUrl()).toBe('http://www.server.org:1234/base#!#%2Fpath');
+ expect(locationUrl.hash()).toBe('/path');
+ expect(locationUrl.path()).toBe('');
- url.$$parse('http://www.server.org:1234/base#');
- expect(url.absUrl()).toBe('http://www.server.org:1234/base');
- expect(url.hash()).toBe('');
- expect(url.path()).toBe('');
+ locationUrl.$$parse('http://www.server.org:1234/base#');
+ expect(locationUrl.absUrl()).toBe('http://www.server.org:1234/base');
+ expect(locationUrl.hash()).toBe('');
+ expect(locationUrl.path()).toBe('');
});
it('should ignore extra path segments if no hashbang is given', function() {
- url.$$parse('http://www.server.org:1234/base/extra/path');
- expect(url.absUrl()).toBe('http://www.server.org:1234/base');
- expect(url.path()).toBe('');
- expect(url.hash()).toBe('');
+ var locationUrl = createHashbangUrl();
+ locationUrl.$$parse('http://www.server.org:1234/base/extra/path');
+ expect(locationUrl.absUrl()).toBe('http://www.server.org:1234/base');
+ expect(locationUrl.path()).toBe('');
+ expect(locationUrl.hash()).toBe('');
});
describe('encoding', function() {
it('should encode special characters', function() {
- url.path('/a <>#');
- url.search({'i j': '<>#'});
- url.hash('<>#');
-
- expect(url.path()).toBe('/a <>#');
- expect(url.search()).toEqual({'i j': '<>#'});
- expect(url.hash()).toBe('<>#');
- expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/a%20%3C%3E%23?i%20j=%3C%3E%23#%3C%3E%23');
+ var locationUrl = createHashbangUrl();
+ locationUrl.path('/a <>#');
+ locationUrl.search({'i j': '<>#'});
+ locationUrl.hash('<>#');
+
+ expect(locationUrl.path()).toBe('/a <>#');
+ expect(locationUrl.search()).toEqual({'i j': '<>#'});
+ expect(locationUrl.hash()).toBe('<>#');
+ expect(locationUrl.absUrl()).toBe('http://www.server.org:1234/base#!/a%20%3C%3E%23?i%20j=%3C%3E%23#%3C%3E%23');
});
it('should not encode !$:@', function() {
- url.path('/!$:@');
- url.search('');
- url.hash('!$:@');
+ var locationUrl = createHashbangUrl();
+ locationUrl.path('/!$:@');
+ locationUrl.search('');
+ locationUrl.hash('!$:@');
- expect(url.absUrl()).toBe('http://www.server.org:1234/base#!/!$:@#!$:@');
+ expect(locationUrl.absUrl()).toBe('http://www.server.org:1234/base#!/!$:@#!$:@');
});
it('should decode special characters', function() {
- url = new LocationHashbangUrl('http://host.com/a', '#');
- url.$$parse('http://host.com/a#/%20%3C%3E%23?i%20j=%3C%3E%23#x%20%3C%3E%23');
- expect(url.path()).toBe('/ <>#');
- expect(url.search()).toEqual({'i j': '<>#'});
- expect(url.hash()).toBe('x <>#');
+ var locationUrl = new LocationHashbangUrl('http://host.com/a', 'http://host.com/', '#');
+ locationUrl.$$parse('http://host.com/a#/%20%3C%3E%23?i%20j=%3C%3E%23#x%20%3C%3E%23');
+ expect(locationUrl.path()).toBe('/ <>#');
+ expect(locationUrl.search()).toEqual({'i j': '<>#'});
+ expect(locationUrl.hash()).toBe('x <>#');
});
it('should return decoded characters for search specified in URL', function() {
- var locationUrl = new LocationHtml5Url('http://host.com/');
+ var locationUrl = new LocationHtml5Url('http://host.com/', 'http://host.com/');
locationUrl.$$parse('http://host.com/?q=1%2F2%203');
expect(locationUrl.search()).toEqual({'q': '1/2 3'});
});
it('should return decoded characters for search specified with setter', function() {
- var locationUrl = new LocationHtml5Url('http://host.com/');
+ var locationUrl = new LocationHtml5Url('http://host.com/', 'http://host.com/');
locationUrl.$$parse('http://host.com/');
locationUrl.search('q', '1/2 3');
expect(locationUrl.search()).toEqual({'q': '1/2 3'});
});
it('should return an array for duplicate params', function() {
- var locationUrl = new LocationHtml5Url('http://host.com');
+ var locationUrl = new LocationHtml5Url('http://host.com', 'http://host.com') ;
locationUrl.$$parse('http://host.com');
locationUrl.search('q', ['1/2 3','4/5 6']);
expect(locationUrl.search()).toEqual({'q': ['1/2 3','4/5 6']});
});
it('should encode an array correctly from search and add to url', function() {
- var locationUrl = new LocationHtml5Url('http://host.com');
+ var locationUrl = new LocationHtml5Url('http://host.com', 'http://host.com') ;
locationUrl.$$parse('http://host.com');
locationUrl.search({'q': ['1/2 3','4/5 6']});
expect(locationUrl.absUrl()).toEqual('http://host.com?q=1%2F2%203&q=4%2F5%206');
});
it('should rewrite params when specifing a single param in search', function() {
- var locationUrl = new LocationHtml5Url('http://host.com');
+ var locationUrl = new LocationHtml5Url('http://host.com', 'http://host.com') ;
locationUrl.$$parse('http://host.com');
locationUrl.search({'q': '1/2 3'});
expect(locationUrl.absUrl()).toEqual('http://host.com?q=1%2F2%203');
@@ -647,183 +640,390 @@ describe('$location', function() {
});
- function initService(options) {
- return module(function($provide, $locationProvider) {
- $locationProvider.html5Mode(options.html5Mode);
- $locationProvider.hashPrefix(options.hashPrefix);
- $provide.value('$sniffer', {history: options.supportHistory});
+ describe('location watch', function() {
+
+ it('should not update browser if only the empty hash fragment is cleared by updating the search', function() {
+ initService({supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b#', baseHref:'/base/'});
+ inject(function($rootScope, $browser, $location) {
+ $browser.url('http://new.com/a/b');
+ var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
+ $rootScope.$digest();
+ expect($browserUrl).not.toHaveBeenCalled();
+ });
});
- }
- function initBrowser(options) {
- return function($browser) {
- $browser.url(options.url);
- $browser.$$baseHref = options.basePath;
- };
- }
- describe('location watch', function() {
- beforeEach(initService({supportHistory: true}));
- beforeEach(inject(initBrowser({url:'http://new.com/a/b#'})));
-
- it('should not update browser if only the empty hash fragment is cleared by updating the search', inject(function($rootScope, $browser, $location) {
- $browser.url('http://new.com/a/b#');
- var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
- $rootScope.$digest();
- expect($browserUrl).not.toHaveBeenCalled();
- }));
- });
- describe('rewrite hashbang url <> html5 url', function() {
- beforeEach(initService({html5Mode: true, supportHistory: true}));
- beforeEach(inject(initBrowser({url:'http://new.com/#', basePath: '/'})));
+ it('should not replace browser url if only the empty hash fragment is cleared', function() {
+ initService({html5Mode: true, supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/#', baseHref: '/'});
+ inject(function($browser, $location) {
+ expect($browser.url()).toBe('http://new.com/#');
+ expect($location.absUrl()).toBe('http://new.com/');
+ });
+ });
- it('should not replace browser url if only the empty hash fragment is cleared', inject(function($browser, $location) {
- expect($browser.url()).toBe('http://new.com/#');
- expect($location.absUrl()).toBe('http://new.com/');
- }));
- });
- describe('wiring', function() {
+ it('should not get caught in infinite digest when replacing path in locationChangeSuccess handler', function() {
+ initService({html5Mode:true,supportHistory:false});
+ mockUpBrowser({initialUrl:'http://server/base/home', baseHref:'/base/'});
+ inject(
+ function($browser, $location, $rootScope, $window) {
+ var handlerCalled = false;
+ $rootScope.$on('$locationChangeSuccess', function() {
+ handlerCalled = true;
+ if ($location.path() !== '/') {
+ $location.path('/').replace();
+ }
+ });
+ expect($browser.url()).toEqual('http://server/base/#/home');
+ $rootScope.$digest();
+ expect(handlerCalled).toEqual(true);
+ expect($browser.url()).toEqual('http://server/base/#/');
+ }
+ );
+ });
- beforeEach(initService({html5Mode:false,hashPrefix: '!',supportHistory: true}));
- beforeEach(inject(initBrowser({url:'http://new.com/a/b#!',basePath: 'http://new.com/a/b'})));
+ it('should not infinitely digest when using a semicolon in initial path', function() {
+ initService({html5Mode:true,supportHistory:true});
+ mockUpBrowser({initialUrl:'http://localhost:9876/;jsessionid=foo', baseHref:'/'});
+ inject(function($location, $browser, $rootScope) {
+ expect(function() {
+ $rootScope.$digest();
+ }).not.toThrow();
+ });
+ });
- it('should update $location when browser url changes', inject(function($browser, $location) {
- spyOn($location, '$$parse').andCallThrough();
- $browser.url('http://new.com/a/b#!/aaa');
- $browser.poll();
- expect($location.absUrl()).toBe('http://new.com/a/b#!/aaa');
- expect($location.path()).toBe('/aaa');
- expect($location.$$parse).toHaveBeenCalledOnce();
- }));
+ function updatePathOnLocationChangeSuccessTo(newPath) {
+ inject(function($rootScope, $location) {
+ $rootScope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl) {
+ $location.path(newPath);
+ });
+ });
+ }
- // location.href = '...' fires hashchange event synchronously, so it might happen inside $apply
- it('should not $apply when browser url changed inside $apply', inject(
- function($rootScope, $browser, $location) {
- var OLD_URL = $browser.url(),
- NEW_URL = 'http://new.com/a/b#!/new';
+ describe('location watch for hashbang browsers', function() {
+
+ it('should not infinite $digest when going to base URL without trailing slash when $locationChangeSuccess watcher changes path to /Home', function() {
+ initService({html5Mode: true, supportHistory: false});
+ mockUpBrowser({initialUrl:'http://server/app/', baseHref:'/app/'});
+ inject(function($rootScope, $location, $browser) {
+ var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
+
+ updatePathOnLocationChangeSuccessTo('/Home');
+ $rootScope.$digest();
- $rootScope.$apply(function() {
- $browser.url(NEW_URL);
- $browser.poll(); // simulate firing event from browser
- expect($location.absUrl()).toBe(OLD_URL); // should be async
+ expect($browser.url()).toEqual('http://server/app/#/Home');
+ expect($location.path()).toEqual('/Home');
+ expect($browserUrl.calls.length).toEqual(1);
+ });
});
- expect($location.absUrl()).toBe(NEW_URL);
- }));
+ it('should not infinite $digest when going to base URL without trailing slash when $locationChangeSuccess watcher changes path to /', function() {
+ initService({html5Mode: true, supportHistory: false});
+ mockUpBrowser({initialUrl:'http://server/app/Home', baseHref:'/app/'});
+ inject(function($rootScope, $location, $browser, $window) {
+ var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
- // location.href = '...' fires hashchange event synchronously, so it might happen inside $digest
- it('should not $apply when browser url changed inside $digest', inject(
- function($rootScope, $browser, $location) {
- var OLD_URL = $browser.url(),
- NEW_URL = 'http://new.com/a/b#!/new',
- notRunYet = true;
-
- $rootScope.$watch(function() {
- if (notRunYet) {
- notRunYet = false;
- $browser.url(NEW_URL);
- $browser.poll(); // simulate firing event from browser
+ updatePathOnLocationChangeSuccessTo('/');
+
+ $rootScope.$digest();
+
+ expect($browser.url()).toEqual('http://server/app/#/');
+ expect($location.path()).toEqual('/');
+ expect($browserUrl.calls.length).toEqual(1);
+ expect($browserUrl.calls[0].args).toEqual(['http://server/app/#/', false, null]);
+ });
+ });
+
+ it('should not infinite $digest when going to base URL with trailing slash when $locationChangeSuccess watcher changes path to /Home', function() {
+ initService({html5Mode: true, supportHistory: false});
+ mockUpBrowser({initialUrl:'http://server/app/', baseHref:'/app/'});
+ inject(function($rootScope, $location, $browser) {
+ var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
+
+ updatePathOnLocationChangeSuccessTo('/Home');
+ $rootScope.$digest();
+
+ expect($browser.url()).toEqual('http://server/app/#/Home');
+ expect($location.path()).toEqual('/Home');
+ expect($browserUrl.calls.length).toEqual(1);
+ expect($browserUrl.calls[0].args).toEqual(['http://server/app/#/Home', false, null]);
+ });
+ });
+
+ it('should not infinite $digest when going to base URL with trailing slash when $locationChangeSuccess watcher changes path to /', function() {
+ initService({html5Mode: true, supportHistory: false});
+ mockUpBrowser({initialUrl:'http://server/app/', baseHref:'/app/'});
+ inject(function($rootScope, $location, $browser) {
+ var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
+
+ updatePathOnLocationChangeSuccessTo('/');
+ $rootScope.$digest();
+
+ expect($browser.url()).toEqual('http://server/app/#/');
+ expect($location.path()).toEqual('/');
+ expect($browserUrl.calls.length).toEqual(1);
+ });
+ });
+ });
+
+
+ describe('location watch for HTML5 browsers', function() {
+
+ it('should not infinite $digest when going to base URL without trailing slash when $locationChangeSuccess watcher changes path to /Home', function() {
+ initService({html5Mode: true, supportHistory: true});
+ mockUpBrowser({initialUrl:'http://server/app/', baseHref:'/app/'});
+ inject(function($rootScope, $injector, $browser) {
+ var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
+
+ var $location = $injector.get('$location');
+ updatePathOnLocationChangeSuccessTo('/Home');
+
+ $rootScope.$digest();
+
+ expect($browser.url()).toEqual('http://server/app/Home');
+ expect($location.path()).toEqual('/Home');
+ expect($browserUrl.calls.length).toEqual(1);
+ });
+ });
+
+ it('should not infinite $digest when going to base URL without trailing slash when $locationChangeSuccess watcher changes path to /', function() {
+ initService({html5Mode: true, supportHistory: true});
+ mockUpBrowser({initialUrl:'http://server/app/', baseHref:'/app/'});
+ inject(function($rootScope, $injector, $browser) {
+ var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
+
+ var $location = $injector.get('$location');
+ updatePathOnLocationChangeSuccessTo('/');
+
+ $rootScope.$digest();
+
+ expect($browser.url()).toEqual('http://server/app/');
+ expect($location.path()).toEqual('/');
+ expect($browserUrl.calls.length).toEqual(0);
+ });
+ });
+
+ it('should not infinite $digest when going to base URL with trailing slash when $locationChangeSuccess watcher changes path to /Home', function() {
+ initService({html5Mode: true, supportHistory: true});
+ mockUpBrowser({initialUrl:'http://server/app/', baseHref:'/app/'});
+ inject(function($rootScope, $injector, $browser) {
+ var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
+
+ var $location = $injector.get('$location');
+ updatePathOnLocationChangeSuccessTo('/Home');
+
+ $rootScope.$digest();
+
+ expect($browser.url()).toEqual('http://server/app/Home');
+ expect($location.path()).toEqual('/Home');
+ expect($browserUrl.calls.length).toEqual(1);
+ });
+ });
+
+ it('should not infinite $digest when going to base URL with trailing slash when $locationChangeSuccess watcher changes path to /', function() {
+ initService({html5Mode: true, supportHistory: true});
+ mockUpBrowser({initialUrl:'http://server/app/', baseHref:'/app/'});
+ inject(function($rootScope, $injector, $browser) {
+ var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
+
+ var $location = $injector.get('$location');
+ updatePathOnLocationChangeSuccessTo('/');
+
+ $rootScope.$digest();
+
+ expect($browser.url()).toEqual('http://server/app/');
+ expect($location.path()).toEqual('/');
+ expect($browserUrl.calls.length).toEqual(0);
+ });
+ });
+ });
+
+ });
+
+ describe('wiring', function() {
+
+ it('should update $location when browser url changes', function() {
+ initService({html5Mode:false,hashPrefix: '!',supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b#!', baseHref:'/a/b'});
+ inject(function($window, $browser, $location, $rootScope) {
+ spyOn($location, '$$parse').andCallThrough();
+ $window.location.href = 'http://new.com/a/b#!/aaa';
+ $browser.$$checkUrlChange();
+ expect($location.absUrl()).toBe('http://new.com/a/b#!/aaa');
+ expect($location.path()).toBe('/aaa');
+ expect($location.$$parse).toHaveBeenCalledOnce();
+ });
+ });
+
+ // location.href = '...' fires hashchange event synchronously, so it might happen inside $apply
+ it('should not $apply when browser url changed inside $apply', function() {
+ initService({html5Mode:false,hashPrefix: '!',supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b#!', baseHref:'/a/b'});
+ inject(function($rootScope, $browser, $location, $window) {
+ var OLD_URL = $browser.url(),
+ NEW_URL = 'http://new.com/a/b#!/new';
+
+ $rootScope.$apply(function() {
+ $window.location.href= NEW_URL;
+ $browser.$$checkUrlChange(); // simulate firing event from browser
expect($location.absUrl()).toBe(OLD_URL); // should be async
- }
+ });
+
+ expect($location.absUrl()).toBe(NEW_URL);
});
+ });
- $rootScope.$digest();
- expect($location.absUrl()).toBe(NEW_URL);
- }));
+ // location.href = '...' fires hashchange event synchronously, so it might happen inside $digest
+ it('should not $apply when browser url changed inside $digest', function() {
+ initService({html5Mode:false,hashPrefix: '!',supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b#!', baseHref:'/a/b'});
+ inject(function($rootScope, $browser, $location, $window) {
+ var OLD_URL = $browser.url(),
+ NEW_URL = 'http://new.com/a/b#!/new',
+ notRunYet = true;
+
+ $rootScope.$watch(function() {
+ if (notRunYet) {
+ notRunYet = false;
+ $window.location.href = NEW_URL;
+ $browser.$$checkUrlChange(); // simulate firing event from browser
+ expect($location.absUrl()).toBe(OLD_URL); // should be async
+ }
+ });
+ $rootScope.$digest();
+ expect($location.absUrl()).toBe(NEW_URL);
+ });
+ });
- it('should update browser when $location changes', inject(function($rootScope, $browser, $location) {
- var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
- $location.path('/new/path');
- expect($browserUrl).not.toHaveBeenCalled();
- $rootScope.$apply();
- expect($browserUrl).toHaveBeenCalledOnce();
- expect($browser.url()).toBe('http://new.com/a/b#!/new/path');
- }));
+ it('should update browser when $location changes', function() {
+ initService({html5Mode:false,hashPrefix: '!',supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b#!', baseHref:'/a/b'});
+ inject(function($rootScope, $browser, $location) {
+ var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
+ $location.path('/new/path');
+ expect($browserUrl).not.toHaveBeenCalled();
+ $rootScope.$apply();
+ expect($browserUrl).toHaveBeenCalledOnce();
+ expect($browser.url()).toBe('http://new.com/a/b#!/new/path');
+ });
+ });
- it('should update browser only once per $apply cycle', inject(function($rootScope, $browser, $location) {
- var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
- $location.path('/new/path');
- $rootScope.$watch(function() {
- $location.search('a=b');
+ it('should update browser only once per $apply cycle', function() {
+ initService({html5Mode:false,hashPrefix: '!',supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b#!', baseHref:'/a/b'});
+ inject(function($rootScope, $browser, $location) {
+ var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
+ $location.path('/new/path');
+
+ $rootScope.$watch(function() {
+ $location.search('a=b');
+ });
+
+ $rootScope.$apply();
+ expect($browserUrl).toHaveBeenCalledOnce();
+ expect($browser.url()).toBe('http://new.com/a/b#!/new/path?a=b');
});
+ });
- $rootScope.$apply();
- expect($browserUrl).toHaveBeenCalledOnce();
- expect($browser.url()).toBe('http://new.com/a/b#!/new/path?a=b');
- }));
+ it('should replace browser url when url was replaced at least once', function() {
+ initService({html5Mode:false,hashPrefix: '!',supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b#!', baseHref:'/a/b'});
+ inject(function($rootScope, $browser, $location) {
+ var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
+ $location.path('/n/url').replace();
+ $rootScope.$apply();
- it('should replace browser url when url was replaced at least once',
- inject(function($rootScope, $location, $browser) {
- var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
- $location.path('/n/url').replace();
- $rootScope.$apply();
+ expect($browserUrl).toHaveBeenCalledOnce();
+ expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b#!/n/url', true, null]);
+ expect($location.$$replace).toBe(false);
+ });
+ });
- expect($browserUrl).toHaveBeenCalledOnce();
- expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b#!/n/url', true, null]);
- expect($location.$$replace).toBe(false);
- }));
+ it('should always reset replace flag after running watch', function() {
+ initService({html5Mode:false,hashPrefix: '!',supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b#!', baseHref:'/a/b'});
+ inject(function($rootScope, $browser, $location) {
+ // init watches
+ $location.url('/initUrl');
+ $rootScope.$apply();
- it('should always reset replace flag after running watch', inject(function($rootScope, $location) {
- // init watches
- $location.url('/initUrl');
- $rootScope.$apply();
+ // changes url but resets it before digest
+ $location.url('/newUrl').replace().url('/initUrl');
+ $rootScope.$apply();
+ expect($location.$$replace).toBe(false);
- // changes url but resets it before digest
- $location.url('/newUrl').replace().url('/initUrl');
- $rootScope.$apply();
- expect($location.$$replace).toBe(false);
+ // set the url to the old value
+ $location.url('/newUrl').replace();
+ $rootScope.$apply();
+ expect($location.$$replace).toBe(false);
- // set the url to the old value
- $location.url('/newUrl').replace();
- $rootScope.$apply();
- expect($location.$$replace).toBe(false);
+ // doesn't even change url only calls replace()
+ $location.replace();
+ $rootScope.$apply();
+ expect($location.$$replace).toBe(false);
+ });
+ });
- // doesn't even change url only calls replace()
- $location.replace();
- $rootScope.$apply();
- expect($location.$$replace).toBe(false);
- }));
+ it('should update the browser if changed from within a watcher', function() {
+ initService({html5Mode:false,hashPrefix: '!',supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b#!', baseHref:'/a/b'});
+ inject(function($rootScope, $browser, $location) {
+ $rootScope.$watch(function() { return true; }, function() {
+ $location.path('/changed');
+ });
- it('should update the browser if changed from within a watcher', inject(function($rootScope, $location, $browser) {
- $rootScope.$watch(function() { return true; }, function() {
- $location.path('/changed');
+ $rootScope.$digest();
+ expect($browser.url()).toBe('http://new.com/a/b#!/changed');
});
+ });
- $rootScope.$digest();
- expect($browser.url()).toBe('http://new.com/a/b#!/changed');
- }));
+
+ it('should not infinitely digest if hash is set when there is no hashPrefix', function() {
+ initService({html5Mode:false, hashPrefix:'', supportHistory:true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b', baseHref:'/a/b'});
+ inject(function($rootScope, $browser, $location) {
+ $location.hash('test');
+
+ $rootScope.$digest();
+ expect($browser.url()).toBe('http://new.com/a/b##test');
+ });
+ });
});
describe('wiring in html5 mode', function() {
- beforeEach(initService({html5Mode: true, supportHistory: true}));
- beforeEach(inject(initBrowser({url:'http://new.com/a/b/', basePath: '/a/b/'})));
-
- it('should initialize state to $browser.state()', inject(function($browser) {
- $browser.$$state = {a: 2};
+ it('should initialize state to initial state from the browser', function() {
+ initService({html5Mode:true, supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b/', baseHref:'/a/b/', state: {a: 2}});
inject(function($location) {
expect($location.state()).toEqual({a: 2});
});
- }));
+ });
- it('should update $location when browser state changes', inject(function($browser, $location) {
- $browser.url('http://new.com/a/b/', false, {b: 3});
- $browser.poll();
- expect($location.state()).toEqual({b: 3});
- }));
+ it('should update $location when browser state changes', function() {
+ initService({html5Mode:true, supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b/', baseHref:'/a/b/'});
+ inject(function($location, $window) {
+ $window.history.pushState({b: 3});
+ expect($location.state()).toEqual({b: 3});
+ });
+ });
- it('should replace browser url & state when replace() was called at least once',
+ it('should replace browser url & state when replace() was called at least once', function() {
+ initService({html5Mode:true, supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b/', baseHref:'/a/b/'});
inject(function($rootScope, $location, $browser) {
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
$location.path('/n/url').state({a: 2}).replace();
@@ -833,9 +1033,13 @@ describe('$location', function() {
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b/n/url', true, {a: 2}]);
expect($location.$$replace).toBe(false);
expect($location.$$state).toEqual({a: 2});
- }));
+ });
+ });
+
+ it('should use only the most recent url & state definition', function() {
+ initService({html5Mode:true, supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b/', baseHref:'/a/b/'});
- it('should use only the most recent url & state definition',
inject(function($rootScope, $location, $browser) {
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
$location.path('/n/url').state({a: 2}).replace().state({b: 3}).path('/o/url');
@@ -845,9 +1049,13 @@ describe('$location', function() {
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b/o/url', true, {b: 3}]);
expect($location.$$replace).toBe(false);
expect($location.$$state).toEqual({b: 3});
- }));
+ });
+ });
+
+ it('should allow to set state without touching the URL', function() {
+ initService({html5Mode:true, supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b/', baseHref:'/a/b/'});
- it('should allow to set state without touching the URL',
inject(function($rootScope, $location, $browser) {
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
$location.state({a: 2}).replace().state({b: 3});
@@ -857,30 +1065,39 @@ describe('$location', function() {
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b/', true, {b: 3}]);
expect($location.$$replace).toBe(false);
expect($location.$$state).toEqual({b: 3});
- }));
+ });
+ });
- it('should always reset replace flag after running watch', inject(function($rootScope, $location) {
- // init watches
- $location.url('/initUrl').state({a: 2});
- $rootScope.$apply();
+ it('should always reset replace flag after running watch', function() {
+ initService({html5Mode:true, supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b/', baseHref:'/a/b/'});
- // changes url & state but resets them before digest
- $location.url('/newUrl').state({a: 2}).replace().state({b: 3}).url('/initUrl');
- $rootScope.$apply();
- expect($location.$$replace).toBe(false);
+ inject(function($rootScope, $location) {
+ // init watches
+ $location.url('/initUrl').state({a: 2});
+ $rootScope.$apply();
- // set the url to the old value
- $location.url('/newUrl').state({a: 2}).replace();
- $rootScope.$apply();
- expect($location.$$replace).toBe(false);
+ // changes url & state but resets them before digest
+ $location.url('/newUrl').state({a: 2}).replace().state({b: 3}).url('/initUrl');
+ $rootScope.$apply();
+ expect($location.$$replace).toBe(false);
- // doesn't even change url only calls replace()
- $location.replace();
- $rootScope.$apply();
- expect($location.$$replace).toBe(false);
- }));
+ // set the url to the old value
+ $location.url('/newUrl').state({a: 2}).replace();
+ $rootScope.$apply();
+ expect($location.$$replace).toBe(false);
+
+ // doesn't even change url only calls replace()
+ $location.replace();
+ $rootScope.$apply();
+ expect($location.$$replace).toBe(false);
+ });
+ });
+
+ it('should allow to modify state only before digest', function() {
+ initService({html5Mode:true, supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b/', baseHref:'/a/b/'});
- it('should allow to modify state only before digest',
inject(function($rootScope, $location, $browser) {
var o = {a: 2};
$location.state(o);
@@ -891,23 +1108,35 @@ describe('$location', function() {
o.a = 4;
$rootScope.$apply();
expect($browser.state()).toEqual({a: 3});
- }));
+ });
+ });
+
+ it('should make $location.state() referencially identical with $browser.state() after digest', function() {
+ initService({html5Mode:true, supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b/', baseHref:'/a/b/'});
- it('should make $location.state() referencially identical with $browser.state() after digest',
inject(function($rootScope, $location, $browser) {
$location.state({a: 2});
$rootScope.$apply();
expect($location.state()).toBe($browser.state());
- }));
+ });
+ });
+
+ it('should allow to query the state after digest', function() {
+ initService({html5Mode:true, supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b/', baseHref:'/a/b/'});
- it('should allow to query the state after digest',
inject(function($rootScope, $location) {
$location.url('/foo').state({a: 2});
$rootScope.$apply();
expect($location.state()).toEqual({a: 2});
- }));
+ });
+ });
+
+ it('should reset the state on .url() after digest', function() {
+ initService({html5Mode:true, supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b/', baseHref:'/a/b/'});
- it('should reset the state on .url() after digest',
inject(function($rootScope, $location, $browser) {
$location.url('/foo').state({a: 2});
$rootScope.$apply();
@@ -918,7 +1147,21 @@ describe('$location', function() {
expect($browserUrl).toHaveBeenCalledOnce();
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b/bar', false, null]);
- }));
+ });
+ });
+
+ it('should force a page reload if navigating outside of the application base href', function() {
+ initService({html5Mode:true, supportHistory: true});
+ mockUpBrowser({initialUrl:'http://new.com/a/b/', baseHref:'/a/b/'});
+
+ inject(function($window, $browser, $location) {
+ $window.location.href = 'http://new.com/a/outside.html';
+ spyOn($window.location, '$$setHref');
+ expect($window.location.$$setHref).not.toHaveBeenCalled();
+ $browser.$$checkUrlChange();
+ expect($window.location.$$setHref).toHaveBeenCalledWith('http://new.com/a/outside.html');
+ });
+ });
});
@@ -927,8 +1170,8 @@ describe('$location', function() {
it('should use hashbang url with hash prefix', function() {
initService({html5Mode:false,hashPrefix: '!'});
+ mockUpBrowser({initialUrl:'http://domain.com/base/index.html#!/a/b', baseHref:'/base/index.html'});
inject(
- initBrowser({url:'http://domain.com/base/index.html#!/a/b',basePath: '/base/index.html'}),
function($rootScope, $location, $browser) {
expect($browser.url()).toBe('http://domain.com/base/index.html#!/a/b');
$location.path('/new');
@@ -942,8 +1185,8 @@ describe('$location', function() {
it('should use hashbang url without hash prefix', function() {
initService({html5Mode:false,hashPrefix: ''});
+ mockUpBrowser({initialUrl:'http://domain.com/base/index.html#/a/b', baseHref:'/base/index.html'});
inject(
- initBrowser({url:'http://domain.com/base/index.html#/a/b',basePath: '/base/index.html'}),
function($rootScope, $location, $browser) {
expect($browser.url()).toBe('http://domain.com/base/index.html#/a/b');
$location.path('/new');
@@ -959,10 +1202,6 @@ describe('$location', function() {
// html5 history enabled, but not supported by browser
describe('history on old browser', function() {
- afterEach(inject(function($rootElement) {
- dealoc($rootElement);
- }));
-
it('should use hashbang url with hash prefix', function() {
initService({html5Mode:true,hashPrefix: '!!',supportHistory: false});
inject(
@@ -977,7 +1216,6 @@ describe('$location', function() {
);
});
-
it('should redirect to hashbang url when new url given', function() {
initService({html5Mode:true,hashPrefix: '!'});
inject(
@@ -1003,14 +1241,10 @@ describe('$location', function() {
// html5 history enabled and supported by browser
describe('history on new browser', function() {
- afterEach(inject(function($rootElement) {
- dealoc($rootElement);
- }));
-
it('should use new url', function() {
- initService({html5Mode:true,hashPrefix: '',supportHistory: true});
+ initService({html5Mode:true,hashPrefix:'',supportHistory:true});
+ mockUpBrowser({initialUrl:'http://domain.com/base/old/index.html#a', baseHref:'/base/index.html'});
inject(
- initBrowser({url:'http://domain.com/base/old/index.html#a',basePath: '/base/index.html'}),
function($rootScope, $location, $browser) {
expect($browser.url()).toBe('http://domain.com/base/old/index.html#a');
$location.path('/new');
@@ -1024,8 +1258,8 @@ describe('$location', function() {
it('should rewrite when hashbang url given', function() {
initService({html5Mode:true,hashPrefix: '!',supportHistory: true});
+ mockUpBrowser({initialUrl:'http://domain.com/base/index.html#!/a/b', baseHref:'/base/index.html'});
inject(
- initBrowser({url:'http://domain.com/base/index.html#!/a/b',basePath: '/base/index.html'}),
function($rootScope, $location, $browser) {
expect($browser.url()).toBe('http://domain.com/base/a/b');
$location.path('/new');
@@ -1040,8 +1274,8 @@ describe('$location', function() {
it('should rewrite when hashbang url given (without hash prefix)', function() {
initService({html5Mode:true,hashPrefix: '',supportHistory: true});
+ mockUpBrowser({initialUrl:'http://domain.com/base/index.html#/a/b', baseHref:'/base/index.html'});
inject(
- initBrowser({url:'http://domain.com/base/index.html#/a/b',basePath: '/base/index.html'}),
function($rootScope, $location, $browser) {
expect($browser.url()).toBe('http://domain.com/base/a/b');
expect($location.path()).toBe('/a/b');
@@ -1084,37 +1318,30 @@ describe('$location', function() {
var root, link, originalBrowser, lastEventPreventDefault;
- function configureService(options) {
+ function configureTestLink(options) {
var linkHref = options.linkHref,
- html5Mode = options.html5Mode,
- supportHist = options.supportHist,
relLink = options.relLink,
attrs = options.attrs,
content = options.content;
- module(function($provide, $locationProvider) {
- attrs = attrs ? ' ' + attrs + ' ' : '';
+ attrs = attrs ? ' ' + attrs + ' ' : '';
- if (typeof linkHref === 'string') {
- if (!relLink) {
- if (linkHref[0] == '/') {
- linkHref = 'http://host.com' + linkHref;
- } else if (!linkHref.match(/:\/\//)) {
- // fake the behavior of tag
- linkHref = 'http://host.com/base/' + linkHref;
- }
- }
+ if (typeof linkHref === 'string' && !relLink) {
+ if (linkHref[0] == '/') {
+ linkHref = 'http://host.com' + linkHref;
+ } else if (!linkHref.match(/:\/\//)) {
+ // fake the behavior of tag
+ linkHref = 'http://host.com/base/' + linkHref;
}
+ }
- if (linkHref) {
- link = jqLite('' + content + ' ')[0];
- } else {
- link = jqLite('' + content + ' ')[0];
- }
+ if (linkHref) {
+ link = jqLite('' + content + ' ')[0];
+ } else {
+ link = jqLite('' + content + ' ')[0];
+ }
- $provide.value('$sniffer', {history: supportHist});
- $locationProvider.html5Mode(html5Mode);
- $locationProvider.hashPrefix('!');
+ module(function($provide) {
return function($rootElement, $document) {
$rootElement.append(link);
root = $rootElement[0];
@@ -1124,14 +1351,7 @@ describe('$location', function() {
});
}
- function initBrowser() {
- return function($browser, $document) {
- $browser.url('http://host.com/base/index.html');
- $browser.$$baseHref = '/base/index.html';
- };
- }
-
- function initLocation() {
+ function setupRewriteChecks() {
return function($browser, $location, $rootElement) {
originalBrowser = $browser.url();
// we have to prevent the default operation, as we need to test absolute links (http://...)
@@ -1160,10 +1380,11 @@ describe('$location', function() {
it('should rewrite rel link to new url when history enabled on new browser', function() {
- configureService({linkHref: 'link?a#b', html5Mode: true, supportHist: true});
+ configureTestLink({linkHref: 'link?a#b'});
+ initService({html5Mode:true,supportHistory:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectRewriteTo($browser, 'http://host.com/base/link?a#b');
@@ -1173,10 +1394,11 @@ describe('$location', function() {
it('should do nothing if already on the same URL', function() {
- configureService({linkHref: '/base/', html5Mode: true, supportHist: true});
+ configureTestLink({linkHref: '/base/'});
+ initService({html5Mode:true,supportHistory:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectRewriteTo($browser, 'http://host.com/base/');
@@ -1200,10 +1422,11 @@ describe('$location', function() {
it('should rewrite abs link to new url when history enabled on new browser', function() {
- configureService({linkHref: '/base/link?a#b', html5Mode: true, supportHist: true});
+ configureTestLink({linkHref: '/base/link?a#b'});
+ initService({html5Mode:true,supportHistory:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectRewriteTo($browser, 'http://host.com/base/link?a#b');
@@ -1213,10 +1436,11 @@ describe('$location', function() {
it('should rewrite rel link to hashbang url when history enabled on old browser', function() {
- configureService({linkHref: 'link?a#b', html5Mode: true, supportHist: false});
+ configureTestLink({linkHref: 'link?a#b'});
+ initService({html5Mode:true,supportHistory:false,hashPrefix:'!'});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectRewriteTo($browser, 'http://host.com/base/index.html#!/link?a#b');
@@ -1227,10 +1451,11 @@ describe('$location', function() {
// Regression (gh-7721)
it('should not throw when clicking anchor with no href attribute when history enabled on old browser', function() {
- configureService({linkHref: null, html5Mode: true, supportHist: false});
+ configureTestLink({linkHref: null});
+ initService({html5Mode:true,supportHistory:false});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectNoRewrite($browser);
@@ -1240,10 +1465,11 @@ describe('$location', function() {
it('should produce relative paths correctly when $location.path() is "/" when history enabled on old browser', function() {
- configureService({linkHref: 'partial1', html5Mode: true, supportHist: false});
+ configureTestLink({linkHref: 'partial1'});
+ initService({html5Mode:true,supportHistory:false,hashPrefix:'!'});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser, $location, $rootScope) {
$rootScope.$apply(function() {
$location.path('/');
@@ -1256,10 +1482,11 @@ describe('$location', function() {
it('should rewrite abs link to hashbang url when history enabled on old browser', function() {
- configureService({linkHref: '/base/link?a#b', html5Mode: true, supportHist: false});
+ configureTestLink({linkHref: '/base/link?a#b'});
+ initService({html5Mode:true,supportHistory:false,hashPrefix:'!'});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectRewriteTo($browser, 'http://host.com/base/index.html#!/link?a#b');
@@ -1269,10 +1496,11 @@ describe('$location', function() {
it('should not rewrite full url links to different domain', function() {
- configureService({linkHref: 'http://www.dot.abc/a?b=c', html5Mode: true});
+ configureTestLink({linkHref: 'http://www.dot.abc/a?b=c'});
+ initService({html5Mode:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectNoRewrite($browser);
@@ -1282,10 +1510,11 @@ describe('$location', function() {
it('should not rewrite links with target="_blank"', function() {
- configureService({linkHref: 'base/a?b=c', html5Mode: true, supportHist: true, attrs: 'target="_blank"'});
+ configureTestLink({linkHref: 'base/a?b=c', attrs: 'target="_blank"'});
+ initService({html5Mode:true,supportHistory:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectNoRewrite($browser);
@@ -1295,10 +1524,11 @@ describe('$location', function() {
it('should not rewrite links with target specified', function() {
- configureService({linkHref: 'base/a?b=c', html5Mode: true, supportHist: true, attrs: 'target="some-frame"'});
+ configureTestLink({linkHref: 'base/a?b=c', attrs: 'target="some-frame"'});
+ initService({html5Mode:true,supportHistory:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectNoRewrite($browser);
@@ -1308,10 +1538,11 @@ describe('$location', function() {
it('should not rewrite links with `javascript:` URI', function() {
- configureService({linkHref: ' jAvAsCrIpT:throw new Error("Boom!")', html5Mode: true, supportHist: true, relLink: true});
+ configureTestLink({linkHref: ' jAvAsCrIpT:throw new Error("Boom!")', relLink: true});
+ initService({html5Mode:true,supportHistory:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectNoRewrite($browser);
@@ -1321,10 +1552,11 @@ describe('$location', function() {
it('should not rewrite links with `mailto:` URI', function() {
- configureService({linkHref: ' mAiLtO:foo@bar.com', html5Mode: true, supportHist: true, relLink: true});
+ configureTestLink({linkHref: ' mAiLtO:foo@bar.com', relLink: true});
+ initService({html5Mode:true,supportHistory:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectNoRewrite($browser);
@@ -1334,10 +1566,11 @@ describe('$location', function() {
it('should not rewrite links when rewriting links is disabled', function() {
- configureService({linkHref: 'link?a#b', html5Mode: {enabled: true, rewriteLinks:false}, supportHist: true});
+ configureTestLink({linkHref: 'link?a#b', html5Mode: {enabled: true, rewriteLinks:false}, supportHist: true});
+ initService({html5Mode:{enabled: true, rewriteLinks:false},supportHistory:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectNoRewrite($browser);
@@ -1347,10 +1580,11 @@ describe('$location', function() {
it('should rewrite full url links to same domain and base path', function() {
- configureService({linkHref: 'http://host.com/base/new', html5Mode: true});
+ configureTestLink({linkHref: 'http://host.com/base/new'});
+ initService({html5Mode:true,supportHistory:false,hashPrefix:'!'});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectRewriteTo($browser, 'http://host.com/base/index.html#!/new');
@@ -1360,10 +1594,11 @@ describe('$location', function() {
it('should rewrite when clicked span inside link', function() {
- configureService({linkHref: 'some/link', html5Mode: true, supportHist: true, attrs: '', content: 'link '});
+ configureTestLink({linkHref: 'some/link', attrs: '', content: 'link '});
+ initService({html5Mode:true,supportHistory:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
var span = jqLite(link).find('span');
@@ -1376,10 +1611,11 @@ describe('$location', function() {
it('should not rewrite when link to different base path when history enabled on new browser',
function() {
- configureService({linkHref: '/other_base/link', html5Mode: true, supportHist: true});
+ configureTestLink({linkHref: '/other_base/link'});
+ initService({html5Mode:true,supportHistory:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectNoRewrite($browser);
@@ -1390,10 +1626,11 @@ describe('$location', function() {
it('should not rewrite when link to different base path when history enabled on old browser',
function() {
- configureService({linkHref: '/other_base/link', html5Mode: true, supportHist: false});
+ configureTestLink({linkHref: '/other_base/link'});
+ initService({html5Mode:true,supportHistory:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectNoRewrite($browser);
@@ -1403,10 +1640,11 @@ describe('$location', function() {
it('should not rewrite when link to different base path when history disabled', function() {
- configureService({linkHref: '/other_base/link', html5Mode: false});
+ configureTestLink({linkHref: '/other_base/link'});
+ initService({html5Mode:false});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectNoRewrite($browser);
@@ -1417,10 +1655,11 @@ describe('$location', function() {
it('should not rewrite when full link to different base path when history enabled on new browser',
function() {
- configureService({linkHref: 'http://host.com/other_base/link', html5Mode: true, supportHist: true});
+ configureTestLink({linkHref: 'http://host.com/other_base/link'});
+ initService({html5Mode:true,supportHistory:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectNoRewrite($browser);
@@ -1431,10 +1670,10 @@ describe('$location', function() {
it('should not rewrite when full link to different base path when history enabled on old browser',
function() {
- configureService({linkHref: 'http://host.com/other_base/link', html5Mode: true, supportHist: false});
+ configureTestLink({linkHref: 'http://host.com/other_base/link', html5Mode: true, supportHist: false});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectNoRewrite($browser);
@@ -1444,10 +1683,11 @@ describe('$location', function() {
it('should not rewrite when full link to different base path when history disabled', function() {
- configureService({linkHref: 'http://host.com/other_base/link', html5Mode: false});
+ configureTestLink({linkHref: 'http://host.com/other_base/link'});
+ initService({html5Mode:false});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click');
expectNoRewrite($browser);
@@ -1456,10 +1696,11 @@ describe('$location', function() {
});
it('should replace current hash fragment when link begins with "#" history disabled', function() {
- configureService({linkHref: '#link', html5Mode: true, supportHist: false, relLink: true});
+ configureTestLink({linkHref: '#link', relLink: true});
+ initService({html5Mode:true,supportHistory:false,hashPrefix:'!'});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser, $location, $rootScope) {
$rootScope.$apply(function() {
$location.path('/some');
@@ -1473,10 +1714,11 @@ describe('$location', function() {
});
it('should replace current hash fragment when link begins with "#" history enabled', function() {
- configureService({linkHref: '#link', html5Mode: true, supportHist: true, relLink: true});
+ configureTestLink({linkHref: '#link', relLink: true});
+ initService({html5Mode:true,supportHistory:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser, $location, $rootScope) {
$rootScope.$apply(function() {
$location.path('/some');
@@ -1490,10 +1732,11 @@ describe('$location', function() {
});
it('should not rewrite when clicked with ctrl pressed', function() {
- configureService({linkHref: 'base/a?b=c', html5Mode: true, supportHist: true});
+ configureTestLink({linkHref: 'base/a?b=c'});
+ initService({html5Mode:true,supportHistory:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click', { keys: ['ctrl'] });
expectNoRewrite($browser);
@@ -1503,10 +1746,11 @@ describe('$location', function() {
it('should not rewrite when clicked with meta pressed', function() {
- configureService({linkHref: 'base/a?b=c', html5Mode: true, supportHist: true});
+ configureTestLink({linkHref: 'base/a?b=c'});
+ initService({html5Mode:true,supportHistory:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click', { keys: ['meta'] });
expectNoRewrite($browser);
@@ -1515,10 +1759,11 @@ describe('$location', function() {
});
it('should not rewrite when right click pressed', function() {
- configureService({linkHref: 'base/a?b=c', html5Mode: true, supportHist: true});
+ configureTestLink({linkHref: 'base/a?b=c'});
+ initService({html5Mode:true,supportHistory:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
var rightClick;
if (document.createEvent) {
@@ -1550,10 +1795,11 @@ describe('$location', function() {
it('should not rewrite when clicked with shift pressed', function() {
- configureService({linkHref: 'base/a?b=c', html5Mode: true, supportHist: true});
+ configureTestLink({linkHref: 'base/a?b=c'});
+ initService({html5Mode:true,supportHistory:true});
inject(
- initBrowser(),
- initLocation(),
+ initBrowser({ url: 'http://host.com/base/index.html', basePath: '/base/index.html' }),
+ setupRewriteChecks(),
function($browser) {
browserTrigger(link, 'click', { keys: ['shift'] });
expectNoRewrite($browser);
@@ -2047,99 +2293,95 @@ describe('$location', function() {
);
});
- function parseLinkAndReturn(location, url, relHref) {
- if (location.$$parseLinkUrl(url, relHref)) {
- return location.absUrl();
- }
- return undefined;
- }
-
- describe('html5Mode', function() {
- it('should set enabled, requireBase and rewriteLinks when called with object', function() {
- module(function($locationProvider) {
- $locationProvider.html5Mode({enabled: true, requireBase: false, rewriteLinks: false});
- expect($locationProvider.html5Mode()).toEqual({
- enabled: true,
- requireBase: false,
- rewriteLinks: false
+ describe('$locationProvider', function() {
+ describe('html5Mode', function() {
+ it('should set enabled, requireBase and rewriteLinks when called with object', function() {
+ module(function($locationProvider) {
+ $locationProvider.html5Mode({enabled: true, requireBase: false, rewriteLinks: false});
+ expect($locationProvider.html5Mode()).toEqual({
+ enabled: true,
+ requireBase: false,
+ rewriteLinks: false
+ });
});
+
+ inject(function() {});
});
- inject(function() {});
- });
+ it('should only overwrite existing properties if values are boolean', function() {
+ module(function($locationProvider) {
+ $locationProvider.html5Mode({
+ enabled: 'duh',
+ requireBase: 'probably',
+ rewriteLinks: 'nope'
+ });
- it('should only overwrite existing properties if values are boolean', function() {
- module(function($locationProvider) {
- $locationProvider.html5Mode({
- enabled: 'duh',
- requireBase: 'probably',
- rewriteLinks: 'nope'
+ expect($locationProvider.html5Mode()).toEqual({
+ enabled: false,
+ requireBase: true,
+ rewriteLinks: true
+ });
});
- expect($locationProvider.html5Mode()).toEqual({
- enabled: false,
- requireBase: true,
- rewriteLinks: true
- });
+ inject(function() {});
});
- inject(function() {});
- });
+ it('should not set unknown input properties to html5Mode object', function() {
+ module(function($locationProvider) {
+ $locationProvider.html5Mode({
+ someProp: 'foo'
+ });
- it('should not set unknown input properties to html5Mode object', function() {
- module(function($locationProvider) {
- $locationProvider.html5Mode({
- someProp: 'foo'
+ expect($locationProvider.html5Mode()).toEqual({
+ enabled: false,
+ requireBase: true,
+ rewriteLinks: true
+ });
});
- expect($locationProvider.html5Mode()).toEqual({
- enabled: false,
- requireBase: true,
- rewriteLinks: true
- });
+ inject(function() {});
});
- inject(function() {});
- });
-
- it('should default to enabled:false, requireBase:true and rewriteLinks:true', function() {
- module(function($locationProvider) {
- expect($locationProvider.html5Mode()).toEqual({
- enabled: false,
- requireBase: true,
- rewriteLinks: true
+ it('should default to enabled:false, requireBase:true and rewriteLinks:true', function() {
+ module(function($locationProvider) {
+ expect($locationProvider.html5Mode()).toEqual({
+ enabled: false,
+ requireBase: true,
+ rewriteLinks: true
+ });
});
- });
- inject(function() {});
+ inject(function() {});
+ });
});
});
+
describe('LocationHtml5Url', function() {
- var location, locationIndex;
+ var locationUrl, locationIndexUrl;
beforeEach(function() {
- location = new LocationHtml5Url('http://server/pre/', 'http://server/pre/path');
- locationIndex = new LocationHtml5Url('http://server/pre/index.html', 'http://server/pre/path');
+ locationUrl = new LocationHtml5Url('http://server/pre/', 'http://server/pre/', 'http://server/pre/path');
+ locationIndexUrl = new LocationHtml5Url('http://server/pre/index.html', 'http://server/pre/', 'http://server/pre/path');
});
it('should rewrite URL', function() {
- expect(parseLinkAndReturn(location, 'http://other')).toEqual(undefined);
- expect(parseLinkAndReturn(location, 'http://server/pre')).toEqual('http://server/pre/');
- expect(parseLinkAndReturn(location, 'http://server/pre/')).toEqual('http://server/pre/');
- expect(parseLinkAndReturn(location, 'http://server/pre/otherPath')).toEqual('http://server/pre/otherPath');
+ expect(parseLinkAndReturn(locationUrl, 'http://other')).toEqual(undefined);
+ expect(parseLinkAndReturn(locationUrl, 'http://server/pre')).toEqual('http://server/pre/');
+ expect(parseLinkAndReturn(locationUrl, 'http://server/pre/')).toEqual('http://server/pre/');
+ expect(parseLinkAndReturn(locationUrl, 'http://server/pre/otherPath')).toEqual('http://server/pre/otherPath');
// Note: relies on the previous state!
- expect(parseLinkAndReturn(location, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/otherPath#test');
+ expect(parseLinkAndReturn(locationUrl, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/otherPath#test');
- expect(parseLinkAndReturn(locationIndex, 'http://server/pre')).toEqual('http://server/pre/');
- expect(parseLinkAndReturn(locationIndex, 'http://server/pre/')).toEqual('http://server/pre/');
- expect(parseLinkAndReturn(locationIndex, 'http://server/pre/otherPath')).toEqual('http://server/pre/otherPath');
+ expect(parseLinkAndReturn(locationIndexUrl, 'http://server/pre')).toEqual('http://server/pre/');
+ expect(parseLinkAndReturn(locationIndexUrl, 'http://server/pre/')).toEqual('http://server/pre/');
+ expect(parseLinkAndReturn(locationIndexUrl, 'http://server/pre/otherPath')).toEqual('http://server/pre/otherPath');
// Note: relies on the previous state!
- expect(parseLinkAndReturn(location, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/otherPath#test');
+ expect(parseLinkAndReturn(locationUrl, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/otherPath#test');
});
@@ -2176,106 +2418,190 @@ describe('$location', function() {
});
it('should support state', function() {
- expect(location.state({a: 2}).state()).toEqual({a: 2});
+ expect(locationUrl.state({a: 2}).state()).toEqual({a: 2});
});
});
- function throwOnState(location) {
- expect(function() {
- location.state({a: 2});
- }).toThrowMinErr('$location', 'nostate', 'History API state support is available only ' +
- 'in HTML5 mode and only in browsers supporting HTML5 History API'
- );
- }
-
describe('LocationHashbangUrl', function() {
- var location;
-
- function parseLinkAndReturn(location, url, relHref) {
- if (location.$$parseLinkUrl(url, relHref)) {
- return location.absUrl();
- }
- return undefined;
- }
+ var locationUrl;
it('should rewrite URL', function() {
/* jshint scripturl: true */
- location = new LocationHashbangUrl('http://server/pre/', '#');
+ locationUrl = new LocationHashbangUrl('http://server/pre/', 'http://server/pre/', '#');
- expect(parseLinkAndReturn(location, 'http://other')).toEqual(undefined);
- expect(parseLinkAndReturn(location, 'http://server/pre/')).toEqual('http://server/pre/');
- expect(parseLinkAndReturn(location, 'http://server/pre/#otherPath')).toEqual('http://server/pre/#/otherPath');
- expect(parseLinkAndReturn(location, 'javascript:void(0)')).toEqual(undefined);
+ expect(parseLinkAndReturn(locationUrl, 'http://other')).toEqual(undefined);
+ expect(parseLinkAndReturn(locationUrl, 'http://server/pre/')).toEqual('http://server/pre/');
+ expect(parseLinkAndReturn(locationUrl, 'http://server/pre/#otherPath')).toEqual('http://server/pre/#/otherPath');
+ expect(parseLinkAndReturn(locationUrl, 'javascript:void(0)')).toEqual(undefined);
});
it("should not set hash if one was not originally specified", function() {
- location = new LocationHashbangUrl('http://server/pre/index.html', '#');
+ locationUrl = new LocationHashbangUrl('http://server/pre/index.html', 'http://server/pre/', '#');
- location.$$parse('http://server/pre/index.html');
- expect(location.url()).toBe('');
- expect(location.absUrl()).toBe('http://server/pre/index.html');
+ locationUrl.$$parse('http://server/pre/index.html');
+ expect(locationUrl.url()).toBe('');
+ expect(locationUrl.absUrl()).toBe('http://server/pre/index.html');
});
it("should parse hash if one was specified", function() {
- location = new LocationHashbangUrl('http://server/pre/index.html', '#');
+ locationUrl = new LocationHashbangUrl('http://server/pre/index.html', 'http://server/pre/', '#');
- location.$$parse('http://server/pre/index.html#/foo/bar');
- expect(location.url()).toBe('/foo/bar');
- expect(location.absUrl()).toBe('http://server/pre/index.html#/foo/bar');
+ locationUrl.$$parse('http://server/pre/index.html#/foo/bar');
+ expect(locationUrl.url()).toBe('/foo/bar');
+ expect(locationUrl.absUrl()).toBe('http://server/pre/index.html#/foo/bar');
});
it("should prefix hash url with / if one was originally missing", function() {
- location = new LocationHashbangUrl('http://server/pre/index.html', '#');
+ locationUrl = new LocationHashbangUrl('http://server/pre/index.html', 'http://server/pre/', '#');
- location.$$parse('http://server/pre/index.html#not-starting-with-slash');
- expect(location.url()).toBe('/not-starting-with-slash');
- expect(location.absUrl()).toBe('http://server/pre/index.html#/not-starting-with-slash');
+ locationUrl.$$parse('http://server/pre/index.html#not-starting-with-slash');
+ expect(locationUrl.url()).toBe('/not-starting-with-slash');
+ expect(locationUrl.absUrl()).toBe('http://server/pre/index.html#/not-starting-with-slash');
});
it('should not strip stuff from path just because it looks like Windows drive when it\'s not',
function() {
- location = new LocationHashbangUrl('http://server/pre/index.html', '#');
+ locationUrl = new LocationHashbangUrl('http://server/pre/index.html', 'http://server/pre/', '#');
- location.$$parse('http://server/pre/index.html#http%3A%2F%2Fexample.com%2F');
- expect(location.url()).toBe('/http://example.com/');
- expect(location.absUrl()).toBe('http://server/pre/index.html#/http://example.com/');
+ locationUrl.$$parse('http://server/pre/index.html#http%3A%2F%2Fexample.com%2F');
+ expect(locationUrl.url()).toBe('/http://example.com/');
+ expect(locationUrl.absUrl()).toBe('http://server/pre/index.html#/http://example.com/');
});
it('should throw on url(urlString, stateObject)', function() {
- throwOnState(location);
+ expectThrowOnStateChange(locationUrl);
+ });
+
+ it('should allow navigating outside the original base URL', function() {
+ locationUrl = new LocationHashbangUrl('http://server/pre/index.html', 'http://server/pre/', '#');
+
+ locationUrl.$$parse('http://server/next/index.html');
+ expect(locationUrl.url()).toBe('');
+ expect(locationUrl.absUrl()).toBe('http://server/next/index.html');
});
});
describe('LocationHashbangInHtml5Url', function() {
/* global LocationHashbangInHtml5Url: false */
- var location, locationIndex;
+ var locationUrl, locationIndexUrl;
beforeEach(function() {
- location = new LocationHashbangInHtml5Url('http://server/pre/', '#!');
- locationIndex = new LocationHashbangInHtml5Url('http://server/pre/index.html', '#!');
+ locationUrl = new LocationHashbangInHtml5Url('http://server/pre/', 'http://server/pre/', '#!');
+ locationIndexUrl = new LocationHashbangInHtml5Url('http://server/pre/index.html', 'http://server/pre/', '#!');
});
it('should rewrite URL', function() {
- expect(parseLinkAndReturn(location, 'http://other')).toEqual(undefined);
- expect(parseLinkAndReturn(location, 'http://server/pre')).toEqual('http://server/pre/#!');
- expect(parseLinkAndReturn(location, 'http://server/pre/')).toEqual('http://server/pre/#!');
- expect(parseLinkAndReturn(location, 'http://server/pre/otherPath')).toEqual('http://server/pre/#!/otherPath');
+ expect(parseLinkAndReturn(locationUrl, 'http://other')).toEqual(undefined);
+ expect(parseLinkAndReturn(locationUrl, 'http://server/pre')).toEqual('http://server/pre/#!');
+ expect(parseLinkAndReturn(locationUrl, 'http://server/pre/')).toEqual('http://server/pre/#!');
+ expect(parseLinkAndReturn(locationUrl, 'http://server/pre/otherPath')).toEqual('http://server/pre/#!/otherPath');
// Note: relies on the previous state!
- expect(parseLinkAndReturn(location, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/#!/otherPath#test');
+ expect(parseLinkAndReturn(locationUrl, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/#!/otherPath#test');
- expect(parseLinkAndReturn(locationIndex, 'http://server/pre')).toEqual('http://server/pre/index.html#!');
- expect(parseLinkAndReturn(locationIndex, 'http://server/pre/')).toEqual(undefined);
- expect(parseLinkAndReturn(locationIndex, 'http://server/pre/otherPath')).toEqual('http://server/pre/index.html#!/otherPath');
+ expect(parseLinkAndReturn(locationIndexUrl, 'http://server/pre')).toEqual('http://server/pre/index.html#!');
+ expect(parseLinkAndReturn(locationIndexUrl, 'http://server/pre/')).toEqual(undefined);
+ expect(parseLinkAndReturn(locationIndexUrl, 'http://server/pre/otherPath')).toEqual('http://server/pre/index.html#!/otherPath');
// Note: relies on the previous state!
- expect(parseLinkAndReturn(locationIndex, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/index.html#!/otherPath#test');
+ expect(parseLinkAndReturn(locationIndexUrl, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/index.html#!/otherPath#test');
});
it('should throw on url(urlString, stateObject)', function() {
- throwOnState(location);
+ expectThrowOnStateChange(locationUrl);
});
});
+
+
+ function initService(options) {
+ return module(function($provide, $locationProvider) {
+ $locationProvider.html5Mode(options.html5Mode);
+ $locationProvider.hashPrefix(options.hashPrefix);
+ $provide.value('$sniffer', {history: options.supportHistory});
+ });
+ }
+
+
+ function mockUpBrowser(options) {
+ module(function($windowProvider, $browserProvider) {
+ var browser;
+ var parser = window.document.createElement('a');
+ parser.href = options.initialUrl;
+
+ $windowProvider.$get = function() {
+ var win = {};
+ angular.extend(win, window);
+ // Ensure `window` is a reference to the mock global object, so that
+ // jqLite does the right thing.
+ win.window = win;
+ win.history = {
+ state: options.state || null,
+ replaceState: function(state, title, url) {
+ win.history.state = copy(state);
+ if (url) win.location.href = url;
+ jqLite(win).triggerHandler('popstate');
+ },
+ pushState: function(state, title, url) {
+ win.history.state = copy(state);
+ if (url) win.location.href = url;
+ jqLite(win).triggerHandler('popstate');
+ }
+ };
+ win.addEventListener = angular.noop;
+ win.removeEventListener = angular.noop;
+ win.location = {
+ get href() { return this.$$getHref(); },
+ $$getHref: function() { return parser.href; },
+ set href(val) { this.$$setHref(val); },
+ $$setHref: function(val) { parser.href = val; },
+ get hash() { return parser.hash; },
+ // The parser correctly strips on a single preceding hash character if necessary
+ // before joining the fragment onto the href by a new hash character
+ // See hash setter spec: https://url.spec.whatwg.org/#urlutils-and-urlutilsreadonly-members
+ set hash(val) { parser.hash = val; },
+
+ replace: function(val) {
+ win.location.href = val;
+ }
+ };
+ return win;
+ };
+ $browserProvider.$get = function($document, $window, $log, $sniffer) {
+ /* global Browser: false */
+ browser = new Browser($window, $document, $log, $sniffer);
+ browser.baseHref = function() {
+ return options.baseHref;
+ };
+ return browser;
+ };
+ });
+ }
+
+
+ function initBrowser(options) {
+ return function($browser) {
+ $browser.url(options.url);
+ $browser.$$baseHref = options.basePath;
+ };
+ }
+
+
+ function expectThrowOnStateChange(location) {
+ expect(function() {
+ location.state({a: 2});
+ }).toThrowMinErr('$location', 'nostate', 'History API state support is available only ' +
+ 'in HTML5 mode and only in browsers supporting HTML5 History API'
+ );
+ }
+
+
+ function parseLinkAndReturn(location, url, relHref) {
+ if (location.$$parseLinkUrl(url, relHref)) {
+ return location.absUrl();
+ }
+ return undefined;
+ }
+
});
diff --git a/test/ng/logSpec.js b/test/ng/logSpec.js
index d9a3cf57c405..cb7ad2d46937 100644
--- a/test/ng/logSpec.js
+++ b/test/ng/logSpec.js
@@ -1,12 +1,6 @@
/* global $LogProvider: false */
'use strict';
-function initService(debugEnabled) {
- return module(function($logProvider) {
- $logProvider.debugEnabled(debugEnabled);
- });
- }
-
describe('$log', function() {
var $window, logger, log, warn, info, error, debug;
@@ -178,4 +172,11 @@ describe('$log', function() {
expect(errorArgs).toEqual(['abc', 'message\nsourceURL:123']);
});
});
+
+ function initService(debugEnabled) {
+ return module(function($logProvider) {
+ $logProvider.debugEnabled(debugEnabled);
+ });
+ }
+
});
diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js
index 2983c7ea6397..321cff48f93a 100644
--- a/test/ng/parseSpec.js
+++ b/test/ng/parseSpec.js
@@ -1745,6 +1745,16 @@ describe('parser', function() {
expect(scope.$eval("0&&2")).toEqual(0 && 2);
expect(scope.$eval("0||2")).toEqual(0 || 2);
expect(scope.$eval("0||1&&2")).toEqual(0 || 1 && 2);
+ expect(scope.$eval("true&&a")).toEqual(true && undefined);
+ expect(scope.$eval("true&&a()")).toEqual(true && undefined);
+ expect(scope.$eval("true&&a()()")).toEqual(true && undefined);
+ expect(scope.$eval("true&&a.b")).toEqual(true && undefined);
+ expect(scope.$eval("true&&a.b.c")).toEqual(true && undefined);
+ expect(scope.$eval("false||a")).toEqual(false || undefined);
+ expect(scope.$eval("false||a()")).toEqual(false || undefined);
+ expect(scope.$eval("false||a()()")).toEqual(false || undefined);
+ expect(scope.$eval("false||a.b")).toEqual(false || undefined);
+ expect(scope.$eval("false||a.b.c")).toEqual(false || undefined);
});
it('should parse ternary', function() {
diff --git a/test/ng/qSpec.js b/test/ng/qSpec.js
index 643daf4d14bd..dbafc18fcfca 100644
--- a/test/ng/qSpec.js
+++ b/test/ng/qSpec.js
@@ -1643,6 +1643,14 @@ describe('q', function() {
});
+ describe('resolve', function() {
+ it('should be an alias of the "when" function', function() {
+ expect(q.resolve).toBeDefined();
+ expect(q.resolve).toEqual(q.when);
+ });
+ });
+
+
describe('optional callbacks', function() {
it('should not require success callback and propagate resolution', function() {
q.when('hi', null, error()).then(success(2), error());
diff --git a/test/ng/rafSpec.js b/test/ng/rafSpec.js
index 04ac6de211bc..e700d392aae4 100644
--- a/test/ng/rafSpec.js
+++ b/test/ng/rafSpec.js
@@ -31,6 +31,46 @@ describe('$$rAF', function() {
expect(present).toBe(true);
}));
+ it('should only consume only one RAF if multiple async functions are registered before the first frame kicks in', inject(function($$rAF) {
+ if (!$$rAF.supported) return;
+
+ //we need to create our own injector to work around the ngMock overrides
+ var rafLog = [];
+ var injector = createInjector(['ng', function($provide) {
+ $provide.value('$window', {
+ location: window.location,
+ history: window.history,
+ webkitRequestAnimationFrame: function(fn) {
+ rafLog.push(fn);
+ }
+ });
+ }]);
+
+ $$rAF = injector.get('$$rAF');
+
+ var log = [];
+ function logFn() {
+ log.push(log.length);
+ }
+
+ $$rAF(logFn);
+ $$rAF(logFn);
+ $$rAF(logFn);
+
+ expect(log).toEqual([]);
+ expect(rafLog.length).toBe(1);
+
+ rafLog[0]();
+
+ expect(log).toEqual([0,1,2]);
+ expect(rafLog.length).toBe(1);
+
+ $$rAF(logFn);
+
+ expect(log).toEqual([0,1,2]);
+ expect(rafLog.length).toBe(2);
+ }));
+
describe('$timeout fallback', function() {
it("it should use a $timeout incase native rAF isn't suppored", function() {
var timeoutSpy = jasmine.createSpy('callback');
diff --git a/test/ng/rootScopeSpec.js b/test/ng/rootScopeSpec.js
index 75fb3232536c..a598499d6c3a 100644
--- a/test/ng/rootScopeSpec.js
+++ b/test/ng/rootScopeSpec.js
@@ -1021,16 +1021,40 @@ describe('Scope', function() {
it('should broadcast $destroy on rootScope', inject(function($rootScope) {
- var spy = spyOn(angular, 'noop');
- $rootScope.$on('$destroy', angular.noop);
+ var spy = jasmine.createSpy('$destroy handler');
+ $rootScope.$on('$destroy', spy);
$rootScope.$destroy();
- $rootScope.$digest();
- expect(log).toEqual('123');
expect(spy).toHaveBeenCalled();
expect($rootScope.$$destroyed).toBe(true);
}));
+ it('should remove all listeners after $destroy of rootScope', inject(function($rootScope) {
+ var spy = jasmine.createSpy('$destroy handler');
+ $rootScope.$on('dummy', spy);
+ $rootScope.$destroy();
+ $rootScope.$broadcast('dummy');
+ expect(spy).not.toHaveBeenCalled();
+ }));
+
+
+ it('should remove all watchers after $destroy of rootScope', inject(function($rootScope) {
+ var spy = jasmine.createSpy('$watch spy');
+ var digest = $rootScope.$digest;
+ $rootScope.$watch(spy);
+ $rootScope.$destroy();
+ digest.call($rootScope);
+ expect(spy).not.toHaveBeenCalled();
+ }));
+
+
+ it('should call $browser.$$applicationDestroyed when destroying rootScope', inject(function($rootScope, $browser) {
+ spyOn($browser, '$$applicationDestroyed');
+ $rootScope.$destroy();
+ expect($browser.$$applicationDestroyed).toHaveBeenCalledOnce();
+ }));
+
+
it('should remove first', inject(function($rootScope) {
first.$destroy();
$rootScope.$digest();
@@ -1481,6 +1505,22 @@ describe('Scope', function() {
}));
+ it('should not clear the state when calling $apply during an $apply', inject(
+ function($rootScope) {
+ $rootScope.$apply(function() {
+ expect(function() {
+ $rootScope.$apply();
+ }).toThrowMinErr('$rootScope', 'inprog', '$apply already in progress');
+ expect(function() {
+ $rootScope.$apply();
+ }).toThrowMinErr('$rootScope', 'inprog', '$apply already in progress');
+ });
+ expect(function() {
+ $rootScope.$apply();
+ }).not.toThrow();
+ }));
+
+
it('should throw an exception if $apply is called while flushing evalAsync queue', inject(
function($rootScope) {
expect(function() {
diff --git a/test/ng/sceSpecs.js b/test/ng/sceSpecs.js
index 11006744d18f..a35f6f3d83b7 100644
--- a/test/ng/sceSpecs.js
+++ b/test/ng/sceSpecs.js
@@ -2,12 +2,6 @@
describe('SCE', function() {
- // Work around an IE8 bug. Though window.inject === angular.mock.inject, if it's invoked the
- // window scope, IE8 loses the exception object that bubbles up and replaces it with a TypeError.
- // By using a local alias, it gets invoked on the global scope instead of window.
- // Ref: https://github.com/angular/angular.js/pull/4221#/issuecomment-25515813
- var inject = angular.mock.inject;
-
describe('when disabled', function() {
beforeEach(function() {
module(function($sceProvider) {
diff --git a/test/ng/snifferSpec.js b/test/ng/snifferSpec.js
index cd4728e3bb1d..c16d4802d966 100644
--- a/test/ng/snifferSpec.js
+++ b/test/ng/snifferSpec.js
@@ -73,8 +73,11 @@ describe('$sniffer', function() {
describe('csp', function() {
- it('should be false by default', function() {
- expect(sniffer({}).csp).toBe(false);
+ it('should have all rules set to false by default', function() {
+ var csp = sniffer({}).csp;
+ forEach(Object.keys(csp), function(key) {
+ expect(csp[key]).toEqual(false);
+ });
});
});
diff --git a/test/ng/timeoutSpec.js b/test/ng/timeoutSpec.js
index c3582af5a94b..2aed8b743a5f 100644
--- a/test/ng/timeoutSpec.js
+++ b/test/ng/timeoutSpec.js
@@ -22,7 +22,7 @@ describe('$timeout', function() {
it('should call $apply after each callback is executed', inject(function($timeout, $rootScope) {
var applySpy = spyOn($rootScope, '$apply').andCallThrough();
- $timeout(function() {});
+ $timeout(noop);
expect(applySpy).not.toHaveBeenCalled();
$timeout.flush();
@@ -30,8 +30,8 @@ describe('$timeout', function() {
applySpy.reset();
- $timeout(function() {});
- $timeout(function() {});
+ $timeout(noop);
+ $timeout(noop);
$timeout.flush();
expect(applySpy.callCount).toBe(2);
}));
@@ -40,7 +40,7 @@ describe('$timeout', function() {
it('should NOT call $apply if skipApply is set to true', inject(function($timeout, $rootScope) {
var applySpy = spyOn($rootScope, '$apply').andCallThrough();
- $timeout(function() {}, 12, false);
+ $timeout(noop, 12, false);
expect(applySpy).not.toHaveBeenCalled();
$timeout.flush();
@@ -89,8 +89,8 @@ describe('$timeout', function() {
// $browser.defer.cancel is only called on cancel if the deferred object is still referenced
var cancelSpy = spyOn($browser.defer, 'cancel').andCallThrough();
- var promise1 = $timeout(function() {}, 0, false);
- var promise2 = $timeout(function() {}, 100, false);
+ var promise1 = $timeout(noop, 0, false);
+ var promise2 = $timeout(noop, 100, false);
expect(cancelSpy).not.toHaveBeenCalled();
$timeout.flush(0);
@@ -104,7 +104,6 @@ describe('$timeout', function() {
expect(cancelSpy).toHaveBeenCalled();
}));
-
it('should allow the `fn` parameter to be optional', inject(function($timeout, log) {
$timeout().then(function(value) { log('promise success: ' + value); }, log.fn('promise error'));
@@ -123,6 +122,35 @@ describe('$timeout', function() {
expect(log).toEqual(['promise success: undefined']);
}));
+ it('should pass the timeout arguments in the timeout callback',
+ inject(function($timeout, $browser, log) {
+ var task1 = jasmine.createSpy('Nappa'),
+ task2 = jasmine.createSpy('Vegeta');
+
+ $timeout(task1, 9000, true, 'What does', 'the timeout', 'say about', 'its delay level');
+ expect($browser.deferredFns.length).toBe(1);
+
+ $timeout(task2, 9001, false, 'It\'s', 'over', 9000);
+ expect($browser.deferredFns.length).toBe(2);
+
+ $timeout(9000, false, 'What!', 9000).then(function(value) { log('There\'s no way that can be right! ' + value); }, log.fn('It can\'t!'));
+ expect($browser.deferredFns.length).toBe(3);
+ expect(log).toEqual([]);
+
+ $timeout.flush(0);
+ expect(task1).not.toHaveBeenCalled();
+
+ $timeout.flush(9000);
+ expect(task1).toHaveBeenCalledWith('What does', 'the timeout', 'say about', 'its delay level');
+
+ $timeout.flush(1);
+ expect(task2).toHaveBeenCalledWith('It\'s', 'over', 9000);
+
+ $timeout.flush(9000);
+ expect(log).toEqual(['There\'s no way that can be right! undefined']);
+
+ }));
+
describe('exception handling', function() {
@@ -133,7 +161,7 @@ describe('$timeout', function() {
it('should delegate exception to the $exceptionHandler service', inject(
function($timeout, $exceptionHandler) {
- $timeout(function() {throw "Test Error";});
+ $timeout(function() { throw "Test Error"; });
expect($exceptionHandler.errors).toEqual([]);
$timeout.flush();
@@ -145,7 +173,7 @@ describe('$timeout', function() {
function($timeout, $rootScope) {
var applySpy = spyOn($rootScope, '$apply').andCallThrough();
- $timeout(function() {throw "Test Error";});
+ $timeout(function() { throw "Test Error"; });
expect(applySpy).not.toHaveBeenCalled();
$timeout.flush();
@@ -164,6 +192,25 @@ describe('$timeout', function() {
}));
+ it('should pass the timeout arguments in the timeout callback even if an exception is thrown',
+ inject(function($timeout, log) {
+ var promise1 = $timeout(function(arg) { throw arg; }, 9000, true, 'Some Arguments');
+ var promise2 = $timeout(function(arg1, args2) { throw arg1 + ' ' + args2; }, 9001, false, 'Are Meant', 'To Be Thrown');
+
+ promise1.then(log.fn('success'), function(reason) { log('error: ' + reason); });
+ promise2.then(log.fn('success'), function(reason) { log('error: ' + reason); });
+
+ $timeout.flush(0);
+ expect(log).toEqual('');
+
+ $timeout.flush(9000);
+ expect(log).toEqual('error: Some Arguments');
+
+ $timeout.flush(1);
+ expect(log).toEqual('error: Some Arguments; error: Are Meant To Be Thrown');
+ }));
+
+
it('should forget references to relevant deferred even when exception is thrown',
inject(function($timeout, $browser) {
// $browser.defer.cancel is only called on cancel if the deferred object is still referenced
@@ -242,7 +289,7 @@ describe('$timeout', function() {
// $browser.defer.cancel is only called on cancel if the deferred object is still referenced
var cancelSpy = spyOn($browser.defer, 'cancel').andCallThrough();
- var promise = $timeout(function() {}, 0, false);
+ var promise = $timeout(noop, 0, false);
expect(cancelSpy).not.toHaveBeenCalled();
$timeout.cancel(promise);
diff --git a/test/ngAnimate/.jshintrc b/test/ngAnimate/.jshintrc
new file mode 100644
index 000000000000..b307fb405952
--- /dev/null
+++ b/test/ngAnimate/.jshintrc
@@ -0,0 +1,13 @@
+{
+ "extends": "../.jshintrc",
+ "browser": true,
+ "newcap": false,
+ "globals": {
+ "mergeAnimationOptions": false,
+ "prepareAnimationOptions": false,
+ "applyAnimationStyles": false,
+ "applyAnimationFromStyles": false,
+ "applyAnimationToStyles": false,
+ "applyAnimationClassesFactory": false
+ }
+}
diff --git a/test/ngAnimate/animateCssDriverSpec.js b/test/ngAnimate/animateCssDriverSpec.js
new file mode 100644
index 000000000000..fc4f56bc62a2
--- /dev/null
+++ b/test/ngAnimate/animateCssDriverSpec.js
@@ -0,0 +1,988 @@
+'use strict';
+
+describe("ngAnimate $$animateCssDriver", function() {
+
+ beforeEach(module('ngAnimate'));
+
+ function int(x) {
+ return parseInt(x, 10);
+ }
+
+ function hasAll(array, vals) {
+ for (var i = 0; i < vals.length; i++) {
+ if (array.indexOf(vals[i]) === -1) return false;
+ }
+ return true;
+ }
+
+ it('should return a noop driver handler if the browser does not support CSS transitions and keyframes', function() {
+ module(function($provide) {
+ $provide.value('$sniffer', {});
+ });
+ inject(function($$animateCssDriver) {
+ expect($$animateCssDriver).toBe(noop);
+ });
+ });
+
+ describe('when active', function() {
+ if (!browserSupportsCssAnimations()) return;
+
+ var element;
+ var ss;
+ afterEach(function() {
+ dealoc(element);
+ if (ss) {
+ ss.destroy();
+ }
+ });
+
+ var capturedAnimation;
+ var captureLog;
+ var driver;
+ var captureFn;
+ beforeEach(module(function($provide) {
+ capturedAnimation = null;
+ captureLog = [];
+ captureFn = noop;
+
+ $provide.factory('$animateCss', function($$AnimateRunner) {
+ return function() {
+ var runner = new $$AnimateRunner();
+
+ capturedAnimation = arguments;
+ captureFn.apply(null, arguments);
+ captureLog.push({
+ element: arguments[0],
+ args: arguments,
+ runner: runner
+ });
+
+ return {
+ $$willAnimate: true,
+ start: function() {
+ return runner;
+ }
+ };
+ };
+ });
+
+ element = jqLite('
');
+
+ return function($$animateCssDriver, $document, $window) {
+ driver = function(details, cb) {
+ return $$animateCssDriver(details, cb || noop);
+ };
+ ss = createMockStyleSheet($document, $window);
+ };
+ }));
+
+ it('should register the $$animateCssDriver into the list of drivers found in $animateProvider',
+ module(function($animateProvider) {
+
+ expect($animateProvider.drivers).toContain('$$animateCssDriver');
+ }));
+
+ it('should register the $$animateCssDriver into the list of drivers found in $animateProvider',
+ module(function($animateProvider) {
+
+ expect($animateProvider.drivers).toContain('$$animateCssDriver');
+ }));
+
+ describe("regular animations", function() {
+ it("should render an animation on the given element", inject(function() {
+ driver({ element: element });
+ expect(capturedAnimation[0]).toBe(element);
+ }));
+
+ it("should return an object with a start function", inject(function() {
+ var runner = driver({ element: element });
+ expect(isFunction(runner.start)).toBeTruthy();
+ }));
+
+ it("should not signal $animateCss to apply the classes early when animation is structural", inject(function() {
+ driver({ element: element });
+ expect(capturedAnimation[1].applyClassesEarly).toBeFalsy();
+
+ driver({ element: element, structural: true });
+ expect(capturedAnimation[1].applyClassesEarly).toBeFalsy();
+ }));
+
+ it("should only set the event value if the animation is structural", inject(function() {
+ driver({ element: element, structural: true, event: 'superman' });
+ expect(capturedAnimation[1].event).toBe('superman');
+
+ driver({ element: element, event: 'batman' });
+ expect(capturedAnimation[1].event).toBeFalsy();
+ }));
+ });
+
+ describe("anchored animations", function() {
+ var from, to, fromAnimation, toAnimation;
+
+ beforeEach(module(function() {
+ return function($rootElement, $$body) {
+ from = element;
+ to = jqLite('
');
+ fromAnimation = { element: from, event: 'enter' };
+ toAnimation = { element: to, event: 'leave' };
+ $rootElement.append(from);
+ $rootElement.append(to);
+
+ // we need to do this so that style detection works
+ $$body.append($rootElement);
+ };
+ }));
+
+ it("should not return anything if no animation is detected", function() {
+ module(function($provide) {
+ $provide.value('$animateCss', function() {
+ return { $$willAnimate: false };
+ });
+ });
+ inject(function() {
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation
+ });
+ expect(runner).toBeFalsy();
+ });
+ });
+
+ it("should return a start method", inject(function() {
+ var animator = driver({
+ from: fromAnimation,
+ to: toAnimation
+ });
+ expect(isFunction(animator.start)).toBeTruthy();
+ }));
+
+ they("should return a runner with a $prop() method which will end the animation",
+ ['end', 'cancel'], function(method) {
+
+ var closeAnimation;
+ module(function($provide) {
+ $provide.factory('$animateCss', function($q, $$AnimateRunner) {
+ return function() {
+ return {
+ $$willAnimate: true,
+ start: function() {
+ return new $$AnimateRunner({
+ end: function() {
+ closeAnimation();
+ }
+ });
+ }
+ };
+ };
+ });
+ });
+
+ inject(function() {
+ var animator = driver({
+ from: fromAnimation,
+ to: toAnimation
+ });
+
+ var animationClosed = false;
+ closeAnimation = function() {
+ animationClosed = true;
+ };
+
+ var runner = animator.start();
+
+ expect(isFunction(runner[method])).toBe(true);
+ runner[method]();
+ expect(animationClosed).toBe(true);
+ });
+ });
+
+ it("should end the animation for each of the from and to elements as well as all the anchors", function() {
+ var closeLog = {};
+ module(function($provide) {
+ $provide.factory('$animateCss', function($q, $$AnimateRunner) {
+ return function(element, options) {
+ var type = options.event || 'anchor';
+ closeLog[type] = closeLog[type] || [];
+ return {
+ $$willAnimate: true,
+ start: function() {
+ return new $$AnimateRunner({
+ end: function() {
+ closeLog[type].push(element);
+ }
+ });
+ }
+ };
+ };
+ });
+ });
+
+ inject(function() {
+ //we'll just use one animation to make the test smaller
+ var anchorAnimation = {
+ 'in': jqLite('
'),
+ 'out': jqLite('
')
+ };
+
+ fromAnimation.structural = true;
+ fromAnimation.element.append(anchorAnimation['out']);
+ toAnimation.structural = true;
+ toAnimation.element.append(anchorAnimation['in']);
+
+ var animator = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [
+ anchorAnimation,
+ anchorAnimation,
+ anchorAnimation
+ ]
+ });
+
+ var runner = animator.start();
+ runner.end();
+
+ expect(closeLog.enter[0]).toEqual(fromAnimation.element);
+ expect(closeLog.leave[0]).toEqual(toAnimation.element);
+ expect(closeLog.anchor.length).toBe(3);
+ });
+ });
+
+ it("should render an animation on both the from and to elements", inject(function() {
+ captureFn = function(element, details) {
+ element.addClass(details.event);
+ };
+
+ fromAnimation.structural = true;
+ toAnimation.structural = true;
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation
+ });
+
+ expect(captureLog.length).toBe(2);
+ expect(fromAnimation.element).toHaveClass('enter');
+ expect(toAnimation.element).toHaveClass('leave');
+ }));
+
+ it("should start the animations on the from and to elements in parallel", function() {
+ var animationLog = [];
+ module(function($provide) {
+ $provide.factory('$animateCss', function($$AnimateRunner) {
+ return function(element, details) {
+ return {
+ $$willAnimate: true,
+ start: function() {
+ animationLog.push([element, details.event]);
+ return new $$AnimateRunner();
+ }
+ };
+ };
+ });
+ });
+ inject(function() {
+ fromAnimation.structural = true;
+ toAnimation.structural = true;
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation
+ });
+
+ expect(animationLog.length).toBe(0);
+ runner.start();
+ expect(animationLog).toEqual([
+ [fromAnimation.element, 'enter'],
+ [toAnimation.element, 'leave']
+ ]);
+ });
+ });
+
+ it("should start an animation for each anchor", inject(function() {
+ var o1 = jqLite('
');
+ from.append(o1);
+ var o2 = jqLite('
');
+ from.append(o2);
+ var o3 = jqLite('
');
+ from.append(o3);
+
+ var i1 = jqLite('
');
+ to.append(i1);
+ var i2 = jqLite('
');
+ to.append(i2);
+ var i3 = jqLite('
');
+ to.append(i3);
+
+ var anchors = [
+ { 'out': o1, 'in': i1, classes: 'red' },
+ { 'out': o2, 'in': i2, classes: 'blue' },
+ { 'out': o2, 'in': i2, classes: 'green' }
+ ];
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: anchors
+ });
+
+ expect(captureLog.length).toBe(5);
+ }));
+
+ it("should create a clone of the starting element for each anchor animation", inject(function() {
+ var o1 = jqLite('
');
+ from.append(o1);
+ var o2 = jqLite('
');
+ from.append(o2);
+
+ var i1 = jqLite('
');
+ to.append(i1);
+ var i2 = jqLite('
');
+ to.append(i2);
+
+ var anchors = [
+ { 'out': o1, 'in': i1 },
+ { 'out': o2, 'in': i2 }
+ ];
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: anchors
+ });
+
+ var a2 = captureLog.pop().element;
+ var a1 = captureLog.pop().element;
+
+ expect(a1).not.toEqual(o1);
+ expect(a1.attr('class')).toMatch(/\bout1\b/);
+ expect(a2).not.toEqual(o2);
+ expect(a2.attr('class')).toMatch(/\bout2\b/);
+ }));
+
+ it("should create a clone of the starting element and place it at the end of the $rootElement container",
+ inject(function($rootElement) {
+
+ //stick some garbage into the rootElement
+ $rootElement.append(jqLite('
'));
+ $rootElement.append(jqLite('
'));
+ $rootElement.append(jqLite('
'));
+
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'in': fromAnchor,
+ 'out': toAnchor
+ }]
+ });
+
+ var anchor = captureLog.pop().element;
+ var anchorNode = anchor[0];
+ var contents = $rootElement.contents();
+
+ expect(contents.length).toBeGreaterThan(1);
+ expect(contents[contents.length - 1]).toEqual(anchorNode);
+ }));
+
+ it("should first do an addClass('ng-anchor-out') animation on the cloned anchor", inject(function($rootElement) {
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ });
+
+ var anchorDetails = captureLog.pop().args[1];
+ expect(anchorDetails.addClass).toBe('ng-anchor-out');
+ expect(anchorDetails.event).toBeFalsy();
+ }));
+
+ it("should then do an addClass('ng-anchor-in') animation on the cloned anchor and remove the old class",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ captureLog.pop().runner.end();
+ $$rAF.flush();
+
+ var anchorDetails = captureLog.pop().args[1];
+ expect(anchorDetails.removeClass.trim()).toBe('ng-anchor-out');
+ expect(anchorDetails.addClass.trim()).toBe('ng-anchor-in');
+ expect(anchorDetails.event).toBeFalsy();
+ }));
+
+ they("should only fire the ng-anchor-$prop animation if only a $prop animation is defined",
+ ['out', 'in'], function(direction) {
+
+ var expectedClass = 'ng-anchor-' + direction;
+ var animationStarted;
+ var runner;
+
+ module(function($provide) {
+ $provide.factory('$animateCss', function($$AnimateRunner) {
+ return function(element, options) {
+ var addClass = (options.addClass || '').trim();
+ return {
+ $$willAnimate: addClass === expectedClass,
+ start: function() {
+ animationStarted = addClass;
+ return runner = new $$AnimateRunner();
+ }
+ };
+ };
+ });
+ });
+
+ inject(function($rootElement, $$rAF) {
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ var complete = false;
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start().done(function() {
+ complete = true;
+ });
+
+ expect(animationStarted).toBe(expectedClass);
+ runner.end();
+ $$rAF.flush();
+ expect(complete).toBe(true);
+ });
+ });
+
+
+ it("should provide an explicit delay setting in the options provided to $animateCss for anchor animations",
+ inject(function($rootElement) {
+
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ });
+
+ expect(capturedAnimation[1].delay).toBeTruthy();
+ }));
+
+ it("should begin the anchor animation by seeding the from styles based on where the from anchor element is positioned",
+ inject(function($rootElement) {
+
+ ss.addRule('.starting-element', 'width:200px; height:100px; display:block;');
+
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ });
+
+ var anchorAnimation = captureLog.pop();
+ var anchorElement = anchorAnimation.element;
+ var anchorDetails = anchorAnimation.args[1];
+
+ var fromStyles = anchorDetails.from;
+ expect(int(fromStyles.width)).toBe(200);
+ expect(int(fromStyles.height)).toBe(100);
+ // some browsers have their own body margin defaults
+ expect(int(fromStyles.top)).toBeGreaterThan(499);
+ expect(int(fromStyles.left)).toBeGreaterThan(149);
+ }));
+
+ it("should append a `px` value for all seeded animation styles", inject(function($rootElement, $$rAF) {
+ ss.addRule('.starting-element', 'width:10px; height:20px; display:inline-block;');
+
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ var runner = driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ });
+
+ var anchorAnimation = captureLog.pop();
+ var anchorDetails = anchorAnimation.args[1];
+
+ forEach(anchorDetails.from, function(value) {
+ expect(value.substr(value.length - 2)).toBe('px');
+ });
+
+ // the out animation goes first
+ anchorAnimation.runner.end();
+ $$rAF.flush();
+
+ anchorAnimation = captureLog.pop();
+ anchorDetails = anchorAnimation.args[1];
+
+ forEach(anchorDetails.to, function(value) {
+ expect(value.substr(value.length - 2)).toBe('px');
+ });
+ }));
+
+ it("should then do an removeClass('out') + addClass('in') animation on the cloned anchor",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ // the out animation goes first
+ captureLog.pop().runner.end();
+ $$rAF.flush();
+
+ var anchorDetails = captureLog.pop().args[1];
+ expect(anchorDetails.removeClass).toMatch(/\bout\b/);
+ expect(anchorDetails.addClass).toMatch(/\bin\b/);
+ expect(anchorDetails.event).toBeFalsy();
+ }));
+
+ it("should add the `ng-anchor` class to the cloned anchor element",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ var clonedAnchor = captureLog.pop().element;
+ expect(clonedAnchor).toHaveClass('ng-anchor');
+ }));
+
+ it("should add and remove the `ng-animate-shim` class on the in anchor element during the animation",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ expect(fromAnchor).toHaveClass('ng-animate-shim');
+
+ // the out animation goes first
+ captureLog.pop().runner.end();
+ $$rAF.flush();
+ captureLog.pop().runner.end();
+
+ expect(fromAnchor).not.toHaveClass('ng-animate-shim');
+ }));
+
+ it("should add and remove the `ng-animate-shim` class on the out anchor element during the animation",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ expect(toAnchor).toHaveClass('ng-animate-shim');
+
+ // the out animation goes first
+ captureLog.pop().runner.end();
+ $$rAF.flush();
+
+ expect(toAnchor).toHaveClass('ng-animate-shim');
+ captureLog.pop().runner.end();
+
+ expect(toAnchor).not.toHaveClass('ng-animate-shim');
+ }));
+
+ it("should create the cloned anchor with all of the classes from the from anchor element",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ var addedClasses = captureLog.pop().element.attr('class').split(' ');
+ expect(hasAll(addedClasses, ['yes', 'no', 'maybe'])).toBe(true);
+ }));
+
+ it("should remove the classes of the starting anchor from the cloned anchor node during the in animation and also add the classes of the destination anchor within the same animation",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ // the out animation goes first
+ captureLog.pop().runner.end();
+ $$rAF.flush();
+
+ var anchorDetails = captureLog.pop().args[1];
+ var removedClasses = anchorDetails.removeClass.split(' ');
+ var addedClasses = anchorDetails.addClass.split(' ');
+
+ expect(hasAll(removedClasses, ['yes', 'no', 'maybe'])).toBe(true);
+ expect(hasAll(addedClasses, ['why', 'ok', 'so-what'])).toBe(true);
+ }));
+
+ it("should not attempt to add/remove any classes that contain a `ng-` prefix",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ // the out animation goes first
+ captureLog.pop().runner.end();
+ $$rAF.flush();
+
+ var inAnimation = captureLog.pop();
+ var details = inAnimation.args[1];
+
+ var addedClasses = details.addClass.split(' ');
+ var removedClasses = details.removeClass.split(' ');
+
+ expect(addedClasses).not.toContain('ng-foo');
+ expect(addedClasses).not.toContain('ng-bar');
+
+ expect(removedClasses).not.toContain('ng-yes');
+ expect(removedClasses).not.toContain('ng-no');
+ }));
+
+ it("should not remove any shared CSS classes between the starting and destination anchor element during the in animation",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ // the out animation goes first
+ captureLog.pop().runner.end();
+ $$rAF.flush();
+
+ var inAnimation = captureLog.pop();
+ var clonedAnchor = inAnimation.element;
+ var details = inAnimation.args[1];
+
+ var addedClasses = details.addClass.split(' ');
+ var removedClasses = details.removeClass.split(' ');
+
+ expect(hasAll(addedClasses, ['brown', 'black'])).toBe(true);
+ expect(hasAll(removedClasses, ['green'])).toBe(true);
+
+ expect(addedClasses).not.toContain('red');
+ expect(addedClasses).not.toContain('blue');
+
+ expect(removedClasses).not.toContain('brown');
+ expect(removedClasses).not.toContain('black');
+
+ expect(removedClasses).not.toContain('red');
+ expect(removedClasses).not.toContain('blue');
+
+ inAnimation.runner.end();
+
+ expect(clonedAnchor).toHaveClass('red');
+ expect(clonedAnchor).toHaveClass('blue');
+ }));
+
+ it("should continue the anchor animation by seeding the to styles based on where the final anchor element will be positioned",
+ inject(function($rootElement, $$rAF) {
+ ss.addRule('.ending-element', 'width:9999px; height:6666px; display:inline-block;');
+
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ captureLog.pop().runner.end();
+ $$rAF.flush();
+
+ var anchorAnimation = captureLog.pop();
+ var anchorElement = anchorAnimation.element;
+ var anchorDetails = anchorAnimation.args[1];
+
+ var toStyles = anchorDetails.to;
+ expect(int(toStyles.width)).toBe(9999);
+ expect(int(toStyles.height)).toBe(6666);
+ // some browsers have their own body margin defaults
+ expect(int(toStyles.top)).toBeGreaterThan(300);
+ expect(int(toStyles.left)).toBeGreaterThan(20);
+ }));
+
+ it("should remove the cloned anchor node from the DOM once the 'in' animation is complete",
+ inject(function($rootElement, $$rAF) {
+
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start();
+
+ // the out animation goes first
+ var inAnimation = captureLog.pop();
+ var clonedAnchor = inAnimation.element;
+ expect(clonedAnchor.parent().length).toBe(1);
+ inAnimation.runner.end();
+ $$rAF.flush();
+
+ // now the in animation completes
+ expect(clonedAnchor.parent().length).toBe(1);
+ captureLog.pop().runner.end();
+
+ expect(clonedAnchor.parent().length).toBe(0);
+ }));
+
+ it("should pass the provided domOperation into $animateCss to be run right after the element is animated if a leave animation is present",
+ inject(function($rootElement, $$rAF) {
+
+ toAnimation.structural = true;
+ toAnimation.event = 'enter';
+ toAnimation.options = {};
+
+ fromAnimation.structural = true;
+ fromAnimation.event = 'leave';
+ fromAnimation.options = {};
+
+ var leaveOp = function() { };
+ fromAnimation.options.domOperation = leaveOp;
+
+ driver({
+ from: fromAnimation,
+ to: toAnimation
+ }).start();
+
+ var leaveAnimation = captureLog.shift();
+ var enterAnimation = captureLog.shift();
+
+ expect(leaveAnimation.args[1].onDone).toBe(leaveOp);
+ expect(enterAnimation.args[1].onDone).toBeUndefined();
+ }));
+
+ it("should fire the returned runner promise when the from, to and anchor animations are all complete",
+ inject(function($rootElement, $rootScope, $$rAF) {
+
+ ss.addRule('.ending-element', 'width:9999px; height:6666px; display:inline-block;');
+
+ var fromAnchor = jqLite('
');
+ from.append(fromAnchor);
+
+ var toAnchor = jqLite('
');
+ to.append(toAnchor);
+
+ $rootElement.append(fromAnchor);
+ $rootElement.append(toAnchor);
+
+ var completed = false;
+ driver({
+ from: fromAnimation,
+ to: toAnimation,
+ anchors: [{
+ 'out': fromAnchor,
+ 'in': toAnchor
+ }]
+ }).start().then(function() {
+ completed = true;
+ });
+
+ captureLog.pop().runner.end(); //from
+ captureLog.pop().runner.end(); //to
+ captureLog.pop().runner.end(); //anchor(out)
+ captureLog.pop().runner.end(); //anchor(in)
+ $$rAF.flush();
+ $rootScope.$digest();
+
+ expect(completed).toBe(true);
+ }));
+ });
+ });
+});
diff --git a/test/ngAnimate/animateCssSpec.js b/test/ngAnimate/animateCssSpec.js
new file mode 100644
index 000000000000..b83c77c5046b
--- /dev/null
+++ b/test/ngAnimate/animateCssSpec.js
@@ -0,0 +1,2705 @@
+'use strict';
+
+describe("ngAnimate $animateCss", function() {
+
+ beforeEach(module('ngAnimate'));
+
+ function assertAnimationRunning(element, not) {
+ var className = element.attr('class');
+ var regex = /\b\w+-active\b/;
+ not ? expect(className).toMatch(regex)
+ : expect(className).not.toMatch(regex);
+ }
+
+ var fakeStyle = {
+ color: 'blue'
+ };
+
+ var ss, prefix, triggerAnimationStartFrame;
+ beforeEach(module(function() {
+ return function($document, $window, $sniffer, $$rAF) {
+ prefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-';
+ ss = createMockStyleSheet($document, $window);
+ triggerAnimationStartFrame = function() {
+ $$rAF.flush();
+ };
+ };
+ }));
+
+ afterEach(function() {
+ if (ss) {
+ ss.destroy();
+ }
+ });
+
+ it("should return false if neither transitions or keyframes are supported by the browser",
+ inject(function($animateCss, $sniffer, $rootElement, $$body) {
+
+ var animator;
+ var element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ $sniffer.transitions = $sniffer.animations = false;
+ animator = $animateCss(element, {
+ duration: 10,
+ to: { 'background': 'red' }
+ });
+ expect(animator.$$willAnimate).toBeFalsy();
+ }));
+
+ describe('when active', function() {
+ if (!browserSupportsCssAnimations()) return;
+
+ it("should silently quit the animation and not throw when an element has no parent during preparation",
+ inject(function($animateCss, $$rAF, $rootScope, $document, $rootElement) {
+
+ var element = jqLite('
');
+ expect(function() {
+ $animateCss(element, {
+ duration: 1000,
+ event: 'fake',
+ to: fakeStyle
+ }).start();
+ }).not.toThrow();
+
+ expect(element).not.toHaveClass('fake');
+ triggerAnimationStartFrame();
+ expect(element).not.toHaveClass('fake-active');
+ }));
+
+ it("should silently quit the animation and not throw when an element has no parent before starting",
+ inject(function($animateCss, $$rAF, $rootScope, $document, $rootElement) {
+
+ var element = jqLite('
');
+ jqLite($document[0].body).append($rootElement);
+ $rootElement.append(element);
+
+ $animateCss(element, {
+ duration: 1000,
+ addClass: 'wait-for-it',
+ to: fakeStyle
+ }).start();
+
+ element.remove();
+
+ expect(function() {
+ triggerAnimationStartFrame();
+ }).not.toThrow();
+ }));
+
+ describe("rAF usage", function() {
+ it("should buffer all requests into a single requestAnimationFrame call",
+ inject(function($animateCss, $$rAF, $rootScope, $$body, $rootElement) {
+
+ $$body.append($rootElement);
+
+ var count = 0;
+ var runners = [];
+ function makeRequest() {
+ var element = jqLite('
');
+ $rootElement.append(element);
+ var runner = $animateCss(element, { duration: 5, to: fakeStyle }).start();
+ runner.then(function() {
+ count++;
+ });
+ runners.push(runner);
+ }
+
+ makeRequest();
+ makeRequest();
+ makeRequest();
+
+ expect(count).toBe(0);
+
+ triggerAnimationStartFrame();
+ forEach(runners, function(runner) {
+ runner.end();
+ });
+
+ $rootScope.$digest();
+ expect(count).toBe(3);
+ }));
+
+ it("should cancel previous requests to rAF to avoid premature flushing", function() {
+ var count = 0;
+ module(function($provide) {
+ $provide.value('$$rAF', function() {
+ return function cancellationFn() {
+ count++;
+ };
+ });
+ });
+ inject(function($animateCss, $$rAF, $$body, $rootElement) {
+ $$body.append($rootElement);
+
+ function makeRequest() {
+ var element = jqLite('
');
+ $rootElement.append(element);
+ $animateCss(element, { duration: 5, to: fakeStyle }).start();
+ }
+
+ makeRequest();
+ makeRequest();
+ makeRequest();
+ expect(count).toBe(2);
+ });
+ });
+ });
+
+ describe("animator and runner", function() {
+ var animationDuration = 5;
+ var element, animator;
+ beforeEach(inject(function($animateCss, $rootElement, $$body) {
+ element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ animator = $animateCss(element, {
+ event: 'enter',
+ structural: true,
+ duration: animationDuration,
+ to: fakeStyle
+ });
+ }));
+
+ it('should expose start and end functions for the animator object', inject(function() {
+ expect(typeof animator.start).toBe('function');
+ expect(typeof animator.end).toBe('function');
+ }));
+
+ it('should expose end, cancel, resume and pause methods on the runner object', inject(function() {
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+
+ expect(typeof runner.end).toBe('function');
+ expect(typeof runner.cancel).toBe('function');
+ expect(typeof runner.resume).toBe('function');
+ expect(typeof runner.pause).toBe('function');
+ }));
+
+ it('should start the animation', inject(function() {
+ expect(element).not.toHaveClass('ng-enter-active');
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element).toHaveClass('ng-enter-active');
+ }));
+
+ it('should end the animation when called from the animator object', inject(function() {
+ animator.start();
+ triggerAnimationStartFrame();
+
+ animator.end();
+ expect(element).not.toHaveClass('ng-enter-active');
+ }));
+
+ it('should end the animation when called from the runner object', inject(function() {
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+ runner.end();
+ expect(element).not.toHaveClass('ng-enter-active');
+ }));
+
+ it('should permanently close the animation if closed before the next rAF runs', inject(function() {
+ var runner = animator.start();
+ runner.end();
+
+ triggerAnimationStartFrame();
+ expect(element).not.toHaveClass('ng-enter-active');
+ }));
+
+ it('should return a runner object at the start of the animation that contains a `then` method',
+ inject(function($rootScope) {
+
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+
+ expect(isPromiseLike(runner)).toBeTruthy();
+
+ var resolved;
+ runner.then(function() {
+ resolved = true;
+ });
+
+ runner.end();
+ $rootScope.$digest();
+ expect(resolved).toBeTruthy();
+ }));
+
+ it('should cancel the animation and reject', inject(function($rootScope) {
+ var rejected;
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+
+ runner.then(noop, function() {
+ rejected = true;
+ });
+
+ runner.cancel();
+ $rootScope.$digest();
+ expect(rejected).toBeTruthy();
+ }));
+
+ it('should run pause, but not effect the transition animation', inject(function() {
+ var blockingDelay = '-' + animationDuration + 's';
+
+ expect(element.css('transition-delay')).toEqual(blockingDelay);
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element.css('transition-delay')).not.toEqual(blockingDelay);
+ runner.pause();
+ expect(element.css('transition-delay')).not.toEqual(blockingDelay);
+ }));
+
+ it('should pause the transition, have no effect, but not end it', inject(function() {
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+
+ runner.pause();
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now(), elapsedTime: 5 });
+
+ expect(element).toHaveClass('ng-enter-active');
+ }));
+
+ it('should resume the animation', inject(function() {
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+
+ runner.pause();
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now(), elapsedTime: 5 });
+
+ expect(element).toHaveClass('ng-enter-active');
+ runner.resume();
+
+ expect(element).not.toHaveClass('ng-enter-active');
+ }));
+
+ it('should pause and resume a keyframe animation using animation-play-state',
+ inject(function($animateCss) {
+
+ element.attr('style', '');
+ ss.addRule('.ng-enter', '-webkit-animation:1.5s keyframe_animation;' +
+ 'animation:1.5s keyframe_animation;');
+
+ animator = $animateCss(element, {
+ event: 'enter',
+ structural: true
+ });
+
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+
+ runner.pause();
+ expect(element.css(prefix + 'animation-play-state')).toEqual('paused');
+ runner.resume();
+ expect(element.attr('style')).toBeFalsy();
+ }));
+
+ it('should remove the animation-play-state style if the animation is closed',
+ inject(function($animateCss) {
+
+ element.attr('style', '');
+ ss.addRule('.ng-enter', '-webkit-animation:1.5s keyframe_animation;' +
+ 'animation:1.5s keyframe_animation;');
+
+ animator = $animateCss(element, {
+ event: 'enter',
+ structural: true
+ });
+
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+
+ runner.pause();
+ expect(element.css(prefix + 'animation-play-state')).toEqual('paused');
+ runner.end();
+ expect(element.attr('style')).toBeFalsy();
+ }));
+ });
+
+ describe("CSS", function() {
+ describe("detected styles", function() {
+ var element, options;
+
+ function assertAnimationComplete(bool) {
+ var assert = expect(element);
+ if (bool) {
+ assert = assert.not;
+ }
+ assert.toHaveClass('ng-enter');
+ assert.toHaveClass('ng-enter-active');
+ }
+
+ function keyframeProgress(element, duration, delay) {
+ browserTrigger(element, 'animationend',
+ { timeStamp: Date.now() + ((delay || 1) * 1000), elapsedTime: duration });
+ }
+
+ function transitionProgress(element, duration, delay) {
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + ((delay || 1) * 1000), elapsedTime: duration });
+ }
+
+ beforeEach(inject(function($rootElement, $$body) {
+ element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+ options = { event: 'enter', structural: true };
+ }));
+
+ it("should always return an object even if no animation is detected",
+ inject(function($animateCss) {
+
+ ss.addRule('.some-animation', 'background:red;');
+
+ element.addClass('some-animation');
+ var animator = $animateCss(element, options);
+
+ expect(animator).toBeTruthy();
+ expect(isFunction(animator.start)).toBeTruthy();
+ expect(animator.end).toBeTruthy();
+ expect(animator.$$willAnimate).toBe(false);
+ }));
+
+ it("should close the animation immediately, but still return an animator object if no animation is detected",
+ inject(function($animateCss) {
+
+ ss.addRule('.another-fake-animation', 'background:blue;');
+
+ element.addClass('another-fake-animation');
+ var animator = $animateCss(element, {
+ event: 'enter',
+ structural: true
+ });
+
+ expect(element).not.toHaveClass('ng-enter');
+ expect(isFunction(animator.start)).toBeTruthy();
+ }));
+
+ they("should close the animation, but still accept $prop callbacks if no animation is detected",
+ ['done', 'then'], function(method) {
+
+ inject(function($animateCss, $$rAF, $rootScope) {
+ ss.addRule('.the-third-fake-animation', 'background:green;');
+
+ element.addClass('another-fake-animation');
+ var animator = $animateCss(element, {
+ event: 'enter',
+ structural: true
+ });
+
+ var done = false;
+ animator.start()[method](function() {
+ done = true;
+ });
+
+ expect(done).toBe(false);
+ $$rAF.flush();
+ if (method === 'then') {
+ $rootScope.$digest();
+ }
+ expect(done).toBe(true);
+ });
+ });
+
+ they("should close the animation, but still accept recognize runner.$prop if no animation is detected",
+ ['done(cancel)', 'catch'], function(method) {
+
+ inject(function($animateCss, $$rAF, $rootScope) {
+ ss.addRule('.the-third-fake-animation', 'background:green;');
+
+ element.addClass('another-fake-animation');
+ var animator = $animateCss(element, {
+ event: 'enter',
+ structural: true
+ });
+
+ var cancelled = false;
+ var runner = animator.start();
+
+ if (method === 'catch') {
+ runner.catch(function() {
+ cancelled = true;
+ });
+ } else {
+ runner.done(function(status) {
+ cancelled = status === false;
+ });
+ }
+
+ expect(cancelled).toBe(false);
+ runner.cancel();
+
+ if (method === 'catch') {
+ $rootScope.$digest();
+ }
+ expect(cancelled).toBe(true);
+ });
+ });
+
+ it("should use the highest transition duration value detected in the CSS class", inject(function($animateCss) {
+ ss.addRule('.ng-enter', 'transition:1s linear all;' +
+ 'transition-duration:10s, 15s, 20s;');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ transitionProgress(element, 10);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 15);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 20);
+ assertAnimationComplete(true);
+ }));
+
+ it("should use the highest transition delay value detected in the CSS class", inject(function($animateCss) {
+ ss.addRule('.ng-enter', 'transition:1s linear all;' +
+ 'transition-delay:10s, 15s, 20s;');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ transitionProgress(element, 1, 10);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 1, 15);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 1, 20);
+ assertAnimationComplete(true);
+ }));
+
+ it("should only close when both the animation delay and duration have passed",
+ inject(function($animateCss) {
+
+ ss.addRule('.ng-enter', 'transition:10s 5s linear all;');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+ transitionProgress(element, 10, 2);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 9, 6);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 10, 5);
+ assertAnimationComplete(true);
+ }));
+
+ it("should use the highest keyframe duration value detected in the CSS class", inject(function($animateCss) {
+ ss.addRule('.ng-enter', 'animation:animation 1s, animation 2s, animation 3s;' +
+ '-webkit-animation:animation 1s, animation 2s, animation 3s;');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ keyframeProgress(element, 1);
+ assertAnimationComplete(false);
+
+ keyframeProgress(element, 2);
+ assertAnimationComplete(false);
+
+ keyframeProgress(element, 3);
+ assertAnimationComplete(true);
+ }));
+
+ it("should use the highest keyframe delay value detected in the CSS class", inject(function($animateCss) {
+ ss.addRule('.ng-enter', 'animation:animation 1s 2s, animation 1s 10s, animation 1s 1000ms;' +
+ '-webkit-animation:animation 1s 2s, animation 1s 10s, animation 1s 1000ms;');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ keyframeProgress(element, 1, 1);
+ assertAnimationComplete(false);
+
+ keyframeProgress(element, 1, 2);
+ assertAnimationComplete(false);
+
+ keyframeProgress(element, 1, 10);
+ assertAnimationComplete(true);
+ }));
+
+ it("should use the highest keyframe duration value detected in the CSS class with respect to the animation-iteration-count property", inject(function($animateCss) {
+ ss.addRule('.ng-enter',
+ 'animation:animation 1s 2s 3, animation 1s 10s 2, animation 1s 1000ms infinite;' +
+ '-webkit-animation:animation 1s 2s 3, animation 1s 10s 2, animation 1s 1000ms infinite;');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ keyframeProgress(element, 1, 1);
+ assertAnimationComplete(false);
+
+ keyframeProgress(element, 1, 2);
+ assertAnimationComplete(false);
+
+ keyframeProgress(element, 3, 10);
+ assertAnimationComplete(true);
+ }));
+
+ it("should use the highest duration value when both transitions and keyframes are used", inject(function($animateCss) {
+ ss.addRule('.ng-enter', 'transition:1s linear all;' +
+ 'transition-duration:10s, 15s, 20s;' +
+ 'animation:animation 1s, animation 2s, animation 3s 0s 7;' +
+ '-webkit-animation:animation 1s, animation 2s, animation 3s 0s 7;');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ transitionProgress(element, 10);
+ keyframeProgress(element, 10);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 15);
+ keyframeProgress(element, 15);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 20);
+ keyframeProgress(element, 20);
+ assertAnimationComplete(false);
+
+ // 7 * 3 = 21
+ transitionProgress(element, 21);
+ keyframeProgress(element, 21);
+ assertAnimationComplete(true);
+ }));
+
+ it("should use the highest delay value when both transitions and keyframes are used", inject(function($animateCss) {
+ ss.addRule('.ng-enter', 'transition:1s linear all;' +
+ 'transition-delay:10s, 15s, 20s;' +
+ 'animation:animation 1s 2s, animation 1s 16s, animation 1s 19s;' +
+ '-webkit-animation:animation 1s 2s, animation 1s 16s, animation 1s 19s;');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ transitionProgress(element, 1, 10);
+ keyframeProgress(element, 1, 10);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 1, 16);
+ keyframeProgress(element, 1, 16);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 1, 19);
+ keyframeProgress(element, 1, 19);
+ assertAnimationComplete(false);
+
+ transitionProgress(element, 1, 20);
+ keyframeProgress(element, 1, 20);
+ assertAnimationComplete(true);
+ }));
+ });
+
+ describe("staggering", function() {
+ it("should apply a stagger based when an active ng-EVENT-stagger class with a transition-delay is detected",
+ inject(function($animateCss, $$body, $rootElement, $timeout) {
+
+ $$body.append($rootElement);
+
+ ss.addRule('.ng-enter-stagger', 'transition-delay:0.2s');
+ ss.addRule('.ng-enter', 'transition:2s linear all');
+
+ var elements = [];
+ var i;
+ var elm;
+
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('
');
+ elements.push(elm);
+ $rootElement.append(elm);
+
+ $animateCss(elm, { event: 'enter', structural: true }).start();
+ expect(elm).not.toHaveClass('ng-enter-stagger');
+ expect(elm).toHaveClass('ng-enter');
+ }
+
+ triggerAnimationStartFrame();
+
+ expect(elements[0]).toHaveClass('ng-enter-active');
+ for (i = 1; i < 5; i++) {
+ elm = elements[i];
+
+ expect(elm).not.toHaveClass('ng-enter-active');
+ $timeout.flush(200);
+ expect(elm).toHaveClass('ng-enter-active');
+
+ browserTrigger(elm, 'transitionend',
+ { timeStamp: Date.now() + 1000, elapsedTime: 2 });
+
+ expect(elm).not.toHaveClass('ng-enter');
+ expect(elm).not.toHaveClass('ng-enter-active');
+ expect(elm).not.toHaveClass('ng-enter-stagger');
+ }
+ }));
+
+ it("should apply a stagger based when for all provided addClass/removeClass CSS classes",
+ inject(function($animateCss, $$body, $rootElement, $timeout) {
+
+ $$body.append($rootElement);
+
+ ss.addRule('.red-add-stagger,' +
+ '.blue-remove-stagger,' +
+ '.green-add-stagger', 'transition-delay:0.2s');
+
+ ss.addRule('.red-add,' +
+ '.blue-remove,' +
+ '.green-add', 'transition:2s linear all');
+
+ var elements = [];
+ var i;
+ var elm;
+
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('
');
+ elements.push(elm);
+ $rootElement.append(elm);
+
+ $animateCss(elm, {
+ addClass: 'red green',
+ removeClass: 'blue'
+ }).start();
+ }
+
+ triggerAnimationStartFrame();
+ for (i = 0; i < 5; i++) {
+ elm = elements[i];
+
+ expect(elm).not.toHaveClass('red-add-stagger');
+ expect(elm).not.toHaveClass('green-add-stagger');
+ expect(elm).not.toHaveClass('blue-remove-stagger');
+
+ expect(elm).toHaveClass('red-add');
+ expect(elm).toHaveClass('green-add');
+ expect(elm).toHaveClass('blue-remove');
+ }
+
+ expect(elements[0]).toHaveClass('red-add-active');
+ expect(elements[0]).toHaveClass('green-add-active');
+ expect(elements[0]).toHaveClass('blue-remove-active');
+ for (i = 1; i < 5; i++) {
+ elm = elements[i];
+
+ expect(elm).not.toHaveClass('red-add-active');
+ expect(elm).not.toHaveClass('green-add-active');
+ expect(elm).not.toHaveClass('blue-remove-active');
+
+ $timeout.flush(200);
+
+ expect(elm).toHaveClass('red-add-active');
+ expect(elm).toHaveClass('green-add-active');
+ expect(elm).toHaveClass('blue-remove-active');
+
+ browserTrigger(elm, 'transitionend',
+ { timeStamp: Date.now() + 1000, elapsedTime: 2 });
+
+ expect(elm).not.toHaveClass('red-add-active');
+ expect(elm).not.toHaveClass('green-add-active');
+ expect(elm).not.toHaveClass('blue-remove-active');
+
+ expect(elm).not.toHaveClass('red-add-stagger');
+ expect(elm).not.toHaveClass('green-add-stagger');
+ expect(elm).not.toHaveClass('blue-remove-stagger');
+ }
+ }));
+
+ it("should block the transition animation between start and animate when staggered",
+ inject(function($animateCss, $$body, $rootElement) {
+
+ $$body.append($rootElement);
+
+ ss.addRule('.ng-enter-stagger', 'transition-delay:0.2s');
+ ss.addRule('.ng-enter', 'transition:2s linear all;');
+
+ var element;
+ var i;
+ var elms = [];
+
+ for (i = 0; i < 5; i++) {
+ element = jqLite('
');
+ $rootElement.append(element);
+
+ $animateCss(element, { event: 'enter', structural: true }).start();
+ elms.push(element);
+ }
+
+ triggerAnimationStartFrame();
+ for (i = 0; i < 5; i++) {
+ element = elms[i];
+ if (i === 0) {
+ expect(element.attr('style')).toBeFalsy();
+ } else {
+ expect(element.css('transition-delay')).toContain('-2s');
+ }
+ }
+ }));
+
+ it("should block (pause) the keyframe animation between start and animate when staggered",
+ inject(function($animateCss, $$body, $rootElement) {
+
+ $$body.append($rootElement);
+
+ ss.addRule('.ng-enter-stagger', prefix + 'animation-delay:0.2s');
+ ss.addRule('.ng-enter', prefix + 'animation:my_animation 2s;');
+
+ var i, element, elements = [];
+ for (i = 0; i < 5; i++) {
+ element = jqLite('
');
+ $rootElement.append(element);
+
+ $animateCss(element, { event: 'enter', structural: true }).start();
+ elements.push(element);
+ }
+
+ triggerAnimationStartFrame();
+
+ for (i = 0; i < 5; i++) {
+ element = elements[i];
+ if (i === 0) { // the first element is always run right away
+ expect(element.attr('style')).toBeFalsy();
+ } else {
+ expect(element.css(prefix + 'animation-play-state')).toBe('paused');
+ }
+ }
+ }));
+
+ it("should not apply a stagger if the transition delay value is inherited from a earlier CSS class",
+ inject(function($animateCss, $$body, $rootElement) {
+
+ $$body.append($rootElement);
+
+ ss.addRule('.transition-animation', 'transition:2s 5s linear all;');
+
+ for (var i = 0; i < 5; i++) {
+ var element = jqLite('
');
+ $rootElement.append(element);
+
+ $animateCss(element, { event: 'enter', structural: true }).start();
+ triggerAnimationStartFrame();
+
+
+ expect(element).toHaveClass('ng-enter-active');
+ }
+ }));
+
+ it("should apply a stagger only if the transition duration value is zero when inherited from a earlier CSS class",
+ inject(function($animateCss, $$body, $rootElement) {
+
+ $$body.append($rootElement);
+
+ ss.addRule('.transition-animation', 'transition:2s 5s linear all;');
+ ss.addRule('.transition-animation.ng-enter-stagger',
+ 'transition-duration:0s; transition-delay:0.2s;');
+
+ var element, i, elms = [];
+ for (i = 0; i < 5; i++) {
+ element = jqLite('
');
+ $rootElement.append(element);
+
+ elms.push(element);
+ $animateCss(element, { event: 'enter', structural: true }).start();
+ }
+
+ triggerAnimationStartFrame();
+ for (i = 1; i < 5; i++) {
+ element = elms[i];
+ expect(element).not.toHaveClass('ng-enter-active');
+ }
+ }));
+
+
+ it("should ignore animation staggers if only transition animations were detected",
+ inject(function($animateCss, $$body, $rootElement) {
+
+ $$body.append($rootElement);
+
+ ss.addRule('.ng-enter-stagger', prefix + 'animation-delay:0.2s');
+ ss.addRule('.transition-animation', 'transition:2s 5s linear all;');
+
+ for (var i = 0; i < 5; i++) {
+ var element = jqLite('
');
+ $rootElement.append(element);
+
+ $animateCss(element, { event: 'enter', structural: true }).start();
+ triggerAnimationStartFrame();
+
+
+ expect(element).toHaveClass('ng-enter-active');
+ }
+ }));
+
+ it("should ignore transition staggers if only keyframe animations were detected",
+ inject(function($animateCss, $$body, $rootElement) {
+
+ $$body.append($rootElement);
+
+ ss.addRule('.ng-enter-stagger', 'transition-delay:0.2s');
+ ss.addRule('.transition-animation', prefix + 'animation:2s 5s my_animation;');
+
+ for (var i = 0; i < 5; i++) {
+ var elm = jqLite('
');
+ $rootElement.append(elm);
+
+ var animator = $animateCss(elm, { event: 'enter', structural: true }).start();
+ triggerAnimationStartFrame();
+
+
+ expect(elm).toHaveClass('ng-enter-active');
+ }
+ }));
+
+ it("should start on the highest stagger value if both transition and keyframe staggers are used together",
+ inject(function($animateCss, $$body, $rootElement, $timeout, $browser) {
+
+ $$body.append($rootElement);
+
+ ss.addRule('.ng-enter-stagger', 'transition-delay:0.5s;' +
+ prefix + 'animation-delay:1s');
+
+ ss.addRule('.ng-enter', 'transition:10s linear all;' +
+ prefix + 'animation:my_animation 20s');
+
+ var i, elm, elements = [];
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('
');
+ elements.push(elm);
+ $rootElement.append(elm);
+
+ $animateCss(elm, { event: 'enter', structural: true }).start();
+
+ expect(elm).toHaveClass('ng-enter');
+ }
+
+ triggerAnimationStartFrame();
+
+ expect(elements[0]).toHaveClass('ng-enter-active');
+ for (i = 1; i < 5; i++) {
+ elm = elements[i];
+
+ expect(elm).not.toHaveClass('ng-enter-active');
+
+ $timeout.flush(500);
+ expect(elm).not.toHaveClass('ng-enter-active');
+
+ $timeout.flush(500);
+ expect(elm).toHaveClass('ng-enter-active');
+ }
+ }));
+
+ it("should apply the closing timeout ontop of the stagger timeout",
+ inject(function($animateCss, $$body, $rootElement, $timeout, $browser) {
+
+ $$body.append($rootElement);
+
+ ss.addRule('.ng-enter-stagger', 'transition-delay:1s;');
+ ss.addRule('.ng-enter', 'transition:10s linear all;');
+
+ var elm, i, elms = [];
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('
');
+ elms.push(elm);
+ $rootElement.append(elm);
+
+ $animateCss(elm, { event: 'enter', structural: true }).start();
+ triggerAnimationStartFrame();
+ }
+
+ for (i = 1; i < 2; i++) {
+ elm = elms[i];
+ expect(elm).toHaveClass('ng-enter');
+ $timeout.flush(1000);
+ $timeout.flush(15000);
+ expect(elm).not.toHaveClass('ng-enter');
+ }
+ }));
+
+ it("should apply the closing timeout ontop of the stagger timeout with an added delay",
+ inject(function($animateCss, $$body, $rootElement, $timeout, $browser) {
+
+ $$body.append($rootElement);
+
+ ss.addRule('.ng-enter-stagger', 'transition-delay:1s;');
+ ss.addRule('.ng-enter', 'transition:10s linear all; transition-delay:50s;');
+
+ var elm, i, elms = [];
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('
');
+ elms.push(elm);
+ $rootElement.append(elm);
+
+ $animateCss(elm, { event: 'enter', structural: true }).start();
+ triggerAnimationStartFrame();
+ }
+
+ for (i = 1; i < 2; i++) {
+ elm = elms[i];
+ expect(elm).toHaveClass('ng-enter');
+ $timeout.flush(1000);
+ $timeout.flush(65000);
+ expect(elm).not.toHaveClass('ng-enter');
+ }
+ }));
+
+ it("should issue a stagger if a stagger value is provided in the options",
+ inject(function($animateCss, $$body, $rootElement, $timeout) {
+
+ $$body.append($rootElement);
+ ss.addRule('.ng-enter', 'transition:2s linear all');
+
+ var elm, i, elements = [];
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('
');
+ elements.push(elm);
+ $rootElement.append(elm);
+
+ $animateCss(elm, {
+ event: 'enter',
+ structural: true,
+ stagger: 0.5
+ }).start();
+ expect(elm).toHaveClass('ng-enter');
+ }
+
+ triggerAnimationStartFrame();
+
+ expect(elements[0]).toHaveClass('ng-enter-active');
+ for (i = 1; i < 5; i++) {
+ elm = elements[i];
+
+ expect(elm).not.toHaveClass('ng-enter-active');
+ $timeout.flush(500);
+ expect(elm).toHaveClass('ng-enter-active');
+
+ browserTrigger(elm, 'transitionend',
+ { timeStamp: Date.now() + 1000, elapsedTime: 2 });
+
+ expect(elm).not.toHaveClass('ng-enter');
+ expect(elm).not.toHaveClass('ng-enter-active');
+ expect(elm).not.toHaveClass('ng-enter-stagger');
+ }
+ }));
+
+ it("should only add/remove classes once the stagger timeout has passed",
+ inject(function($animateCss, $$body, $rootElement, $timeout) {
+
+ $$body.append($rootElement);
+
+ var element = jqLite('
');
+ $rootElement.append(element);
+
+ $animateCss(element, {
+ addClass: 'red',
+ removeClass: 'green',
+ duration: 5,
+ stagger: 0.5,
+ staggerIndex: 3
+ }).start();
+
+ triggerAnimationStartFrame();
+ expect(element).toHaveClass('green');
+ expect(element).not.toHaveClass('red');
+
+ $timeout.flush(1500);
+ expect(element).not.toHaveClass('green');
+ expect(element).toHaveClass('red');
+ }));
+ });
+
+ describe("closing timeout", function() {
+ it("should close off the animation after 150% of the animation time has passed",
+ inject(function($animateCss, $$body, $rootElement, $timeout) {
+
+ ss.addRule('.ng-enter', 'transition:10s linear all;');
+
+ var element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ var animator = $animateCss(element, { event: 'enter', structural: true });
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element).toHaveClass('ng-enter');
+ expect(element).toHaveClass('ng-enter-active');
+
+ $timeout.flush(15000);
+
+ expect(element).not.toHaveClass('ng-enter');
+ expect(element).not.toHaveClass('ng-enter-active');
+ }));
+
+ it("should close off the animation after 150% of the animation time has passed and consider the detected delay value",
+ inject(function($animateCss, $$body, $rootElement, $timeout) {
+
+ ss.addRule('.ng-enter', 'transition:10s linear all; transition-delay:30s;');
+
+ var element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ var animator = $animateCss(element, { event: 'enter', structural: true });
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element).toHaveClass('ng-enter');
+ expect(element).toHaveClass('ng-enter-active');
+
+ $timeout.flush(45000);
+
+ expect(element).not.toHaveClass('ng-enter');
+ expect(element).not.toHaveClass('ng-enter-active');
+ }));
+
+ it("should still resolve the animation once expired",
+ inject(function($animateCss, $$body, $rootElement, $timeout) {
+
+ ss.addRule('.ng-enter', 'transition:10s linear all;');
+
+ var element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ var animator = $animateCss(element, { event: 'enter', structural: true });
+
+ var failed, passed;
+ animator.start().then(function() {
+ passed = true;
+ }, function() {
+ failed = true;
+ });
+
+ triggerAnimationStartFrame();
+ $timeout.flush(15000);
+ expect(passed).toBe(true);
+ }));
+
+ it("should not resolve/reject after passing if the animation completed successfully",
+ inject(function($animateCss, $$body, $rootElement, $timeout, $rootScope) {
+
+ ss.addRule('.ng-enter', 'transition:10s linear all;');
+
+ var element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ var animator = $animateCss(element, { event: 'enter', structural: true });
+
+ var failed, passed;
+ animator.start().then(
+ function() {
+ passed = true;
+ },
+ function() {
+ failed = true;
+ }
+ );
+ triggerAnimationStartFrame();
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 1000, elapsedTime: 10 });
+
+ $rootScope.$digest();
+
+ expect(passed).toBe(true);
+ expect(failed).not.toBe(true);
+
+ $timeout.flush(15000);
+
+ expect(passed).toBe(true);
+ expect(failed).not.toBe(true);
+ }));
+ });
+
+ describe("getComputedStyle", function() {
+ var count;
+ var acceptableTimingsData = {
+ transitionDuration: "10s"
+ };
+
+ beforeEach(module(function($provide) {
+ count = {};
+ $provide.value('$window', extend({}, window, {
+ document: jqLite(window.document),
+ getComputedStyle: function(node) {
+ var key = node.className.indexOf('stagger') >= 0
+ ? 'stagger' : 'normal';
+ count[key] = count[key] || 0;
+ count[key]++;
+ return acceptableTimingsData;
+ }
+ }));
+
+ return function($$body, $rootElement) {
+ $$body.append($rootElement);
+ };
+ }));
+
+ it("should cache frequent calls to getComputedStyle before the next animation frame kicks in",
+ inject(function($animateCss, $document, $rootElement, $$rAF) {
+
+ var i, elm, animator;
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('
');
+ $rootElement.append(elm);
+ animator = $animateCss(elm, { event: 'enter', structural: true });
+ var runner = animator.start();
+ }
+
+ expect(count.normal).toBe(1);
+
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('
');
+ $rootElement.append(elm);
+ animator = $animateCss(elm, { event: 'enter', structural: true });
+ animator.start();
+ }
+
+ expect(count.normal).toBe(1);
+ triggerAnimationStartFrame();
+
+ expect(count.normal).toBe(2);
+
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('
');
+ $rootElement.append(elm);
+ animator = $animateCss(elm, { event: 'enter', structural: true });
+ animator.start();
+ }
+
+ expect(count.normal).toBe(3);
+ }));
+
+ it("should cache frequent calls to getComputedStyle for stagger animations before the next animation frame kicks in",
+ inject(function($animateCss, $document, $rootElement, $$rAF) {
+
+ var element = jqLite('
');
+ $rootElement.append(element);
+ var animator = $animateCss(element, { event: 'enter', structural: true });
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(count.stagger).toBeUndefined();
+
+ var i, elm;
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('
');
+ $rootElement.append(elm);
+ animator = $animateCss(elm, { event: 'enter', structural: true });
+ animator.start();
+ }
+
+ expect(count.stagger).toBe(1);
+
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('
');
+ $rootElement.append(elm);
+ animator = $animateCss(elm, { event: 'enter', structural: true });
+ animator.start();
+ }
+
+ expect(count.stagger).toBe(1);
+ $$rAF.flush();
+
+ for (i = 0; i < 5; i++) {
+ elm = jqLite('
');
+ $rootElement.append(elm);
+ animator = $animateCss(elm, { event: 'enter', structural: true });
+ animator.start();
+ }
+
+ triggerAnimationStartFrame();
+ expect(count.stagger).toBe(2);
+ }));
+ });
+ });
+
+ it('should avoid applying the same cache to an element a follow-up animation is run on the same element',
+ inject(function($animateCss, $rootElement, $$body) {
+
+ function endTransition(element, elapsedTime) {
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now(), elapsedTime: elapsedTime });
+ }
+
+ function startAnimation(element, duration, color) {
+ $animateCss(element, {
+ duration: duration,
+ to: { background: color }
+ }).start();
+ triggerAnimationStartFrame();
+ }
+
+ var element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ startAnimation(element, 0.5, 'red');
+ expect(element.attr('style')).toContain('transition');
+
+ endTransition(element, 0.5);
+ expect(element.attr('style')).not.toContain('transition');
+
+ startAnimation(element, 0.8, 'blue');
+ expect(element.attr('style')).toContain('transition');
+
+ // Trigger an extra transitionend event that matches the original transition
+ endTransition(element, 0.5);
+ expect(element.attr('style')).toContain('transition');
+
+ endTransition(element, 0.8);
+ expect(element.attr('style')).not.toContain('transition');
+ }));
+
+ it('should apply a custom temporary class when a non-structural animation is used',
+ inject(function($animateCss, $rootElement, $$body) {
+
+ var element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ $animateCss(element, {
+ event: 'super',
+ duration: 1000,
+ to: fakeStyle
+ }).start();
+ expect(element).toHaveClass('super');
+
+ triggerAnimationStartFrame();
+ expect(element).toHaveClass('super-active');
+ }));
+
+ describe("structural animations", function() {
+ they('should decorate the element with the ng-$prop CSS class',
+ ['enter', 'leave', 'move'], function(event) {
+ inject(function($animateCss, $rootElement, $$body) {
+ var element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ $animateCss(element, {
+ event: event,
+ structural: true,
+ duration: 1000,
+ to: fakeStyle
+ });
+ expect(element).toHaveClass('ng-' + event);
+ });
+ });
+
+ they('should decorate the element with the ng-$prop-active CSS class',
+ ['enter', 'leave', 'move'], function(event) {
+ inject(function($animateCss, $rootElement, $$body) {
+ var element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ var animator = $animateCss(element, {
+ event: event,
+ structural: true,
+ duration: 1000,
+ to: fakeStyle
+ });
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element).toHaveClass('ng-' + event + '-active');
+ });
+ });
+
+ they('should remove the ng-$prop and ng-$prop-active CSS classes from the element once the animation is done',
+ ['enter', 'leave', 'move'], function(event) {
+ inject(function($animateCss, $rootElement, $$body) {
+ var element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ var animator = $animateCss(element, {
+ event: event,
+ structural: true,
+ duration: 1,
+ to: fakeStyle
+ });
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 1000, elapsedTime: 1 });
+
+ expect(element).not.toHaveClass('ng-' + event);
+ expect(element).not.toHaveClass('ng-' + event + '-active');
+ });
+ });
+
+ they('should allow additional CSS classes to be added and removed alongside the $prop animation',
+ ['enter', 'leave', 'move'], function(event) {
+ inject(function($animateCss, $rootElement) {
+ var element = jqLite('
');
+ $rootElement.append(element);
+ var animator = $animateCss(element, {
+ event: event,
+ structural: true,
+ duration: 1,
+ to: fakeStyle,
+ addClass: 'red',
+ removeClass: 'green'
+ });
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element).toHaveClass('ng-' + event);
+ expect(element).toHaveClass('ng-' + event + '-active');
+
+ expect(element).toHaveClass('red');
+ expect(element).toHaveClass('red-add');
+ expect(element).toHaveClass('red-add-active');
+
+ expect(element).not.toHaveClass('green');
+ expect(element).toHaveClass('green-remove');
+ expect(element).toHaveClass('green-remove-active');
+ });
+ });
+
+ they('should place a CSS transition block after the preparation function to block accidental style changes',
+ ['enter', 'leave', 'move', 'addClass', 'removeClass'], function(event) {
+
+ inject(function($animateCss, $rootElement, $$body) {
+ var element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ ss.addRule('.cool-animation', 'transition:1.5s linear all;');
+ element.addClass('cool-animation');
+
+ var data = {};
+ if (event === 'addClass') {
+ data.addClass = 'green';
+ } else if (event === 'removeClass') {
+ element.addClass('red');
+ data.removeClass = 'red';
+ } else {
+ data.event = event;
+ }
+
+ var animator = $animateCss(element, data);
+ expect(element.css('transition-delay')).toMatch('-1.5s');
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element.attr('style')).toBeFalsy();
+ });
+ });
+
+ they('should not place a CSS transition block if options.skipBlocking is provided',
+ ['enter', 'leave', 'move', 'addClass', 'removeClass'], function(event) {
+
+ inject(function($animateCss, $rootElement, $$body, $window) {
+ var element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ ss.addRule('.cool-animation', 'transition:1.5s linear all;');
+ element.addClass('cool-animation');
+
+ var data = {};
+ if (event === 'addClass') {
+ data.addClass = 'green';
+ } else if (event === 'removeClass') {
+ element.addClass('red');
+ data.removeClass = 'red';
+ } else {
+ data.event = event;
+ }
+
+ var blockSpy = spyOn($window, 'blockTransitions').andCallThrough();
+
+ data.skipBlocking = true;
+ var animator = $animateCss(element, data);
+
+ expect(blockSpy).not.toHaveBeenCalled();
+
+ expect(element.attr('style')).toBeFalsy();
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element.attr('style')).toBeFalsy();
+
+ // just to prove it works
+ data.skipBlocking = false;
+ $animateCss(element, { addClass: 'test' });
+ expect(blockSpy).toHaveBeenCalled();
+ });
+ });
+
+ they('should place a CSS transition block after the preparation function even if a duration is provided',
+ ['enter', 'leave', 'move', 'addClass', 'removeClass'], function(event) {
+
+ inject(function($animateCss, $rootElement, $$body) {
+ var element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ ss.addRule('.cool-animation', 'transition:1.5s linear all;');
+ element.addClass('cool-animation');
+
+ var data = {};
+ if (event === 'addClass') {
+ data.addClass = 'green';
+ } else if (event === 'removeClass') {
+ element.addClass('red');
+ data.removeClass = 'red';
+ } else {
+ data.event = event;
+ }
+
+ data.duration = 10;
+ var animator = $animateCss(element, data);
+
+ expect(element.css('transition-delay')).toMatch('-10s');
+ expect(element.css('transition-duration')).toMatch('');
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element.attr('style')).not.toContain('transition-delay');
+ expect(element.css('transition-property')).toContain('all');
+ expect(element.css('transition-duration')).toContain('10s');
+ });
+ });
+
+ it('should allow multiple events to be animated at the same time',
+ inject(function($animateCss, $rootElement, $$body) {
+
+ var element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ $animateCss(element, {
+ event: ['enter', 'leave', 'move'],
+ structural: true,
+ duration: 1,
+ to: fakeStyle
+ }).start();
+ triggerAnimationStartFrame();
+
+
+ expect(element).toHaveClass('ng-enter');
+ expect(element).toHaveClass('ng-leave');
+ expect(element).toHaveClass('ng-move');
+
+ expect(element).toHaveClass('ng-enter-active');
+ expect(element).toHaveClass('ng-leave-active');
+ expect(element).toHaveClass('ng-move-active');
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 1000, elapsedTime: 1 });
+
+ expect(element).not.toHaveClass('ng-enter');
+ expect(element).not.toHaveClass('ng-leave');
+ expect(element).not.toHaveClass('ng-move');
+ expect(element).not.toHaveClass('ng-enter-active');
+ expect(element).not.toHaveClass('ng-leave-active');
+ expect(element).not.toHaveClass('ng-move-active');
+ }));
+ });
+
+ describe("class-based animations", function() {
+ they('should decorate the element with the class-$prop CSS class',
+ ['add', 'remove'], function(event) {
+ inject(function($animateCss, $rootElement) {
+ var element = jqLite('
');
+ $rootElement.append(element);
+
+ var options = {};
+ options[event + 'Class'] = 'class';
+ options.duration = 1000;
+ options.to = fakeStyle;
+ $animateCss(element, options);
+ expect(element).toHaveClass('class-' + event);
+ });
+ });
+
+ they('should decorate the element with the class-$prop-active CSS class',
+ ['add', 'remove'], function(event) {
+ inject(function($animateCss, $rootElement) {
+ var element = jqLite('
');
+ $rootElement.append(element);
+
+ var options = {};
+ options[event + 'Class'] = 'class';
+ options.duration = 1000;
+ options.to = fakeStyle;
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element).toHaveClass('class-' + event + '-active');
+ });
+ });
+
+ they('should remove the class-$prop-add and class-$prop-active CSS classes from the element once the animation is done',
+ ['enter', 'leave', 'move'], function(event) {
+ inject(function($animateCss, $rootElement, $$body) {
+ var element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ var options = {};
+ options.event = event;
+ options.duration = 10;
+ options.to = fakeStyle;
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 1000, elapsedTime: 10 });
+
+ expect(element).not.toHaveClass('ng-' + event);
+ expect(element).not.toHaveClass('ng-' + event + '-active');
+ });
+ });
+
+ they('should allow the class duration styles to be recalculated once started if the CSS classes being applied result new transition styles',
+ ['add', 'remove'], function(event) {
+ inject(function($animateCss, $rootElement, $$body) {
+
+ var element = jqLite('
');
+
+ if (event == 'add') {
+ ss.addRule('.natural-class', 'transition:1s linear all;');
+ } else {
+ ss.addRule('.natural-class', 'transition:0s linear none;');
+ ss.addRule('.base-class', 'transition:1s linear none;');
+
+ element.addClass('base-class');
+ element.addClass('natural-class');
+ }
+
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ var options = {};
+ options[event + 'Class'] = 'natural-class';
+ var runner = $animateCss(element, options);
+ runner.start();
+ triggerAnimationStartFrame();
+
+ expect(element).toHaveClass('natural-class-' + event);
+ expect(element).toHaveClass('natural-class-' + event + '-active');
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now(), elapsedTime: 1 });
+
+ expect(element).not.toHaveClass('natural-class-' + event);
+ expect(element).not.toHaveClass('natural-class-' + event + '-active');
+ });
+ });
+
+ they('should force the class-based values to be applied early if no options.applyClassEarly is used as an option',
+ ['enter', 'leave', 'move'], function(event) {
+ inject(function($animateCss, $rootElement, $$body) {
+
+ ss.addRule('.blue.ng-' + event, 'transition:2s linear all;');
+
+ var element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+
+ var runner = $animateCss(element, {
+ addClass: 'blue',
+ applyClassesEarly: true,
+ removeClass: 'red',
+ event: event,
+ structural: true
+ });
+
+ runner.start();
+ expect(element).toHaveClass('ng-' + event);
+ expect(element).toHaveClass('blue');
+ expect(element).not.toHaveClass('red');
+
+ triggerAnimationStartFrame();
+ expect(element).toHaveClass('ng-' + event);
+ expect(element).toHaveClass('ng-' + event + '-active');
+ expect(element).toHaveClass('blue');
+ expect(element).not.toHaveClass('red');
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now(), elapsedTime: 2 });
+
+ expect(element).not.toHaveClass('ng-' + event);
+ expect(element).not.toHaveClass('ng-' + event + '-active');
+ expect(element).toHaveClass('blue');
+ expect(element).not.toHaveClass('red');
+ });
+ });
+ });
+
+ describe("options", function() {
+ var element;
+ beforeEach(inject(function($rootElement, $$body) {
+ $$body.append($rootElement);
+
+ element = jqLite('
');
+ $rootElement.append(element);
+ }));
+
+ describe("[$$skipPreparationClasses]", function() {
+ it('should not apply and remove the preparation classes to the element when true',
+ inject(function($animateCss) {
+
+ var options = {
+ duration: 3000,
+ to: fakeStyle,
+ event: 'event',
+ structural: true,
+ addClass: 'klass',
+ $$skipPreparationClasses: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ expect(element).not.toHaveClass('klass-add');
+ expect(element).not.toHaveClass('ng-event');
+
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element).not.toHaveClass('klass-add');
+ expect(element).not.toHaveClass('ng-event');
+
+ expect(element).toHaveClass('klass-add-active');
+ expect(element).toHaveClass('ng-event-active');
+
+ element.addClass('klass-add ng-event');
+
+ runner.end();
+
+ expect(element).toHaveClass('klass-add');
+ expect(element).toHaveClass('ng-event');
+
+ expect(element).not.toHaveClass('klass-add-active');
+ expect(element).not.toHaveClass('ng-event-active');
+ }));
+ });
+
+ describe("[duration]", function() {
+ it("should be applied for a transition directly", inject(function($animateCss, $rootElement) {
+ var element = jqLite('
');
+ $rootElement.append(element);
+
+ var options = {
+ duration: 3000,
+ to: fakeStyle,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+ var style = element.attr('style');
+ expect(style).toContain('3000s');
+ expect(style).toContain('linear');
+ }));
+
+ it("should be applied to a CSS keyframe animation directly if keyframes are detected within the CSS class",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-animation:1.5s keyframe_animation;' +
+ 'animation:1.5s keyframe_animation;');
+
+ var options = {
+ duration: 5,
+ event: 'enter',
+ structural: true
+ };
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element.css(prefix + 'animation-duration')).toEqual('5s');
+ }));
+
+ it("should remove all inline keyframe styling when an animation completes if a custom duration was applied",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-animation:1.5s keyframe_animation;' +
+ 'animation:1.5s keyframe_animation;');
+
+ var options = {
+ duration: 5,
+ event: 'enter',
+ structural: true
+ };
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ browserTrigger(element, 'animationend',
+ { timeStamp: Date.now() + 5000, elapsedTime: 5 });
+
+ expect(element.attr('style')).toBeFalsy();
+ }));
+
+ it("should remove all inline keyframe delay styling when an animation completes if a custom duration was applied",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-animation:1.5s keyframe_animation;' +
+ 'animation:1.5s keyframe_animation;');
+
+ var options = {
+ delay: 5,
+ event: 'enter',
+ structural: true
+ };
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element.css(prefix + 'animation-delay')).toEqual('5s');
+
+ browserTrigger(element, 'animationend',
+ { timeStamp: Date.now() + 5000, elapsedTime: 1.5 });
+
+ expect(element.attr('style')).toBeFalsy();
+ }));
+
+ it("should not prepare the animation at all if a duration of zero is provided",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-transition:1s linear all;' +
+ 'transition:1s linear all;');
+
+ var options = {
+ duration: 0,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ expect(animator.$$willAnimate).toBeFalsy();
+ }));
+
+ it("should apply a transition and keyframe duration directly if both transitions and keyframe classes are detected",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-animation:3s keyframe_animation;' +
+ 'animation:3s keyframe_animation;' +
+ 'transition:5s linear all;');
+
+ var options = {
+ duration: 4,
+ event: 'enter',
+ structural: true
+ };
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ var style = element.attr('style');
+ expect(style).toMatch(/animation(?:-duration)?:\s*4s/);
+ expect(element.css('transition-duration')).toMatch('4s');
+ expect(element.css('transition-property')).toMatch('all');
+ expect(style).toContain('linear');
+ }));
+ });
+
+ describe("[delay]", function() {
+ it("should be applied for a transition directly", inject(function($animateCss, $rootElement) {
+ var element = jqLite('
');
+ $rootElement.append(element);
+
+ var options = {
+ duration: 3000,
+ delay: 500,
+ to: fakeStyle,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var prop = element.css('transition-delay');
+ expect(prop).toEqual('500s');
+ }));
+
+ it("should return false for the animator if a delay is provided but not a duration",
+ inject(function($animateCss, $rootElement) {
+
+ var element = jqLite('
');
+ $rootElement.append(element);
+
+ var options = {
+ delay: 500,
+ to: fakeStyle,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+ expect(animator.$$willAnimate).toBeFalsy();
+ }));
+
+ it("should override the delay value present in the CSS class",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-transition:1s linear all;' +
+ 'transition:1s linear all;' +
+ '-webkit-transition-delay:10s;' +
+ 'transition-delay:10s;');
+
+ var element = jqLite('
');
+ $rootElement.append(element);
+
+ var options = {
+ delay: 500,
+ event: 'enter',
+ structural: true
+ };
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var prop = element.css('transition-delay');
+ expect(prop).toEqual('500s');
+ }));
+
+ it("should allow the delay value to zero if provided",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-transition:1s linear all;' +
+ 'transition:1s linear all;' +
+ '-webkit-transition-delay:10s;' +
+ 'transition-delay:10s;');
+
+ var element = jqLite('
');
+ $rootElement.append(element);
+
+ var options = {
+ delay: 0,
+ event: 'enter',
+ structural: true
+ };
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var prop = element.css('transition-delay');
+ expect(prop).toEqual('0s');
+ }));
+
+ it("should be applied to a CSS keyframe animation if detected within the CSS class",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-animation:1.5s keyframe_animation;' +
+ 'animation:1.5s keyframe_animation;');
+
+ var options = {
+ delay: 400,
+ event: 'enter',
+ structural: true
+ };
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element.css(prefix + 'animation-delay')).toEqual('400s');
+ expect(element.attr('style')).not.toContain('transition-delay');
+ }));
+
+ it("should apply a transition and keyframe delay if both transitions and keyframe classes are detected",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-animation:3s keyframe_animation;' +
+ 'animation:3s keyframe_animation;' +
+ 'transition:5s linear all;');
+
+ var options = {
+ delay: 10,
+ event: 'enter',
+ structural: true
+ };
+ var animator = $animateCss(element, options);
+
+ expect(element.css('transition-delay')).toContain('-5s');
+ expect(element.attr('style')).not.toContain('animation-delay');
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element.css(prefix + 'animation-delay')).toEqual('10s');
+ expect(element.css('transition-delay')).toEqual('10s');
+ }));
+
+ it("should apply blocking before the animation starts, but then apply the detected delay when options.delay is true",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', 'transition:2s linear all; transition-delay: 1s;');
+
+ var options = {
+ delay: true,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+ expect(element.css('transition-delay')).toEqual('-2s');
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element.css('transition-delay')).toEqual('1s');
+ }));
+
+ they("should consider a negative value when delay:true is used with a $prop animation", {
+ 'transition': function() {
+ return {
+ prop: 'transition-delay',
+ css: 'transition:2s linear all; transition-delay: -1s'
+ };
+ },
+ 'keyframe': function(prefix) {
+ return {
+ prop: prefix + 'animation-delay',
+ css: prefix + 'animation:2s keyframe_animation; ' + prefix + 'animation-delay: -1s;'
+ };
+ }
+ }, function(testDetailsFactory) {
+ inject(function($animateCss, $rootElement) {
+ var testDetails = testDetailsFactory(prefix);
+
+ ss.addRule('.ng-enter', testDetails.css);
+ var options = {
+ delay: true,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element.css(testDetails.prop)).toContain('-1s');
+ });
+ });
+
+ they("should consider a negative value when a negative option delay is provided for a $prop animation", {
+ 'transition': function() {
+ return {
+ prop: 'transition-delay',
+ css: 'transition:2s linear all'
+ };
+ },
+ 'keyframe': function(prefix) {
+ return {
+ prop: prefix + 'animation-delay',
+ css: prefix + 'animation:2s keyframe_animation'
+ };
+ }
+ }, function(testDetailsFactory) {
+ inject(function($animateCss, $rootElement) {
+ var testDetails = testDetailsFactory(prefix);
+
+ ss.addRule('.ng-enter', testDetails.css);
+ var options = {
+ delay: -2,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element.css(testDetails.prop)).toContain('-2s');
+ });
+ });
+
+ they("should expect the $propend event to always return the full duration even when negative values are used", {
+ 'transition': function() {
+ return {
+ event: 'transitionend',
+ css: 'transition:5s linear all; transition-delay: -2s'
+ };
+ },
+ 'animation': function(prefix) {
+ return {
+ event: 'animationend',
+ css: prefix + 'animation:5s keyframe_animation; ' + prefix + 'animation-delay: -2s;'
+ };
+ }
+ }, function(testDetailsFactory) {
+ inject(function($animateCss, $rootElement) {
+ var testDetails = testDetailsFactory(prefix);
+ var event = testDetails.event;
+
+ ss.addRule('.ng-enter', testDetails.css);
+ var options = { event: 'enter', structural: true };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+ // 5 + (-2s) = 3
+ browserTrigger(element, event, { timeStamp: Date.now(), elapsedTime: 3 });
+
+ assertAnimationRunning(element, true);
+
+ // 5 seconds is the full animation
+ browserTrigger(element, event, { timeStamp: Date.now(), elapsedTime: 5 });
+
+ assertAnimationRunning(element);
+ });
+ });
+ });
+
+ describe("[transitionStyle]", function() {
+ it("should apply the transition directly onto the element and animate accordingly",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ transitionStyle: '5.5s linear all',
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var style = element.attr('style');
+ expect(element.css('transition-duration')).toMatch('5.5s');
+ expect(element.css('transition-property')).toMatch('all');
+ expect(style).toContain('linear');
+
+ expect(element).toHaveClass('ng-enter');
+ expect(element).toHaveClass('ng-enter-active');
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 10000, elapsedTime: 5.5 });
+
+ expect(element).not.toHaveClass('ng-enter');
+ expect(element).not.toHaveClass('ng-enter-active');
+
+ expect(element.attr('style')).toBeFalsy();
+ }));
+
+ it("should give priority to the provided duration value, but only update the duration style itself",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ transitionStyle: '5.5s ease-in color',
+ duration: 4,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ var style = element.attr('style');
+ expect(element.css('transition-duration')).toMatch('4s');
+ expect(element.css('transition-property')).toMatch('color');
+ expect(style).toContain('ease-in');
+ }));
+
+ it("should give priority to the provided delay value, but only update the delay style itself",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ transitionStyle: '5.5s 4s ease-in color',
+ delay: 20,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ var style = element.attr('style');
+ expect(element.css('transition-delay')).toMatch('20s');
+ expect(element.css('transition-duration')).toMatch('5.5s');
+ expect(element.css('transition-property')).toMatch('color');
+ expect(style).toContain('ease-in');
+ }));
+
+ it("should execute the animation only if there is any provided CSS styling to go with the transition",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ transitionStyle: '6s 4s ease-out all'
+ };
+
+ $animateCss(element, options).start();
+ triggerAnimationStartFrame();
+
+ expect(element.css(prefix + 'transition-delay')).not.toEqual('4s');
+ expect(element.css(prefix + 'transition-duration')).not.toEqual('6s');
+
+ options.to = { color: 'brown' };
+ $animateCss(element, options).start();
+ triggerAnimationStartFrame();
+
+ expect(element.css(prefix + 'transition-delay')).toEqual('4s');
+ expect(element.css(prefix + 'transition-duration')).toEqual('6s');
+ }));
+ });
+
+ describe("[keyframeStyle]", function() {
+ it("should apply the keyframe animation directly onto the element and animate accordingly",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ keyframeStyle: 'my_animation 5.5s',
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var detectedStyle = element.attr('style');
+ expect(detectedStyle).toContain('5.5s');
+ expect(detectedStyle).toContain('my_animation');
+
+ expect(element).toHaveClass('ng-enter');
+ expect(element).toHaveClass('ng-enter-active');
+
+ browserTrigger(element, 'animationend',
+ { timeStamp: Date.now() + 10000, elapsedTime: 5.5 });
+
+ expect(element).not.toHaveClass('ng-enter');
+ expect(element).not.toHaveClass('ng-enter-active');
+
+ expect(element.attr('style')).toBeFalsy();
+ }));
+
+ it("should give priority to the provided duration value, but only update the duration style itself",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ keyframeStyle: 'my_animation 5.5s',
+ duration: 50,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var detectedStyle = element.attr('style');
+ expect(detectedStyle).toContain('50s');
+ expect(detectedStyle).toContain('my_animation');
+ }));
+
+ it("should give priority to the provided delay value, but only update the duration style itself",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ keyframeStyle: 'my_animation 5.5s 10s',
+ delay: 50,
+ event: 'enter',
+ structural: true
+ };
+
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element.css(prefix + 'animation-delay')).toEqual('50s');
+ expect(element.css(prefix + 'animation-duration')).toEqual('5.5s');
+ expect(element.css(prefix + 'animation-name')).toEqual('my_animation');
+ }));
+
+ it("should be able to execute the animation if it is the only provided value",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ keyframeStyle: 'my_animation 5.5s 10s'
+ };
+
+ var animator = $animateCss(element, options);
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element.css(prefix + 'animation-delay')).toEqual('10s');
+ expect(element.css(prefix + 'animation-duration')).toEqual('5.5s');
+ expect(element.css(prefix + 'animation-name')).toEqual('my_animation');
+ }));
+ });
+
+ describe("[from] and [to]", function() {
+ it("should apply from styles to an element during the preparation phase",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ duration: 2.5,
+ event: 'enter',
+ structural: true,
+ from: { width: '50px' },
+ to: { width: '100px' }
+ };
+
+ var animator = $animateCss(element, options);
+ expect(element.attr('style')).toMatch(/width:\s*50px/);
+ }));
+
+ it("should apply to styles to an element during the animation phase",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ duration: 2.5,
+ event: 'enter',
+ structural: true,
+ from: { width: '15px' },
+ to: { width: '25px' }
+ };
+
+ var animator = $animateCss(element, options);
+ var runner = animator.start();
+ triggerAnimationStartFrame();
+ runner.end();
+
+ expect(element.css('width')).toBe('25px');
+ }));
+
+ it("should apply the union of from and to styles to the element if no animation will be run",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ event: 'enter',
+ structural: true,
+ from: { 'width': '10px', height: '50px' },
+ to: { 'width': '15px' }
+ };
+
+ var animator = $animateCss(element, options);
+
+ expect(animator.$$willAnimate).toBeFalsy();
+ animator.start();
+
+ expect(element.css('width')).toBe('15px');
+ expect(element.css('height')).toBe('50px');
+ }));
+
+ it("should retain to and from styles on an element after an animation completes",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ event: 'enter',
+ structural: true,
+ duration: 10,
+ from: { 'width': '10px', height: '66px' },
+ to: { 'width': '5px' }
+ };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 10000, elapsedTime: 10 });
+
+ expect(element).not.toHaveClass('ng-enter');
+ expect(element.css('width')).toBe('5px');
+ expect(element.css('height')).toBe('66px');
+ }));
+
+ it("should always apply the from styles before the start function is called even if no transition is detected when started",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.my-class', 'transition: 0s linear color');
+
+ var options = {
+ addClass: 'my-class',
+ from: { height: '26px' },
+ to: { height: '500px' }
+ };
+
+ var animator = $animateCss(element, options);
+ expect(element.css('height')).toBe('26px');
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element.css('height')).toBe('500px');
+ }));
+
+ it("should apply an inline transition if [to] styles and a duration are provided",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ event: 'enter',
+ structural: true,
+ duration: 2.5,
+ to: { background: 'red' }
+ };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var style = element.attr('style');
+ expect(element.css('transition-duration')).toMatch('2.5s');
+ expect(element.css('transition-property')).toMatch('all');
+ expect(style).toContain('linear');
+ }));
+
+ it("should remove all inline transition styling when an animation completes",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ event: 'enter',
+ structural: true,
+ duration: 2.5,
+ to: { background: 'red' }
+ };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var style = element.attr('style');
+ expect(style).toContain('transition');
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 2500, elapsedTime: 2.5 });
+
+ style = element.attr('style');
+ expect(style).not.toContain('transition');
+ }));
+
+ it("should retain existing styles when an inline styled animation completes",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ event: 'enter',
+ structural: true,
+ duration: 2.5
+ };
+
+ element.css('font-size', '20px');
+ element.css('opacity', '0.5');
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+ var style = element.attr('style');
+ expect(style).toContain('transition');
+ animator.end();
+
+ style = element.attr('style');
+ expect(element.attr('style')).not.toContain('transition');
+ expect(element.css('opacity')).toEqual('0.5');
+ }));
+
+ it("should remove all inline transition delay styling when an animation completes",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', 'transition: 1s linear color');
+
+ var options = {
+ event: 'enter',
+ structural: true,
+ delay: 5
+ };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element.css('transition-delay')).toEqual('5s');
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 5000, elapsedTime: 1 });
+
+ expect(element.attr('style') || '').not.toContain('transition');
+ }));
+
+ it("should not apply an inline transition if only [from] styles and a duration are provided",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ duration: 3,
+ from: { background: 'blue' }
+ };
+
+ var animator = $animateCss(element, options);
+ expect(animator.$$willAnimate).toBeFalsy();
+ }));
+
+ it("should apply a transition if [from] styles are provided with a class that is added",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ addClass: 'superb',
+ from: { background: 'blue' }
+ };
+
+ var animator = $animateCss(element, options);
+ expect(isFunction(animator.start)).toBe(true);
+ }));
+
+ it("should apply an inline transition if only [from] styles, but classes are added or removed and a duration is provided",
+ inject(function($animateCss, $rootElement) {
+
+ var options = {
+ duration: 3,
+ addClass: 'sugar',
+ from: { background: 'yellow' }
+ };
+
+ var animator = $animateCss(element, options);
+ expect(animator.$$willAnimate).toBeTruthy();
+ }));
+
+ it("should not apply an inline transition if no styles are provided",
+ inject(function($animateCss, $rootElement) {
+
+ var emptyObject = {};
+ var options = {
+ duration: 3,
+ to: emptyObject,
+ from: emptyObject
+ };
+
+ var animator = $animateCss(element, options);
+ expect(animator.$$willAnimate).toBeFalsy();
+ }));
+
+ it("should apply a transition duration if the existing transition duration's property value is not 'all'",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', 'transition: 1s linear color');
+
+ var emptyObject = {};
+ var options = {
+ event: 'enter',
+ structural: true,
+ to: { background: 'blue' }
+ };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ var style = element.attr('style');
+ expect(element.css('transition-duration')).toMatch('1s');
+ expect(element.css('transition-property')).toMatch('all');
+ expect(style).toContain('linear');
+ }));
+
+ it("should apply a transition duration and an animation duration if duration + styles options are provided for a matching keyframe animation",
+ inject(function($animateCss, $rootElement) {
+
+ ss.addRule('.ng-enter', '-webkit-animation:3.5s keyframe_animation;' +
+ 'animation:3.5s keyframe_animation;');
+
+ var emptyObject = {};
+ var options = {
+ event: 'enter',
+ structural: true,
+ duration: 10,
+ to: {
+ background: 'blue'
+ }
+ };
+
+ var animator = $animateCss(element, options);
+ animator.start();
+ triggerAnimationStartFrame();
+
+
+ expect(element.css('transition-duration')).toMatch('10s');
+ expect(element.css(prefix + 'animation-duration')).toEqual('10s');
+ }));
+ });
+
+ describe("[easing]", function() {
+
+ var element;
+ beforeEach(inject(function($$body, $rootElement) {
+ element = jqLite('
');
+ $rootElement.append(element);
+ $$body.append($rootElement);
+ }));
+
+ it("should apply easing to a transition animation if it exists", inject(function($animateCss) {
+ ss.addRule('.red', 'transition:1s linear all;');
+ var easing = 'ease-out';
+ var animator = $animateCss(element, { addClass: 'red', easing: easing });
+ animator.start();
+ triggerAnimationStartFrame();
+
+ var style = element.attr('style');
+ expect(style).toContain('ease-out');
+ }));
+
+ it("should not apply easing to transitions nor keyframes on an element animation if nothing is detected",
+ inject(function($animateCss) {
+
+ ss.addRule('.red', ';');
+ var easing = 'ease-out';
+ var animator = $animateCss(element, { addClass: 'red', easing: easing });
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element.attr('style')).toBeFalsy();
+ }));
+
+ it("should apply easing to both keyframes and transition animations if detected",
+ inject(function($animateCss) {
+
+ ss.addRule('.red', 'transition: 1s linear all;');
+ ss.addRule('.blue', prefix + 'animation:my_keyframe 1s;');
+ var easing = 'ease-out';
+ var animator = $animateCss(element, { addClass: 'red blue', easing: easing });
+ animator.start();
+ triggerAnimationStartFrame();
+
+ var style = element.attr('style');
+ expect(style).toMatch(/animation(?:-timing-function)?:\s*ease-out/);
+ expect(style).toMatch(/transition(?:-timing-function)?:\s*ease-out/);
+ }));
+ });
+
+ it('should round up long elapsedTime values to close off a CSS3 animation',
+ inject(function($animateCss) {
+
+ ss.addRule('.millisecond-transition.ng-leave', '-webkit-transition:510ms linear all;' +
+ 'transition:510ms linear all;');
+
+ element.addClass('millisecond-transition');
+ var animator = $animateCss(element, {
+ event: 'leave',
+ structural: true
+ });
+
+ animator.start();
+ triggerAnimationStartFrame();
+
+ expect(element).toHaveClass('ng-leave-active');
+
+ browserTrigger(element, 'transitionend',
+ { timeStamp: Date.now() + 1000, elapsedTime: 0.50999999991 });
+
+ expect(element).not.toHaveClass('ng-leave-active');
+ }));
+ });
+
+ describe('SVG', function() {
+ it('should properly apply transitions on an SVG element',
+ inject(function($animateCss, $rootScope, $compile, $$body, $rootElement) {
+
+ var element = $compile('' +
+ ' ' +
+ ' ')($rootScope);
+
+ $$body.append($rootElement);
+ $rootElement.append(element);
+
+ $animateCss(element, {
+ event: 'enter',
+ structural: true,
+ duration: 10
+ }).start();
+
+ triggerAnimationStartFrame();
+
+ expect(jqLiteHasClass(element[0], 'ng-enter')).toBe(true);
+ expect(jqLiteHasClass(element[0], 'ng-enter-active')).toBe(true);
+
+ browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 10 });
+
+ expect(jqLiteHasClass(element[0], 'ng-enter')).toBe(false);
+ expect(jqLiteHasClass(element[0], 'ng-enter-active')).toBe(false);
+ }));
+
+ it('should properly remove classes from SVG elements', inject(function($animateCss) {
+ var element = jqLite('' +
+ ' ' +
+ ' ');
+ var child = element.find('rect');
+
+ var animator = $animateCss(child, {
+ removeClass: 'class-of-doom',
+ duration: 0
+ });
+ animator.start();
+
+ var className = child[0].getAttribute('class');
+ expect(className).toBe('');
+ }));
+ });
+ });
+});
diff --git a/test/ngAnimate/animateJsDriverSpec.js b/test/ngAnimate/animateJsDriverSpec.js
new file mode 100644
index 000000000000..274457965f10
--- /dev/null
+++ b/test/ngAnimate/animateJsDriverSpec.js
@@ -0,0 +1,178 @@
+'use strict';
+
+describe("ngAnimate $$animateJsDriver", function() {
+
+ beforeEach(module('ngAnimate'));
+
+ it('should register the $$animateJsDriver into the list of drivers found in $animateProvider',
+ module(function($animateProvider) {
+
+ expect($animateProvider.drivers).toContain('$$animateJsDriver');
+ }));
+
+ describe('with $$animateJs', function() {
+ var capturedAnimation = null;
+ var captureLog = [];
+ var element;
+ var driver;
+
+ beforeEach(module(function($provide) {
+ $provide.factory('$$animateJs', function($$AnimateRunner) {
+ return function() {
+ var runner = new $$AnimateRunner();
+ capturedAnimation = arguments;
+ captureLog.push({
+ args: capturedAnimation,
+ runner: runner
+ });
+ return {
+ start: function() {
+ return runner;
+ }
+ };
+ };
+ });
+
+ captureLog.length = 0;
+ element = jqLite('
');
+ element.append(child1);
+ var child2 = jqLite('
');
+ element.append(child2);
+
+ driver({
+ from: {
+ structural: true,
+ element: child1,
+ event: 'leave'
+ },
+ to: {
+ structural: true,
+ element: child2,
+ event: 'enter'
+ }
+ });
+ $rootScope.$digest();
+
+ expect(captureLog.length).toBe(2);
+
+ var first = captureLog[0].args;
+ expect(first[0]).toBe(child1);
+ expect(first[1]).toBe('leave');
+
+ var second = captureLog[1].args;
+ expect(second[0]).toBe(child2);
+ expect(second[1]).toBe('enter');
+ }));
+
+ they('should $prop both animations when $prop() is called on the runner', ['end', 'cancel'], function(method) {
+ inject(function($rootScope, $$rAF) {
+ var child1 = jqLite('
');
+ element.append(child1);
+ var child2 = jqLite('
');
+ element.append(child2);
+
+ var animator = driver({
+ from: {
+ structural: true,
+ element: child1,
+ event: 'leave'
+ },
+ to: {
+ structural: true,
+ element: child2,
+ event: 'enter'
+ }
+ });
+
+ var runner = animator.start();
+
+ var animationsClosed = false;
+ var status;
+ runner.done(function(s) {
+ animationsClosed = true;
+ status = s;
+ });
+
+ $rootScope.$digest();
+
+ runner[method]();
+ $$rAF.flush();
+
+ expect(animationsClosed).toBe(true);
+ expect(status).toBe(method === 'end' ? true : false);
+ });
+ });
+
+ they('should fully $prop when all inner animations are complete', ['end', 'cancel'], function(method) {
+ inject(function($rootScope, $$rAF) {
+ var child1 = jqLite('
');
+ element.append(child1);
+ var child2 = jqLite('
');
+ element.append(child2);
+
+ var animator = driver({
+ from: {
+ structural: true,
+ element: child1,
+ event: 'leave'
+ },
+ to: {
+ structural: true,
+ element: child2,
+ event: 'enter'
+ }
+ });
+
+ var runner = animator.start();
+
+ var animationsClosed = false;
+ var status;
+ runner.done(function(s) {
+ animationsClosed = true;
+ status = s;
+ });
+
+ $$rAF.flush();
+
+ captureLog[0].runner[method]();
+ expect(animationsClosed).toBe(false);
+
+ captureLog[1].runner[method]();
+ expect(animationsClosed).toBe(true);
+
+ expect(status).toBe(method === 'end' ? true : false);
+ });
+ });
+ });
+});
diff --git a/test/ngAnimate/animateJsSpec.js b/test/ngAnimate/animateJsSpec.js
new file mode 100644
index 000000000000..c162d678f098
--- /dev/null
+++ b/test/ngAnimate/animateJsSpec.js
@@ -0,0 +1,732 @@
+'use strict';
+
+describe("ngAnimate $$animateJs", function() {
+
+ beforeEach(module('ngAnimate'));
+
+ function getDoneFunction(args) {
+ for (var i = 1; i < args.length; i++) {
+ var a = args[i];
+ if (isFunction(a)) return a;
+ }
+ }
+
+ it('should return nothing if no animations are registered at all', inject(function($$animateJs) {
+ var element = jqLite('
');
+ expect($$animateJs(element, 'enter')).toBeFalsy();
+ }));
+
+ it('should return nothing if no matching animations classes are found', function() {
+ module(function($animateProvider) {
+ $animateProvider.register('.foo', function() {
+ return { enter: noop };
+ });
+ });
+ inject(function($$animateJs) {
+ var element = jqLite('
');
+ expect($$animateJs(element, 'enter')).toBeFalsy();
+ });
+ });
+
+ it('should return nothing if a matching animation class is found, but not a matching event', function() {
+ module(function($animateProvider) {
+ $animateProvider.register('.foo', function() {
+ return { enter: noop };
+ });
+ });
+ inject(function($$animateJs) {
+ var element = jqLite('
');
+ expect($$animateJs(element, 'leave')).toBeFalsy();
+ });
+ });
+
+ it('should return a truthy value if a matching animation class and event are found', function() {
+ module(function($animateProvider) {
+ $animateProvider.register('.foo', function() {
+ return { enter: noop };
+ });
+ });
+ inject(function($$animateJs) {
+ var element = jqLite('
');
+ expect($$animateJs(element, 'enter')).toBeTruthy();
+ });
+ });
+
+ it('should strictly query for the animation based on the classes value if passed in', function() {
+ module(function($animateProvider) {
+ $animateProvider.register('.superman', function() {
+ return { enter: noop };
+ });
+ $animateProvider.register('.batman', function() {
+ return { leave: noop };
+ });
+ });
+ inject(function($$animateJs) {
+ var element = jqLite('
');
+ expect($$animateJs(element, 'enter', 'superman')).toBeTruthy();
+ expect($$animateJs(element, 'leave', 'legoman batman')).toBeTruthy();
+ expect($$animateJs(element, 'enter', 'legoman')).toBeFalsy();
+ expect($$animateJs(element, 'leave', {})).toBeTruthy();
+ });
+ });
+
+ it('should run multiple animations in parallel', function() {
+ var doneCallbacks = [];
+ function makeAnimation(event) {
+ return function() {
+ var data = {};
+ data[event] = function(element, done) {
+ doneCallbacks.push(done);
+ };
+ return data;
+ };
+ }
+ module(function($animateProvider) {
+ $animateProvider.register('.one', makeAnimation('enter'));
+ $animateProvider.register('.two', makeAnimation('enter'));
+ $animateProvider.register('.three', makeAnimation('enter'));
+ });
+ inject(function($$animateJs, $$rAF) {
+ var element = jqLite('
');
+ var animator = $$animateJs(element, 'enter');
+ var complete = false;
+ animator.start().done(function() {
+ complete = true;
+ });
+ expect(doneCallbacks.length).toBe(3);
+ forEach(doneCallbacks, function(cb) {
+ cb();
+ });
+ $$rAF.flush();
+ expect(complete).toBe(true);
+ });
+ });
+
+ they('should $prop the animation when runner.$prop() is called', ['end', 'cancel'], function(method) {
+ var ended = false;
+ var status;
+ module(function($animateProvider) {
+ $animateProvider.register('.the-end', function() {
+ return {
+ enter: function() {
+ return function(cancelled) {
+ ended = true;
+ status = cancelled ? 'cancel' : 'end';
+ };
+ }
+ };
+ });
+ });
+ inject(function($$animateJs) {
+ var element = jqLite('
');
+ var animator = $$animateJs(element, 'enter');
+ var runner = animator.start();
+
+ expect(isFunction(runner[method])).toBe(true);
+
+ expect(ended).toBeFalsy();
+ runner[method]();
+ expect(ended).toBeTruthy();
+ expect(status).toBe(method);
+ });
+ });
+
+ they('should $prop all of the running the animations when runner.$prop() is called',
+ ['end', 'cancel'], function(method) {
+
+ var lookup = {};
+ module(function($animateProvider) {
+ forEach(['one','two','three'], function(klass) {
+ $animateProvider.register('.' + klass, function() {
+ return {
+ enter: function() {
+ return function(cancelled) {
+ lookup[klass] = cancelled ? 'cancel' : 'end';
+ };
+ }
+ };
+ });
+ });
+ });
+ inject(function($$animateJs) {
+ var element = jqLite('
');
+ var animator = $$animateJs(element, 'enter');
+ var runner = animator.start();
+
+ runner[method]();
+ expect(lookup.one).toBe(method);
+ expect(lookup.two).toBe(method);
+ expect(lookup.three).toBe(method);
+ });
+ });
+
+ they('should only run the $prop operation once', ['end', 'cancel'], function(method) {
+ var ended = false;
+ var count = 0;
+ module(function($animateProvider) {
+ $animateProvider.register('.the-end', function() {
+ return {
+ enter: function() {
+ return function(cancelled) {
+ ended = true;
+ count++;
+ };
+ }
+ };
+ });
+ });
+ inject(function($$animateJs) {
+ var element = jqLite('
');
+ var animator = $$animateJs(element, 'enter');
+ var runner = animator.start();
+
+ expect(isFunction(runner[method])).toBe(true);
+
+ expect(ended).toBeFalsy();
+ runner[method]();
+ expect(ended).toBeTruthy();
+ expect(count).toBe(1);
+
+ runner[method]();
+ expect(count).toBe(1);
+ });
+ });
+
+ it('should always run the provided animation in atleast one RAF frame if defined', function() {
+ var before, after, endCalled;
+ module(function($animateProvider) {
+ $animateProvider.register('.the-end', function() {
+ return {
+ beforeAddClass: function(element, className, done) {
+ before = done;
+ },
+ addClass: function(element, className, done) {
+ after = done;
+ }
+ };
+ });
+ });
+ inject(function($$animateJs, $$rAF) {
+ var element = jqLite('
');
+ var animator = $$animateJs(element, 'addClass', {
+ addClass: 'red'
+ });
+
+ var runner = animator.start();
+ runner.done(function() {
+ endCalled = true;
+ });
+
+ expect(before).toBeDefined();
+ before();
+
+ expect(after).toBeUndefined();
+ $$rAF.flush();
+ expect(after).toBeDefined();
+ after();
+
+ expect(endCalled).toBeUndefined();
+ $$rAF.flush();
+ expect(endCalled).toBe(true);
+ });
+ });
+
+ they('should still run the associated DOM event when the $prop function is run but no more animations', ['cancel', 'end'], function(method) {
+ var log = [];
+ module(function($animateProvider) {
+ $animateProvider.register('.the-end', function() {
+ return {
+ beforeAddClass: function() {
+ return function(cancelled) {
+ var status = cancelled ? 'cancel' : 'end';
+ log.push('before addClass ' + status);
+ };
+ },
+ addClass: function() {
+ return function(cancelled) {
+ var status = cancelled ? 'cancel' : 'end';
+ log.push('after addClass' + status);
+ };
+ }
+ };
+ });
+ });
+ inject(function($$animateJs, $$rAF) {
+ var element = jqLite('
');
+ var animator = $$animateJs(element, 'addClass', {
+ domOperation: function() {
+ log.push('dom addClass');
+ }
+ });
+ var runner = animator.start();
+ runner.done(function() {
+ log.push('addClass complete');
+ });
+ runner[method]();
+
+ $$rAF.flush();
+ expect(log).toEqual(
+ ['before addClass ' + method,
+ 'dom addClass',
+ 'addClass complete']);
+ });
+ });
+
+ it('should resolve the promise when end() is called', function() {
+ module(function($animateProvider) {
+ $animateProvider.register('.the-end', function() {
+ return { beforeAddClass: noop };
+ });
+ });
+ inject(function($$animateJs, $$rAF, $rootScope) {
+ var element = jqLite('
');
+ var animator = $$animateJs(element, 'addClass');
+ var runner = animator.start();
+ var done = false;
+ var cancelled = false;
+ runner.then(function() {
+ done = true;
+ }, function() {
+ cancelled = true;
+ });
+
+ runner.end();
+ $$rAF.flush();
+ $rootScope.$digest();
+ expect(done).toBe(true);
+ expect(cancelled).toBe(false);
+ });
+ });
+
+ it('should reject the promise when cancel() is called', function() {
+ module(function($animateProvider) {
+ $animateProvider.register('.the-end', function() {
+ return { beforeAddClass: noop };
+ });
+ });
+ inject(function($$animateJs, $$rAF, $rootScope) {
+ var element = jqLite('
');
+ var animator = $$animateJs(element, 'addClass');
+ var runner = animator.start();
+ var done = false;
+ var cancelled = false;
+ runner.then(function() {
+ done = true;
+ }, function() {
+ cancelled = true;
+ });
+
+ runner.cancel();
+ $$rAF.flush();
+ $rootScope.$digest();
+ expect(done).toBe(false);
+ expect(cancelled).toBe(true);
+ });
+ });
+
+ describe("events", function() {
+ var animations, runAnimation, element, log;
+ beforeEach(module(function($animateProvider) {
+ element = jqLite('
');
+ animations = {};
+ log = [];
+
+ $animateProvider.register('.test-animation', function() {
+ return animations;
+ });
+
+ return function($$animateJs) {
+ runAnimation = function(method, done, error, options) {
+ options = extend(options || {}, {
+ domOperation: function() {
+ log.push('dom ' + method);
+ }
+ });
+
+ var driver = $$animateJs(element, method, 'test-animation', options);
+ driver.start().done(function(status) {
+ ((status ? done : error) || noop)();
+ });
+ };
+ };
+ }));
+
+ they("$prop should have the function signature of (element, done, options) for the after animation",
+ ['enter', 'move', 'leave'], function(event) {
+ inject(function() {
+ var args;
+ var animationOptions = {};
+ animationOptions.foo = 'bar';
+ animations[event] = function() {
+ args = arguments;
+ };
+ runAnimation(event, noop, noop, animationOptions);
+
+ expect(args.length).toBe(3);
+ expect(args[0]).toBe(element);
+ expect(isFunction(args[1])).toBe(true);
+ expect(args[2].foo).toBe(animationOptions.foo);
+ });
+ });
+
+ they("$prop should not execute a before function", enterMoveEvents, function(event) {
+ inject(function() {
+ var args;
+ var beforeMethod = 'before' + event.charAt(0).toUpperCase() + event.substr(1);
+ var animationOptions = {};
+ animations[beforeMethod] = function() {
+ args = arguments;
+ };
+
+ runAnimation(event, noop, noop, animationOptions);
+ expect(args).toBeFalsy();
+ });
+ });
+
+ they("$prop should have the function signature of (element, className, done, options) for the before animation",
+ ['addClass', 'removeClass'], function(event) {
+ inject(function() {
+ var beforeMethod = 'before' + event.charAt(0).toUpperCase() + event.substr(1);
+ var args;
+ var className = 'matias';
+ animations[beforeMethod] = function() {
+ args = arguments;
+ };
+
+ var animationOptions = {};
+ animationOptions.foo = 'bar';
+ animationOptions[event] = className;
+ runAnimation(event, noop, noop, animationOptions);
+
+ expect(args.length).toBe(4);
+ expect(args[0]).toBe(element);
+ expect(args[1]).toBe(className);
+ expect(isFunction(args[2])).toBe(true);
+ expect(args[3].foo).toBe(animationOptions.foo);
+ });
+ });
+
+ they("$prop should have the function signature of (element, className, done, options) for the after animation",
+ ['addClass', 'removeClass'], function(event) {
+ inject(function() {
+ var args;
+ var className = 'fatias';
+ animations[event] = function() {
+ args = arguments;
+ };
+
+ var animationOptions = {};
+ animationOptions.foo = 'bar';
+ animationOptions[event] = className;
+ runAnimation(event, noop, noop, animationOptions);
+
+ expect(args.length).toBe(4);
+ expect(args[0]).toBe(element);
+ expect(args[1]).toBe(className);
+ expect(isFunction(args[2])).toBe(true);
+ expect(args[3].foo).toBe(animationOptions.foo);
+ });
+ });
+
+ they("setClass should have the function signature of (element, addClass, removeClass, done, options) for the $prop animation", ['before', 'after'], function(event) {
+ inject(function() {
+ var args;
+ var method = event === 'before' ? 'beforeSetClass' : 'setClass';
+ animations[method] = function() {
+ args = arguments;
+ };
+
+ var addClass = 'on';
+ var removeClass = 'on';
+ var animationOptions = {
+ foo: 'bar',
+ addClass: addClass,
+ removeClass: removeClass
+ };
+ runAnimation('setClass', noop, noop, animationOptions);
+
+ expect(args.length).toBe(5);
+ expect(args[0]).toBe(element);
+ expect(args[1]).toBe(addClass);
+ expect(args[2]).toBe(removeClass);
+ expect(isFunction(args[3])).toBe(true);
+ expect(args[4].foo).toBe(animationOptions.foo);
+ });
+ });
+
+ they("animate should have the function signature of (element, from, to, done, options) for the $prop animation", ['before', 'after'], function(event) {
+ inject(function() {
+ var args;
+ var method = event === 'before' ? 'beforeAnimate' : 'animate';
+ animations[method] = function() {
+ args = arguments;
+ };
+
+ var to = { color: 'red' };
+ var from = { color: 'blue' };
+ var animationOptions = {
+ foo: 'bar',
+ to: to,
+ from: from
+ };
+ runAnimation('animate', noop, noop, animationOptions);
+
+ expect(args.length).toBe(5);
+ expect(args[0]).toBe(element);
+ expect(args[1]).toBe(from);
+ expect(args[2]).toBe(to);
+ expect(isFunction(args[3])).toBe(true);
+ expect(args[4].foo).toBe(animationOptions.foo);
+ });
+ });
+
+ they("custom events should have the function signature of (element, done, options) for the $prop animation", ['before', 'after'], function(event) {
+ inject(function() {
+ var args;
+ var method = event === 'before' ? 'beforeCustom' : 'custom';
+ animations[method] = function() {
+ args = arguments;
+ };
+
+ var animationOptions = {};
+ animationOptions.foo = 'bar';
+ runAnimation('custom', noop, noop, animationOptions);
+
+ expect(args.length).toBe(3);
+ expect(args[0]).toBe(element);
+ expect(isFunction(args[1])).toBe(true);
+ expect(args[2].foo).toBe(animationOptions.foo);
+ });
+ });
+
+ var enterMoveEvents = ['enter', 'move'];
+ var otherEvents = ['addClass', 'removeClass', 'setClass'];
+ var allEvents = ['leave'].concat(otherEvents).concat(enterMoveEvents);
+
+ they("$prop should asynchronously render the before$prop animation", otherEvents, function(event) {
+ inject(function($$rAF) {
+ var beforeMethod = 'before' + event.charAt(0).toUpperCase() + event.substr(1);
+ animations[beforeMethod] = function(element, a, b, c) {
+ log.push('before ' + event);
+ var done = getDoneFunction(arguments);
+ done();
+ };
+
+ runAnimation(event);
+ expect(log).toEqual(['before ' + event]);
+ $$rAF.flush();
+
+ expect(log).toEqual(['before ' + event, 'dom ' + event]);
+ });
+ });
+
+ they("$prop should asynchronously render the $prop animation", allEvents, function(event) {
+ inject(function($$rAF) {
+ animations[event] = function(element, a, b, c) {
+ log.push('after ' + event);
+ var done = getDoneFunction(arguments);
+ done();
+ };
+
+ runAnimation(event, function() {
+ log.push('complete');
+ });
+
+ if (event === 'leave') {
+ expect(log).toEqual(['after leave']);
+ $$rAF.flush();
+ expect(log).toEqual(['after leave', 'dom leave', 'complete']);
+ } else {
+ expect(log).toEqual(['dom ' + event, 'after ' + event]);
+ $$rAF.flush();
+ expect(log).toEqual(['dom ' + event, 'after ' + event, 'complete']);
+ }
+ });
+ });
+
+ they("$prop should asynchronously render the $prop animation when a start/end animator object is returned",
+ allEvents, function(event) {
+
+ inject(function($$rAF, $$AnimateRunner) {
+ var runner;
+ animations[event] = function(element, a, b, c) {
+ return {
+ start: function() {
+ log.push('start ' + event);
+ return runner = new $$AnimateRunner();
+ }
+ };
+ };
+
+ runAnimation(event, function() {
+ log.push('complete');
+ });
+
+ if (event === 'leave') {
+ expect(log).toEqual(['start leave']);
+ runner.end();
+ $$rAF.flush();
+ expect(log).toEqual(['start leave', 'dom leave', 'complete']);
+ } else {
+ expect(log).toEqual(['dom ' + event, 'start ' + event]);
+ runner.end();
+ $$rAF.flush();
+ expect(log).toEqual(['dom ' + event, 'start ' + event, 'complete']);
+ }
+ });
+ });
+
+ they("$prop should asynchronously render the $prop animation when an instance of $$AnimateRunner is returned",
+ allEvents, function(event) {
+
+ inject(function($$rAF, $$AnimateRunner) {
+ var runner;
+ animations[event] = function(element, a, b, c) {
+ log.push('start ' + event);
+ return runner = new $$AnimateRunner();
+ };
+
+ runAnimation(event, function() {
+ log.push('complete');
+ });
+
+ if (event === 'leave') {
+ expect(log).toEqual(['start leave']);
+ runner.end();
+ $$rAF.flush();
+ expect(log).toEqual(['start leave', 'dom leave', 'complete']);
+ } else {
+ expect(log).toEqual(['dom ' + event, 'start ' + event]);
+ runner.end();
+ $$rAF.flush();
+ expect(log).toEqual(['dom ' + event, 'start ' + event, 'complete']);
+ }
+ });
+ });
+
+ they("$prop should asynchronously reject the before animation if the callback function is called with false", otherEvents, function(event) {
+ inject(function($$rAF, $rootScope) {
+ var beforeMethod = 'before' + event.charAt(0).toUpperCase() + event.substr(1);
+ animations[beforeMethod] = function(element, a, b, c) {
+ log.push('before ' + event);
+ var done = getDoneFunction(arguments);
+ done(false);
+ };
+
+ animations[event] = function(element, a, b, c) {
+ log.push('after ' + event);
+ var done = getDoneFunction(arguments);
+ done();
+ };
+
+ runAnimation(event,
+ function() { log.push('pass'); },
+ function() { log.push('fail'); });
+
+ expect(log).toEqual(['before ' + event]);
+ $$rAF.flush();
+ expect(log).toEqual(['before ' + event, 'dom ' + event, 'fail']);
+ });
+ });
+
+ they("$prop should asynchronously reject the after animation if the callback function is called with false", allEvents, function(event) {
+ inject(function($$rAF, $rootScope) {
+ animations[event] = function(element, a, b, c) {
+ log.push('after ' + event);
+ var done = getDoneFunction(arguments);
+ done(false);
+ };
+
+ runAnimation(event,
+ function() { log.push('pass'); },
+ function() { log.push('fail'); });
+
+ var expectations = [];
+ if (event === 'leave') {
+ expect(log).toEqual(['after leave']);
+ $$rAF.flush();
+ expect(log).toEqual(['after leave', 'dom leave', 'fail']);
+ } else {
+ expect(log).toEqual(['dom ' + event, 'after ' + event]);
+ $$rAF.flush();
+ expect(log).toEqual(['dom ' + event, 'after ' + event, 'fail']);
+ }
+ });
+ });
+
+ it('setClass should delegate down to addClass/removeClass if not defined', inject(function($$rAF) {
+ animations.addClass = function(element, done) {
+ log.push('addClass');
+ };
+
+ animations.removeClass = function(element, done) {
+ log.push('removeClass');
+ };
+
+ expect(animations.setClass).toBeFalsy();
+
+ runAnimation('setClass');
+
+ expect(log).toEqual(['dom setClass', 'removeClass', 'addClass']);
+ }));
+
+ it('beforeSetClass should delegate down to beforeAddClass/beforeRemoveClass if not defined',
+ inject(function($$rAF) {
+
+ animations.beforeAddClass = function(element, className, done) {
+ log.push('beforeAddClass');
+ done();
+ };
+
+ animations.beforeRemoveClass = function(element, className, done) {
+ log.push('beforeRemoveClass');
+ done();
+ };
+
+ expect(animations.setClass).toBeFalsy();
+
+ runAnimation('setClass');
+ $$rAF.flush();
+
+ expect(log).toEqual(['beforeRemoveClass', 'beforeAddClass', 'dom setClass']);
+ }));
+
+ it('leave should always ignore the `beforeLeave` animation',
+ inject(function($$rAF) {
+
+ animations.beforeLeave = function(element, done) {
+ log.push('beforeLeave');
+ done();
+ };
+
+ animations.leave = function(element, done) {
+ log.push('leave');
+ done();
+ };
+
+ runAnimation('leave');
+ $$rAF.flush();
+
+ expect(log).toEqual(['leave', 'dom leave']);
+ }));
+
+ it('should allow custom events to be triggered',
+ inject(function($$rAF) {
+
+ animations.beforeFlex = function(element, done) {
+ log.push('beforeFlex');
+ done();
+ };
+
+ animations.flex = function(element, done) {
+ log.push('flex');
+ done();
+ };
+
+ runAnimation('flex');
+ $$rAF.flush();
+
+ expect(log).toEqual(['beforeFlex', 'dom flex', 'flex']);
+ }));
+ });
+});
diff --git a/test/ngAnimate/animateRunnerSpec.js b/test/ngAnimate/animateRunnerSpec.js
new file mode 100644
index 000000000000..94178fba6189
--- /dev/null
+++ b/test/ngAnimate/animateRunnerSpec.js
@@ -0,0 +1,340 @@
+'use strict';
+
+describe('$$rAFMutex', function() {
+ beforeEach(module('ngAnimate'));
+
+ it('should fire the callback only when one or more RAFs have passed',
+ inject(function($$rAF, $$rAFMutex) {
+
+ var trigger = $$rAFMutex();
+ var called = false;
+ trigger(function() {
+ called = true;
+ });
+
+ expect(called).toBe(false);
+ $$rAF.flush();
+ expect(called).toBe(true);
+ }));
+
+ it('should immediately fire the callback if a RAF has passed since construction',
+ inject(function($$rAF, $$rAFMutex) {
+
+ var trigger = $$rAFMutex();
+ $$rAF.flush();
+
+ var called = false;
+ trigger(function() {
+ called = true;
+ });
+ expect(called).toBe(true);
+ }));
+});
+
+describe("$$AnimateRunner", function() {
+
+ beforeEach(module('ngAnimate'));
+
+ they("should trigger the host $prop function",
+ ['end', 'cancel', 'pause', 'resume'], function(method) {
+
+ inject(function($$AnimateRunner) {
+ var host = {};
+ var spy = host[method] = jasmine.createSpy();
+ var runner = new $$AnimateRunner(host);
+ runner[method]();
+ expect(spy).toHaveBeenCalled();
+ });
+ });
+
+ they("should trigger the inner runner's host $prop function",
+ ['end', 'cancel', 'pause', 'resume'], function(method) {
+
+ inject(function($$AnimateRunner) {
+ var host = {};
+ var spy = host[method] = jasmine.createSpy();
+ var runner1 = new $$AnimateRunner();
+ var runner2 = new $$AnimateRunner(host);
+ runner1.setHost(runner2);
+ runner1[method]();
+ expect(spy).toHaveBeenCalled();
+ });
+ });
+
+ it("should resolve the done function only if one RAF has passed",
+ inject(function($$AnimateRunner, $$rAF) {
+
+ var runner = new $$AnimateRunner();
+ var spy = jasmine.createSpy();
+ runner.done(spy);
+ runner.complete(true);
+ expect(spy).not.toHaveBeenCalled();
+ $$rAF.flush();
+ expect(spy).toHaveBeenCalled();
+ }));
+
+ it("should resolve with the status provided in the completion function",
+ inject(function($$AnimateRunner, $$rAF) {
+
+ var runner = new $$AnimateRunner();
+ var capturedValue;
+ runner.done(function(val) {
+ capturedValue = val;
+ });
+ runner.complete('special value');
+ $$rAF.flush();
+ expect(capturedValue).toBe('special value');
+ }));
+
+ they("should immediately resolve each combined runner in a bottom-up order when $prop is called",
+ ['end', 'cancel'], function(method) {
+
+ inject(function($$AnimateRunner, $$rAF) {
+ var runner1 = new $$AnimateRunner();
+ var runner2 = new $$AnimateRunner();
+ runner1.setHost(runner2);
+
+ var status1, status2, signature = '';
+ runner1.done(function(status) {
+ signature += '1';
+ status1 = status;
+ });
+
+ runner2.done(function(status) {
+ signature += '2';
+ status2 = status;
+ });
+
+ runner1[method]();
+
+ var expectedStatus = method === 'end' ? true : false;
+ expect(status1).toBe(expectedStatus);
+ expect(status2).toBe(expectedStatus);
+ expect(signature).toBe('21');
+ });
+ });
+
+ they("should resolve/reject using a newly created promise when .then() is used upon $prop",
+ ['end', 'cancel'], function(method) {
+
+ inject(function($$AnimateRunner, $rootScope) {
+ var runner1 = new $$AnimateRunner();
+ var runner2 = new $$AnimateRunner();
+ runner1.setHost(runner2);
+
+ var status1;
+ runner1.then(
+ function() { status1 = 'pass'; },
+ function() { status1 = 'fail'; });
+
+ var status2;
+ runner2.then(
+ function() { status2 = 'pass'; },
+ function() { status2 = 'fail'; });
+
+ runner1[method]();
+
+ var expectedStatus = method === 'end' ? 'pass' : 'fail';
+
+ expect(status1).toBeUndefined();
+ expect(status2).toBeUndefined();
+
+ $rootScope.$digest();
+ expect(status1).toBe(expectedStatus);
+ expect(status2).toBe(expectedStatus);
+ });
+ });
+
+ it("should expose/create the contained promise when getPromise() is called",
+ inject(function($$AnimateRunner, $rootScope) {
+
+ var runner = new $$AnimateRunner();
+ expect(isPromiseLike(runner.getPromise())).toBeTruthy();
+ }));
+
+ it("should expose the `catch` promise function to handle the rejected state",
+ inject(function($$AnimateRunner, $rootScope) {
+
+ var runner = new $$AnimateRunner();
+ var animationFailed = false;
+ runner.catch(function() {
+ animationFailed = true;
+ });
+ runner.cancel();
+ $rootScope.$digest();
+ expect(animationFailed).toBe(true);
+ }));
+
+ they("should expose the `finally` promise function to handle the final state when $prop",
+ { 'rejected': 'cancel', 'resolved': 'end' }, function(method) {
+ inject(function($$AnimateRunner, $rootScope) {
+ var runner = new $$AnimateRunner();
+ var animationComplete = false;
+ runner.finally(function() {
+ animationComplete = true;
+ });
+ runner[method]();
+ $rootScope.$digest();
+ expect(animationComplete).toBe(true);
+ });
+ });
+
+ describe(".all()", function() {
+ it("should resolve when all runners have naturally resolved",
+ inject(function($$rAF, $$AnimateRunner) {
+
+ var runner1 = new $$AnimateRunner();
+ var runner2 = new $$AnimateRunner();
+ var runner3 = new $$AnimateRunner();
+
+ var status;
+ $$AnimateRunner.all([runner1, runner2, runner3], function(response) {
+ status = response;
+ });
+
+ runner1.complete(true);
+ runner2.complete(true);
+ runner3.complete(true);
+
+ expect(status).toBeUndefined();
+
+ $$rAF.flush();
+
+ expect(status).toBe(true);
+ }));
+
+ they("should immediately resolve if and when all runners have been $prop",
+ { ended: 'end', cancelled: 'cancel' }, function(method) {
+
+ inject(function($$rAF, $$AnimateRunner) {
+ var runner1 = new $$AnimateRunner();
+ var runner2 = new $$AnimateRunner();
+ var runner3 = new $$AnimateRunner();
+
+ var expectedStatus = method === 'end' ? true : false;
+
+ var status;
+ $$AnimateRunner.all([runner1, runner2, runner3], function(response) {
+ status = response;
+ });
+
+ runner1[method]();
+ runner2[method]();
+ runner3[method]();
+
+ expect(status).toBe(expectedStatus);
+ });
+ });
+
+ it("should return a status of `false` if one or more runners was cancelled",
+ inject(function($$rAF, $$AnimateRunner) {
+
+ var runner1 = new $$AnimateRunner();
+ var runner2 = new $$AnimateRunner();
+ var runner3 = new $$AnimateRunner();
+
+ var status;
+ $$AnimateRunner.all([runner1, runner2, runner3], function(response) {
+ status = response;
+ });
+
+ runner1.end();
+ runner2.end();
+ runner3.cancel();
+
+ expect(status).toBe(false);
+ }));
+ });
+
+ describe(".chain()", function() {
+ it("should evaluate an array of functions in a chain",
+ inject(function($$rAF, $$AnimateRunner) {
+
+ var runner1 = new $$AnimateRunner();
+ var runner2 = new $$AnimateRunner();
+ var runner3 = new $$AnimateRunner();
+
+ var log = [];
+
+ var items = [];
+ items.push(function(fn) {
+ runner1.done(function() {
+ log.push(1);
+ fn();
+ });
+ });
+
+ items.push(function(fn) {
+ runner2.done(function() {
+ log.push(2);
+ fn();
+ });
+ });
+
+ items.push(function(fn) {
+ runner3.done(function() {
+ log.push(3);
+ fn();
+ });
+ });
+
+ var status;
+ $$AnimateRunner.chain(items, function(response) {
+ status = response;
+ });
+
+ $$rAF.flush();
+
+ runner2.complete(true);
+ expect(log).toEqual([]);
+ expect(status).toBeUndefined();
+
+ runner1.complete(true);
+ expect(log).toEqual([1,2]);
+ expect(status).toBeUndefined();
+
+ runner3.complete(true);
+ expect(log).toEqual([1,2,3]);
+ expect(status).toBe(true);
+ }));
+
+ it("should break the chian when a function evaluates to false",
+ inject(function($$rAF, $$AnimateRunner) {
+
+ var runner1 = new $$AnimateRunner();
+ var runner2 = new $$AnimateRunner();
+ var runner3 = new $$AnimateRunner();
+ var runner4 = new $$AnimateRunner();
+ var runner5 = new $$AnimateRunner();
+ var runner6 = new $$AnimateRunner();
+
+ var log = [];
+
+ var items = [];
+ items.push(function(fn) { log.push(1); runner1.done(fn); });
+ items.push(function(fn) { log.push(2); runner2.done(fn); });
+ items.push(function(fn) { log.push(3); runner3.done(fn); });
+ items.push(function(fn) { log.push(4); runner4.done(fn); });
+ items.push(function(fn) { log.push(5); runner5.done(fn); });
+ items.push(function(fn) { log.push(6); runner6.done(fn); });
+
+ var status;
+ $$AnimateRunner.chain(items, function(response) {
+ status = response;
+ });
+
+ runner1.complete('');
+ runner2.complete(null);
+ runner3.complete(undefined);
+ runner4.complete(0);
+ runner5.complete(false);
+
+ runner6.complete(true);
+
+ $$rAF.flush();
+
+ expect(log).toEqual([1,2,3,4,5]);
+ expect(status).toBe(false);
+ }));
+ });
+});
diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js
index be5a1b59e27e..7d2b96bc2085 100644
--- a/test/ngAnimate/animateSpec.js
+++ b/test/ngAnimate/animateSpec.js
@@ -1,5578 +1,1779 @@
'use strict';
-describe("ngAnimate", function() {
- var $originalAnimate;
- beforeEach(module(function($provide) {
- $provide.decorator('$animate', function($delegate) {
- $originalAnimate = $delegate;
- return $delegate;
- });
- }));
- beforeEach(module('ngAnimate'));
- beforeEach(module('ngAnimateMock'));
-
- function getMaxValue(prop, element, $window) {
- var node = element[0];
- var cs = $window.getComputedStyle(node);
- var prop0 = 'webkit' + prop.charAt(0).toUpperCase() + prop.substr(1);
- var values = (cs[prop0] || cs[prop]).split(/\s*,\s*/);
- var maxDelay = 0;
- forEach(values, function(value) {
- maxDelay = Math.max(parseFloat(value) || 0, maxDelay);
- });
- return maxDelay;
- }
-
- it("should disable animations on bootstrap for structural animations even after the first digest has passed", function() {
- var hasBeenAnimated = false;
- module(function($animateProvider) {
- $animateProvider.register('.my-structrual-animation', function() {
- return {
- enter: function(element, done) {
- hasBeenAnimated = true;
- done();
- },
- leave: function(element, done) {
- hasBeenAnimated = true;
- done();
- }
- };
- });
- });
- inject(function($rootScope, $compile, $animate, $rootElement, $document) {
- var element = $compile('
...
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
-
- $animate.enter(element, $rootElement);
- $rootScope.$digest();
-
- expect(hasBeenAnimated).toBe(false);
-
- $animate.leave(element);
- $rootScope.$digest();
-
- expect(hasBeenAnimated).toBe(true);
- });
- });
-
- it("should disable animations for two digests until all pending HTTP requests are complete during bootstrap", function() {
- var animateSpy = jasmine.createSpy();
- module(function($animateProvider, $compileProvider) {
- $compileProvider.directive('myRemoteDirective', function() {
- return {
- templateUrl: 'remote.html'
- };
- });
- $animateProvider.register('.my-structrual-animation', function() {
- return {
- enter: animateSpy,
- leave: animateSpy
- };
- });
- });
- inject(function($rootScope, $compile, $animate, $rootElement, $document, $httpBackend) {
-
- $httpBackend.whenGET('remote.html').respond(200, '
content ');
-
- var element = $compile('
...
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
-
- // running this twice just to prove that the dual post digest is run
- $rootScope.$digest();
- $rootScope.$digest();
-
- $animate.enter(element, $rootElement);
- $rootScope.$digest();
-
- expect(animateSpy).not.toHaveBeenCalled();
-
- $httpBackend.flush();
- $rootScope.$digest();
-
- $animate.leave(element);
- $rootScope.$digest();
-
- expect(animateSpy).toHaveBeenCalled();
- });
- });
-
-
- //we use another describe block because the before/after operations below
- //are used across all animations tests and we don't want that same behavior
- //to be used on the root describe block at the start of the animateSpec.js file
- describe('', function() {
-
- var ss, body;
- beforeEach(module(function() {
- body = jqLite(document.body);
- return function($window, $document, $animate, $timeout, $rootScope) {
- ss = createMockStyleSheet($document, $window);
- try {
- $timeout.flush();
- } catch (e) {}
- $animate.enabled(true);
- $rootScope.$digest();
- };
- }));
-
- afterEach(function() {
- if (ss) {
- ss.destroy();
- }
- dealoc(body);
- });
-
-
- describe("$animate", function() {
-
- var element, $rootElement;
-
- function html(content) {
- body.append($rootElement);
- $rootElement.html(content);
- element = $rootElement.children().eq(0);
- return element;
- }
-
- describe("enable / disable", function() {
-
- it("should work for all animations", inject(function($animate) {
-
- expect($animate.enabled()).toBe(true);
-
- expect($animate.enabled(0)).toBe(false);
- expect($animate.enabled()).toBe(false);
-
- expect($animate.enabled(1)).toBe(true);
- expect($animate.enabled()).toBe(true);
- }));
-
-
- it('should place a hard disable on all child animations', function() {
- var count = 0;
- module(function($animateProvider) {
- $animateProvider.register('.animated', function() {
- return {
- addClass: function(element, className, done) {
- count++;
- done();
- }
- };
- });
- });
- inject(function($compile, $rootScope, $animate, $sniffer, $rootElement) {
- $animate.enabled(true);
-
- var elm1 = $compile('
')($rootScope);
- var elm2 = $compile('
')($rootScope);
- $rootElement.append(elm1);
- angular.element(document.body).append($rootElement);
-
- $animate.addClass(elm1, 'klass');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(count).toBe(1);
-
- $animate.enabled(false);
-
- $animate.addClass(elm1, 'klass2');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(count).toBe(1);
-
- $animate.enabled(true);
-
- elm1.append(elm2);
-
- $animate.addClass(elm2, 'klass');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(count).toBe(2);
-
- $animate.enabled(false, elm1);
-
- $animate.addClass(elm2, 'klass2');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(count).toBe(2);
-
- var root = angular.element($rootElement[0]);
- $rootElement.addClass('animated');
- $animate.addClass(root, 'klass2');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(count).toBe(3);
- });
- });
-
-
- it('should skip animations if the element is attached to the $rootElement', function() {
- var count = 0;
- module(function($animateProvider) {
- $animateProvider.register('.animated', function() {
- return {
- addClass: function(element, className, done) {
- count++;
- done();
- }
- };
- });
- });
- inject(function($compile, $rootScope, $animate) {
- $animate.enabled(true);
-
- var elm1 = $compile('
')($rootScope);
-
- $animate.addClass(elm1, 'klass2');
- $rootScope.$digest();
- expect(count).toBe(0);
- });
- });
-
-
- it('should check enable/disable animations up until the $rootElement element', function() {
- var rootElm = jqLite('
');
-
- var captured = false;
- module(function($provide, $animateProvider) {
- $provide.value('$rootElement', rootElm);
- $animateProvider.register('.capture-animation', function() {
- return {
- addClass: function(element, className, done) {
- captured = true;
- done();
- }
- };
- });
- });
- inject(function($animate, $rootElement, $rootScope, $compile) {
- angular.bootstrap(rootElm, ['ngAnimate']);
-
- $animate.enabled(true);
-
- var element = $compile('
')($rootScope);
- rootElm.append(element);
-
- expect(captured).toBe(false);
- $animate.addClass(element, 'red');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(captured).toBe(true);
-
- captured = false;
- $animate.enabled(false);
-
- $animate.addClass(element, 'blue');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(captured).toBe(false);
-
- //clean up the mess
- $animate.enabled(false, rootElm);
- dealoc(rootElm);
- });
- });
- });
-
-
- describe("with polyfill", function() {
-
- var child, after;
-
- beforeEach(function() {
- module(function($animateProvider) {
- $animateProvider.register('.custom', function() {
- return {
- start: function(element, done) {
- done();
- }
- };
- });
- $animateProvider.register('.custom-delay', function($timeout) {
- function animate(element, done) {
- done = arguments.length == 4 ? arguments[2] : done;
- $timeout(done, 2000, false);
- return function() {
- element.addClass('animation-cancelled');
- };
- }
- return {
- leave: animate,
- addClass: animate,
- removeClass: animate
- };
- });
- $animateProvider.register('.custom-long-delay', function($timeout) {
- function animate(element, done) {
- done = arguments.length == 4 ? arguments[2] : done;
- $timeout(done, 20000, false);
- return function(cancelled) {
- element.addClass(cancelled ? 'animation-cancelled' : 'animation-ended');
- };
- }
- return {
- leave: animate,
- addClass: animate,
- removeClass: animate
- };
- });
- $animateProvider.register('.setup-memo', function() {
- return {
- removeClass: function(element, className, done) {
- element.text('memento');
- done();
- }
- };
- });
- return function($animate, $compile, $rootScope, $rootElement) {
- element = $compile('
')($rootScope);
-
- forEach(['.ng-hide-add', '.ng-hide-remove', '.ng-enter', '.ng-leave', '.ng-move', '.my-inline-animation'], function(selector) {
- ss.addRule(selector, '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
- });
-
- child = $compile('
...
')($rootScope);
- jqLite($document[0].body).append($rootElement);
- element.append(child);
-
- after = $compile('
')($rootScope);
- $rootElement.append(element);
- };
- });
- });
-
-
- it("should animate the enter animation event",
- inject(function($animate, $rootScope, $sniffer) {
- element[0].removeChild(child[0]);
-
- expect(element.contents().length).toBe(0);
- $animate.enter(child, element);
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(child.hasClass('ng-enter')).toBe(true);
- expect(child.hasClass('ng-enter-active')).toBe(true);
- browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
-
- expect(element.contents().length).toBe(1);
- }));
-
- it("should animate the enter animation event with native dom elements",
- inject(function($animate, $rootScope, $sniffer) {
- element[0].removeChild(child[0]);
-
- expect(element.contents().length).toBe(0);
- $animate.enter(child[0], element[0]);
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(child.hasClass('ng-enter')).toBe(true);
- expect(child.hasClass('ng-enter-active')).toBe(true);
- browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
-
- expect(element.contents().length).toBe(1);
- }));
-
-
- it("should animate the leave animation event",
- inject(function($animate, $rootScope, $sniffer) {
-
- expect(element.contents().length).toBe(1);
- $animate.leave(child);
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(child.hasClass('ng-leave')).toBe(true);
- expect(child.hasClass('ng-leave-active')).toBe(true);
- browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
-
- expect(element.contents().length).toBe(0);
- }));
-
- it("should animate the leave animation event with native dom elements",
- inject(function($animate, $rootScope, $sniffer) {
-
- expect(element.contents().length).toBe(1);
- $animate.leave(child[0]);
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(child.hasClass('ng-leave')).toBe(true);
- expect(child.hasClass('ng-leave-active')).toBe(true);
- browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
-
- expect(element.contents().length).toBe(0);
- }));
-
- it("should animate the move animation event",
- inject(function($animate, $compile, $rootScope, $timeout, $sniffer) {
-
- $rootScope.$digest();
- element.empty();
-
- var child1 = $compile('
1
')($rootScope);
- var child2 = $compile('
2
')($rootScope);
- element.append(child1);
- element.append(child2);
- expect(element.text()).toBe('12');
- $animate.move(child1, element, child2);
- $rootScope.$digest();
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- }
- expect(element.text()).toBe('21');
- }));
-
- it("should animate the move animation event with native dom elements",
- inject(function($animate, $compile, $rootScope, $timeout, $sniffer) {
-
- $rootScope.$digest();
- element.empty();
-
- var child1 = $compile('
1
')($rootScope);
- var child2 = $compile('
2
')($rootScope);
- element.append(child1);
- element.append(child2);
- expect(element.text()).toBe('12');
- $animate.move(child1[0], element[0], child2[0]);
- $rootScope.$digest();
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- }
- expect(element.text()).toBe('21');
- }));
-
- it("should perform the animate event",
- inject(function($animate, $compile, $rootScope, $timeout, $sniffer) {
-
- $rootScope.$digest();
- $animate.animate(element, { color: 'rgb(255, 0, 0)' }, { color: 'rgb(0, 0, 255)' }, 'animated');
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- expect(element.css('color')).toBe('rgb(255, 0, 0)');
- $animate.triggerReflow();
- }
- expect(element.css('color')).toBe('rgb(0, 0, 255)');
- }));
-
- it("should animate the show animation event",
- inject(function($animate, $rootScope, $sniffer) {
-
- $rootScope.$digest();
- child.addClass('ng-hide');
- expect(child).toBeHidden();
- $animate.removeClass(child, 'ng-hide');
- $rootScope.$digest();
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(child.hasClass('ng-hide-remove')).toBe(true);
- expect(child.hasClass('ng-hide-remove-active')).toBe(true);
- browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
- expect(child.hasClass('ng-hide-remove')).toBe(false);
- expect(child.hasClass('ng-hide-remove-active')).toBe(false);
- expect(child).toBeShown();
- }));
-
- it("should animate the hide animation event",
- inject(function($animate, $rootScope, $sniffer) {
-
- $rootScope.$digest();
- expect(child).toBeShown();
- $animate.addClass(child, 'ng-hide');
- $rootScope.$digest();
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(child.hasClass('ng-hide-add')).toBe(true);
- expect(child.hasClass('ng-hide-add-active')).toBe(true);
- browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
- expect(child).toBeHidden();
- }));
-
-
- it("should exclusively animate the setClass animation event", function() {
- var count = 0, fallback = jasmine.createSpy('callback');
- module(function($animateProvider) {
- $animateProvider.register('.classify', function() {
- return {
- beforeAddClass: fallback,
- addClass: fallback,
- beforeRemoveClass: fallback,
- removeClass: fallback,
-
- beforeSetClass: function(element, add, remove, done) {
- count++;
- expect(add).toBe('yes');
- expect(remove).toBe('no');
- done();
- },
- setClass: function(element, add, remove, done) {
- count++;
- expect(add).toBe('yes');
- expect(remove).toBe('no');
- done();
- }
- };
- });
- });
- inject(function($animate, $rootScope) {
- child.attr('class','classify no');
- $animate.setClass(child, 'yes', 'no');
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect(child.hasClass('yes')).toBe(true);
- expect(child.hasClass('no')).toBe(false);
- expect(count).toBe(2);
-
- expect(fallback).not.toHaveBeenCalled();
- });
- });
-
- it("should exclusively animate the setClass animation event with native dom elements", function() {
- var count = 0, fallback = jasmine.createSpy('callback');
- module(function($animateProvider) {
- $animateProvider.register('.classify', function() {
- return {
- beforeAddClass: fallback,
- addClass: fallback,
- beforeRemoveClass: fallback,
- removeClass: fallback,
-
- beforeSetClass: function(element, add, remove, done) {
- count++;
- expect(add).toBe('yes');
- expect(remove).toBe('no');
- done();
- },
- setClass: function(element, add, remove, done) {
- count++;
- expect(add).toBe('yes');
- expect(remove).toBe('no');
- done();
- }
- };
- });
- });
- inject(function($animate, $rootScope) {
- child.attr('class','classify no');
- $animate.setClass(child[0], 'yes', 'no');
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect(child.hasClass('yes')).toBe(true);
- expect(child.hasClass('no')).toBe(false);
- expect(count).toBe(2);
-
- expect(fallback).not.toHaveBeenCalled();
- });
- });
-
- it("should delegate down to addClass/removeClass if a setClass animation is not found", function() {
- var count = 0;
- module(function($animateProvider) {
- $animateProvider.register('.classify', function() {
- return {
- beforeAddClass: function(element, className, done) {
- count++;
- expect(className).toBe('yes');
- done();
- },
- addClass: function(element, className, done) {
- count++;
- expect(className).toBe('yes');
- done();
- },
- beforeRemoveClass: function(element, className, done) {
- count++;
- expect(className).toBe('no');
- done();
- },
- removeClass: function(element, className, done) {
- count++;
- expect(className).toBe('no');
- done();
- }
- };
- });
- });
- inject(function($animate, $rootScope) {
- child.attr('class','classify no');
- $animate.setClass(child, 'yes', 'no');
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect(child.hasClass('yes')).toBe(true);
- expect(child.hasClass('no')).toBe(false);
- expect(count).toBe(4);
- });
- });
-
- it("should assign the ng-event className to all animation events when transitions/keyframes are used",
- inject(function($animate, $sniffer, $rootScope) {
-
- if (!$sniffer.transitions) return;
-
- $rootScope.$digest();
- element[0].removeChild(child[0]);
-
- //enter
- $animate.enter(child, element);
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect(child.attr('class')).toContain('ng-enter');
- expect(child.attr('class')).toContain('ng-enter-active');
- browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- $animate.triggerCallbackPromise();
-
- //move
- element.append(after);
- $animate.move(child, element, after);
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect(child.attr('class')).toContain('ng-move');
- expect(child.attr('class')).toContain('ng-move-active');
- browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- $animate.triggerCallbackPromise();
-
- //hide
- $animate.addClass(child, 'ng-hide');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(child.attr('class')).toContain('ng-hide-add');
- expect(child.attr('class')).toContain('ng-hide-add-active');
- browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
-
- //show
- $animate.removeClass(child, 'ng-hide');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(child.attr('class')).toContain('ng-hide-remove');
- expect(child.attr('class')).toContain('ng-hide-remove-active');
- browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
-
- //animate
- $animate.animate(child, null, null, 'my-inline-animation');
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect(child.attr('class')).toContain('my-inline-animation');
- expect(child.attr('class')).toContain('my-inline-animation-active');
- browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- $animate.triggerCallbackPromise();
-
- //leave
- $animate.leave(child);
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(child.attr('class')).toContain('ng-leave');
- expect(child.attr('class')).toContain('ng-leave-active');
- browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }));
-
-
- it("should trigger a cancellation when the return function is called upon any animation", function() {
- var captures = {};
-
- module(function($animateProvider) {
- $animateProvider.register('.track-me', function() {
- return {
- enter: track('enter'),
- leave: track('leave'),
- move: track('move'),
- addClass: track('addClass'),
- removeClass: track('removeClass'),
- setClass: track('setClass')
- };
-
- function track(type) {
- return function(element, add, remove, done) {
- done = done || remove || add;
- return function(cancelled) {
- captures[type]=cancelled;
- };
- };
- }
- });
- });
- inject(function($animate, $sniffer, $rootScope) {
-
- var promise;
- $animate.enabled(true);
- $rootScope.$digest();
- element[0].removeChild(child[0]);
- child.addClass('track-me');
-
- //enter
- promise = $animate.enter(child, element);
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect(captures.enter).toBeUndefined();
- $animate.cancel(promise);
- expect(captures.enter).toBeTruthy();
- $animate.triggerCallbackPromise();
-
- //move
- element.append(after);
- promise = $animate.move(child, element, after);
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect(captures.move).toBeUndefined();
- $animate.cancel(promise);
- expect(captures.move).toBeTruthy();
- $animate.triggerCallbackPromise();
-
- //addClass
- promise = $animate.addClass(child, 'ng-hide');
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect(captures.addClass).toBeUndefined();
- $animate.cancel(promise);
- expect(captures.addClass).toBeTruthy();
- $animate.triggerCallbackPromise();
-
- //removeClass
- promise = $animate.removeClass(child, 'ng-hide');
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect(captures.removeClass).toBeUndefined();
- $animate.cancel(promise);
- expect(captures.removeClass).toBeTruthy();
- $animate.triggerCallbackPromise();
-
- //setClass
- child.addClass('red');
- promise = $animate.setClass(child, 'blue', 'red');
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect(captures.setClass).toBeUndefined();
- $animate.cancel(promise);
- expect(captures.setClass).toBeTruthy();
- $animate.triggerCallbackPromise();
-
- //leave
- promise = $animate.leave(child);
- $rootScope.$digest();
-
- expect(captures.leave).toBeUndefined();
- $animate.cancel(promise);
- expect(captures.leave).toBeTruthy();
- $animate.triggerCallbackPromise();
- });
- });
-
-
- it("should not run if animations are disabled",
- inject(function($animate, $rootScope, $timeout, $sniffer) {
-
- $animate.enabled(false);
-
- $rootScope.$digest();
-
- element.addClass('setup-memo');
-
- element.text('123');
- expect(element.text()).toBe('123');
- $animate.removeClass(element, 'ng-hide');
- $rootScope.$digest();
- expect(element.text()).toBe('123');
-
- $animate.enabled(true);
-
- element.addClass('ng-hide');
- $animate.removeClass(element, 'ng-hide');
- $rootScope.$digest();
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- }
- expect(element.text()).toBe('memento');
- }));
-
-
- it("should only call done() once and right away if another animation takes place in between",
- inject(function($animate, $rootScope, $sniffer, $timeout) {
-
- element.append(child);
- child.addClass('custom-delay');
-
- expect(element).toBeShown();
- $animate.addClass(child, 'ng-hide');
- $rootScope.$digest();
- if ($sniffer.transitions) {
- expect(child).toBeShown();
- }
-
- $animate.leave(child);
- $rootScope.$digest();
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- }
- expect(child).toBeHidden(); //hides instantly
-
- //lets change this to prove that done doesn't fire anymore for the previous hide() operation
- child.css('display','block');
- child.removeClass('ng-hide');
-
- if ($sniffer.transitions) {
- expect(element.children().length).toBe(1); //still animating
- browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
- $timeout.flush(2000);
- $timeout.flush(2000);
- expect(child).toBeShown();
-
- expect(element.children().length).toBe(0);
- }));
-
-
- it("should retain existing styles of the animated element",
- inject(function($animate, $rootScope, $sniffer) {
-
- element.append(child);
- child.attr('style', 'width: 20px');
-
- $animate.addClass(child, 'ng-hide');
- $rootScope.$digest();
-
- $animate.leave(child);
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- $animate.triggerReflow();
-
- //this is to verify that the existing style is appended with a semicolon automatically
- expect(child.attr('style')).toMatch(/width: 20px;.*?/i);
- browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
-
- expect(child.attr('style')).toMatch(/width: 20px/i);
- }));
-
-
- it("should call the cancel callback when another animation is called on the same element",
- inject(function($animate, $rootScope, $sniffer, $timeout) {
-
- element.append(child);
-
- child.addClass('custom-delay ng-hide');
- $animate.removeClass(child, 'ng-hide');
- $rootScope.$digest();
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
- $timeout.flush(2000);
-
- $animate.addClass(child, 'ng-hide');
-
- expect(child.hasClass('animation-cancelled')).toBe(true);
- }));
-
- it("should remove the .ng-animate class after the next animation is run which interrupted the last animation", function() {
- var addClassDone, removeClassDone,
- addClassDoneSpy = jasmine.createSpy(),
- removeClassDoneSpy = jasmine.createSpy();
-
- module(function($animateProvider) {
- $animateProvider.register('.hide', function() {
- return {
- addClass: function(element, className, done) {
- addClassDone = done;
- return addClassDoneSpy;
- },
- removeClass: function(element, className, done) {
- removeClassDone = done;
- return removeClassDoneSpy;
- }
- };
- });
- });
-
- inject(function($animate, $rootScope) {
- $animate.addClass(element, 'hide');
- $rootScope.$digest();
-
- expect(element).toHaveClass('ng-animate');
-
- $animate.triggerReflow();
-
- $animate.removeClass(element, 'hide');
- $rootScope.$digest();
- expect(addClassDoneSpy).toHaveBeenCalled();
-
- $animate.triggerReflow();
-
- expect(element).toHaveClass('ng-animate');
-
- removeClassDone();
- $animate.triggerCallbackPromise();
-
- expect(element).not.toHaveClass('ng-animate');
- });
- });
-
- it("should skip a class-based animation if the same element already has an ongoing structural animation",
- inject(function($animate, $rootScope, $sniffer) {
-
- var completed = false;
- $animate.enter(child, element, null).then(function() {
- completed = true;
- });
- $rootScope.$digest();
-
- expect(completed).toBe(false);
-
- $animate.addClass(child, 'green');
- $rootScope.$digest();
- expect(element.hasClass('green'));
-
- expect(completed).toBe(false);
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
- $animate.triggerCallbackPromise();
-
- expect(completed).toBe(true);
- }));
-
- it("should skip class-based animations if animations are directly disabled on the same element", function() {
- var capture;
- module(function($animateProvider) {
- $animateProvider.register('.capture', function() {
- return {
- addClass: function(element, className, done) {
- capture = true;
- done();
- }
- };
- });
- });
- inject(function($animate, $rootScope) {
- $animate.enabled(true);
- $animate.enabled(false, element);
-
- $animate.addClass(element, 'capture');
- $rootScope.$digest();
- expect(element.hasClass('capture')).toBe(true);
- expect(capture).not.toBe(true);
- });
- });
-
- it("should not apply a cancellation when addClass is done multiple times",
- inject(function($animate, $rootScope, $sniffer, $timeout) {
-
- element.append(child);
-
- $animate.addClass(child, 'custom-delay');
- $rootScope.$digest();
-
- $animate.addClass(child, 'custom-long-delay');
- $rootScope.$digest();
-
- $animate.triggerReflow();
-
- expect(child.hasClass('animation-cancelled')).toBe(false);
- expect(child.hasClass('animation-ended')).toBe(false);
-
- $timeout.flush();
- expect(child.hasClass('animation-ended')).toBe(true);
- }));
-
-
- it("should NOT clobber all data on an element when animation is finished",
- inject(function($animate, $rootScope) {
-
- child.css('display','none');
- element.data('foo', 'bar');
-
- $animate.removeClass(element, 'ng-hide');
- $rootScope.$digest();
-
- $animate.addClass(element, 'ng-hide');
- $rootScope.$digest();
-
- expect(element.data('foo')).toEqual('bar');
- }));
-
-
- it("should allow multiple JS animations which run in parallel",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
-
- $animate.addClass(element, 'custom-delay custom-long-delay');
- $rootScope.$digest();
- $animate.triggerReflow();
- $timeout.flush(2000);
- $timeout.flush(20000);
- expect(element.hasClass('custom-delay')).toBe(true);
- expect(element.hasClass('custom-long-delay')).toBe(true);
- })
- );
-
-
- it("should allow both multiple JS and CSS animations which run in parallel",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout, _$rootElement_) {
- $rootElement = _$rootElement_;
-
- ss.addRule('.ng-hide-add', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
- ss.addRule('.ng-hide-remove', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
-
- element = $compile(html('
1
'))($rootScope);
- element.addClass('custom-delay custom-long-delay');
- $rootScope.$digest();
-
- $animate.removeClass(element, 'ng-hide');
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
- $timeout.flush(2000);
- $timeout.flush(20000);
-
- expect(element.hasClass('custom-delay')).toBe(true);
- expect(element.hasClass('custom-delay-add')).toBe(false);
- expect(element.hasClass('custom-delay-add-active')).toBe(false);
-
- expect(element.hasClass('custom-long-delay')).toBe(true);
- expect(element.hasClass('custom-long-delay-add')).toBe(false);
- expect(element.hasClass('custom-long-delay-add-active')).toBe(false);
- }));
-
- it('should apply directive styles and provide the style collection to the animation function', function() {
- var animationDone;
- var animationStyles;
- var proxyAnimation = function() {
- var limit = arguments.length - 1;
- animationStyles = arguments[limit];
- animationDone = arguments[limit - 1];
- };
- module(function($animateProvider) {
- $animateProvider.register('.capture', function() {
- return {
- enter: proxyAnimation,
- leave: proxyAnimation,
- move: proxyAnimation,
- addClass: proxyAnimation,
- removeClass: proxyAnimation,
- setClass: proxyAnimation
- };
- });
- });
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout, _$rootElement_) {
- $rootElement = _$rootElement_;
-
- $animate.enabled(true);
-
- element = $compile(html('
'))($rootScope);
- var otherParent = $compile('
')($rootScope);
- var child = $compile('
')($rootScope);
-
- $rootElement.append(otherParent);
- $rootScope.$digest();
-
- var styles = {
- from: { backgroundColor: 'blue' },
- to: { backgroundColor: 'red' }
- };
-
- //enter
- $animate.enter(child, element, null, styles);
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(animationStyles).toEqual(styles);
- animationDone();
- animationDone = animationStyles = null;
- $animate.triggerCallbacks();
-
- //move
- $animate.move(child, null, otherParent, styles);
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(animationStyles).toEqual(styles);
- animationDone();
- animationDone = animationStyles = null;
- $animate.triggerCallbacks();
-
- //addClass
- $animate.addClass(child, 'on', styles);
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(animationStyles).toEqual(styles);
- animationDone();
- animationDone = animationStyles = null;
- $animate.triggerCallbacks();
-
- //setClass
- $animate.setClass(child, 'off', 'on', styles);
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(animationStyles).toEqual(styles);
- animationDone();
- animationDone = animationStyles = null;
- $animate.triggerCallbacks();
-
- //removeClass
- $animate.removeClass(child, 'off', styles);
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(animationStyles).toEqual(styles);
- animationDone();
- animationDone = animationStyles = null;
- $animate.triggerCallbacks();
-
- //leave
- $animate.leave(child, styles);
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(animationStyles).toEqual(styles);
- animationDone();
- animationDone = animationStyles = null;
- $animate.triggerCallbacks();
-
- dealoc(otherParent);
- });
- });
- });
-
- it("should apply animated styles even if there are no detected animations",
- inject(function($compile, $animate, $rootScope, $sniffer, $rootElement, $document) {
-
- $animate.enabled(true);
- jqLite($document[0].body).append($rootElement);
-
- element = $compile('
')($rootScope);
-
- $animate.enter(element, $rootElement, null, {
- to: {borderColor: 'red'}
- });
-
- $rootScope.$digest();
- expect(element).toHaveClass('ng-animate');
-
- $animate.triggerReflow();
- $animate.triggerCallbacks();
-
- expect(element).not.toHaveClass('ng-animate');
- expect(element.attr('style')).toMatch(/border-color: red/);
- }));
-
- describe("with CSS3", function() {
-
- beforeEach(function() {
- module(function() {
- return function(_$rootElement_) {
- $rootElement = _$rootElement_;
- };
- });
- });
-
- /* The CSS animation handler must always be rendered before the other JS animation
- handlers. This is important since the CSS animation handler may place temporary
- styling on the HTML element before the reflow commences which in turn may override
- other transition or keyframe styles that any former JS animations may have placed
- on the element: https://github.com/angular/angular.js/issues/6675 */
- it("should always perform the CSS animation before the JS animation", function() {
- var log = [];
- module(function($animateProvider) {
- //CSS animation handler
- $animateProvider.register('', function() {
- return {
- leave: function() { log.push('css'); }
- };
- });
- //custom JS animation handler
- $animateProvider.register('.js-animation', function() {
- return {
- leave: function() { log.push('js'); }
- };
- });
- });
- inject(function($animate, $rootScope, $compile, $sniffer) {
- if (!$sniffer.transitions) return;
-
- element = $compile(html('
'))($rootScope);
- $animate.leave(element);
- $rootScope.$digest();
- expect(log).toEqual(['css','js']);
- });
- });
-
-
- describe("Animations", function() {
-
- it("should properly detect and make use of CSS Animations",
- inject(function($animate, $rootScope, $compile, $sniffer) {
-
- ss.addRule('.ng-hide-add',
- '-webkit-animation: some_animation 4s linear 0s 1 alternate;' +
- 'animation: some_animation 4s linear 0s 1 alternate;');
- ss.addRule('.ng-hide-remove',
- '-webkit-animation: some_animation 4s linear 0s 1 alternate;' +
- 'animation: some_animation 4s linear 0s 1 alternate;');
-
- element = $compile(html('
1
'))($rootScope);
-
- element.addClass('ng-hide');
- expect(element).toBeHidden();
-
- $animate.removeClass(element, 'ng-hide');
- $rootScope.$digest();
- if ($sniffer.animations) {
- $animate.triggerReflow();
- browserTrigger(element,'animationend', { timeStamp: Date.now() + 4000, elapsedTime: 4 });
- }
- expect(element).toBeShown();
- }));
-
-
- it("should properly detect and make use of CSS Animations with multiple iterations",
- inject(function($animate, $rootScope, $compile, $sniffer) {
-
- var style = '-webkit-animation-duration: 2s;' +
- '-webkit-animation-iteration-count: 3;' +
- 'animation-duration: 2s;' +
- 'animation-iteration-count: 3;';
-
- ss.addRule('.ng-hide-add', style);
- ss.addRule('.ng-hide-remove', style);
-
- element = $compile(html('
1
'))($rootScope);
-
- element.addClass('ng-hide');
- expect(element).toBeHidden();
-
- $animate.removeClass(element, 'ng-hide');
- $rootScope.$digest();
- if ($sniffer.animations) {
- $animate.triggerReflow();
- browserTrigger(element,'animationend', { timeStamp: Date.now() + 6000, elapsedTime: 6 });
- }
- expect(element).toBeShown();
- }));
-
-
- it("should not consider the animation delay is provided",
- inject(function($animate, $rootScope, $compile, $sniffer) {
-
- var style = '-webkit-animation-duration: 2s;' +
- '-webkit-animation-delay: 10s;' +
- '-webkit-animation-iteration-count: 5;' +
- 'animation-duration: 2s;' +
- 'animation-delay: 10s;' +
- 'animation-iteration-count: 5;';
-
- ss.addRule('.ng-hide-add', style);
- ss.addRule('.ng-hide-remove', style);
-
- element = $compile(html('
1
'))($rootScope);
-
- element.addClass('ng-hide');
- expect(element).toBeHidden();
-
- $animate.removeClass(element, 'ng-hide');
- $rootScope.$digest();
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- browserTrigger(element,'animationend', { timeStamp: Date.now() + 20000, elapsedTime: 10 });
- }
- expect(element).toBeShown();
- }));
-
-
- it("should skip animations if disabled and run when enabled",
- inject(function($animate, $rootScope, $compile) {
- $animate.enabled(false);
- var style = '-webkit-animation: some_animation 2s linear 0s 1 alternate;' +
- 'animation: some_animation 2s linear 0s 1 alternate;';
-
- ss.addRule('.ng-hide-add', style);
- ss.addRule('.ng-hide-remove', style);
-
- element = $compile(html('
1
'))($rootScope);
- element.addClass('ng-hide');
- expect(element).toBeHidden();
- $animate.removeClass(element, 'ng-hide');
- $rootScope.$digest();
- expect(element).toBeShown();
- }));
-
-
- it("should finish the previous animation when a new animation is started",
- inject(function($animate, $rootScope, $compile, $sniffer) {
- var style = '-webkit-animation: some_animation 2s linear 0s 1 alternate;' +
- 'animation: some_animation 2s linear 0s 1 alternate;';
-
- ss.addRule('.ng-hide-add', style);
- ss.addRule('.ng-hide-remove', style);
-
- element = $compile(html('
1
'))($rootScope);
- element.addClass('custom');
-
- $animate.removeClass(element, 'ng-hide');
- $rootScope.$digest();
-
- if ($sniffer.animations) {
- $animate.triggerReflow();
- expect(element.hasClass('ng-hide-remove')).toBe(true);
- expect(element.hasClass('ng-hide-remove-active')).toBe(true);
- }
-
- element.removeClass('ng-hide');
- $animate.addClass(element, 'ng-hide');
- $rootScope.$digest();
-
- expect(element.hasClass('ng-hide-remove')).toBe(false); //added right away
-
- if ($sniffer.animations) { //cleanup some pending animations
- $animate.triggerReflow();
- expect(element.hasClass('ng-hide-add')).toBe(true);
- expect(element.hasClass('ng-hide-add-active')).toBe(true);
- browserTrigger(element,'animationend', { timeStamp: Date.now() + 2000, elapsedTime: 2 });
- }
-
- expect(element.hasClass('ng-hide-remove-active')).toBe(false);
- })
- );
-
- it("should piggy-back-transition the styles with the max keyframe duration if provided by the directive",
- inject(function($compile, $animate, $rootScope, $sniffer) {
-
- $animate.enabled(true);
- ss.addRule('.on', '-webkit-animation: 1s keyframeanimation; animation: 1s keyframeanimation;');
-
- element = $compile(html('
1
'))($rootScope);
-
- $animate.addClass(element, 'on', {
- to: {borderColor: 'blue'}
- });
-
- $rootScope.$digest();
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(element.attr('style')).toContain('border-color: blue');
- expect(element.attr('style')).toMatch(/transition:.*1s/);
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
-
- expect(element.attr('style')).toContain('border-color: blue');
- }));
-
- it("should not apply a piggy-back-transition if the styles object contains no styles",
- inject(function($compile, $animate, $rootScope, $sniffer) {
-
- if (!$sniffer.animations) return;
-
- $animate.enabled(true);
- ss.addRule('.on', '-webkit-animation: 1s super-animation; animation: 1s super-animation;');
-
- element = $compile(html('
1
'))($rootScope);
-
- $animate.addClass(element, 'on', {
- to: {}
- });
-
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(element.attr('style')).not.toMatch(/transition/);
- }));
-
- it("should pause the playstate when performing a stagger animation",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
-
- if (!$sniffer.animations) return;
-
- $animate.enabled(true);
-
- ss.addRule('.real-animation.ng-enter, .real-animation.ng-leave',
- '-webkit-animation:1s my_animation;' +
- 'animation:1s my_animation;');
-
- ss.addRule('.real-animation.ng-enter-stagger, .real-animation.ng-leave-stagger',
- '-webkit-animation-delay:0.1s;' +
- '-webkit-animation-duration:0s;' +
- 'animation-delay:0.1s;' +
- 'animation-duration:0s;');
-
- ss.addRule('.fake-animation.ng-enter-stagger, .fake-animation.ng-leave-stagger',
- '-webkit-animation-delay:0.1s;' +
- '-webkit-animation-duration:1s;' +
- 'animation-delay:0.1s;' +
- 'animation-duration:1s;');
-
- var container = $compile(html('
'))($rootScope);
-
- var newScope, element, elements = [];
- for (var i = 0; i < 5; i++) {
- newScope = $rootScope.$new();
- element = $compile('
')(newScope);
- $animate.enter(element, container);
- elements.push(element);
- }
-
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect(elements[0].attr('style')).toBeFalsy();
- for (i = 1; i < 5; i++) {
- expect(elements[i].attr('style')).toMatch(/animation-play-state:\s*paused/);
- }
-
- //final closing timeout
- $timeout.flush();
-
- for (i = 0; i < 5; i++) {
- dealoc(elements[i]);
- newScope = $rootScope.$new();
- element = $compile('
')(newScope);
- $animate.enter(element, container);
- elements[i] = element;
- }
-
- $rootScope.$digest();
-
- //this means no animations were triggered
- $timeout.verifyNoPendingTasks();
-
- expect(elements[0].attr('style')).toBeFalsy();
- for (i = 1; i < 5; i++) {
- expect(elements[i].attr('style')).not.toMatch(/animation-play-state:\s*paused/);
- }
- }));
-
-
- it("should block and unblock keyframe animations when a stagger animation kicks in while skipping the first element",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
-
- if (!$sniffer.animations) return;
-
- $animate.enabled(true);
-
- ss.addRule('.blocked-animation.ng-enter',
- '-webkit-animation:my_animation 1s;' +
- 'animation:my_animation 1s;');
-
- ss.addRule('.blocked-animation.ng-enter-stagger',
- '-webkit-animation-delay:0.2s;' +
- 'animation-delay:0.2s;');
-
- var container = $compile(html('
'))($rootScope);
-
- var elements = [];
- for (var i = 0; i < 4; i++) {
- var newScope = $rootScope.$new();
- var element = $compile('
')(newScope);
- $animate.enter(element, container);
- elements.push(element);
- }
-
- $rootScope.$digest();
-
- expect(elements[0].attr('style')).toBeUndefined();
- for (i = 1; i < 4; i++) {
- expect(elements[i].attr('style')).toMatch(/animation-play-state:\s*paused/);
- }
-
- $animate.triggerReflow();
-
- expect(elements[0].attr('style')).toBeUndefined();
- for (i = 1; i < 4; i++) {
- expect(elements[i].attr('style')).toMatch(/animation-play-state:\s*paused/);
- }
-
- $timeout.flush(800);
-
- for (i = 1; i < 4; i++) {
- expect(elements[i].attr('style')).not.toMatch(/animation-play-state/);
- }
- }));
-
- it("should stagger items when multiple animation durations/delays are defined",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $window) {
-
- if (!$sniffer.transitions) return;
-
- $animate.enabled(true);
-
- ss.addRule('.stagger-animation.ng-enter, .stagger-animation.ng-leave',
- '-webkit-animation:my_animation 1s 1s, your_animation 1s 2s;' +
- 'animation:my_animation 1s 1s, your_animation 1s 2s;');
-
- ss.addRule('.stagger-animation.ng-enter-stagger, .stagger-animation.ng-leave-stagger',
- '-webkit-animation-delay:0.1s;' +
- 'animation-delay:0.1s;');
-
- var container = $compile(html('
'))($rootScope);
-
- var elements = [];
- for (var i = 0; i < 4; i++) {
- var newScope = $rootScope.$new();
- var element = $compile('
')(newScope);
- $animate.enter(element, container);
- elements.push(element);
- }
-
- $rootScope.$digest();
- $animate.triggerReflow();
-
- for (i = 1; i < 4; i++) {
- expect(elements[i]).not.toHaveClass('ng-enter-active');
- expect(elements[i]).toHaveClass('ng-enter-pending');
- expect(getMaxValue('animationDelay', elements[i], $window)).toBe(2);
- }
-
- $timeout.flush(300);
-
- for (i = 1; i < 4; i++) {
- expect(elements[i]).toHaveClass('ng-enter-active');
- expect(elements[i]).not.toHaveClass('ng-enter-pending');
- expect(getMaxValue('animationDelay', elements[i], $window)).toBe(2);
- }
- }));
-
- it("should stagger items and apply the transition + directive styles the right time when piggy-back styles are used",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $window) {
-
- if (!$sniffer.transitions) return;
-
- $animate.enabled(true);
-
- ss.addRule('.stagger-animation.ng-enter, .stagger-animation.ng-leave',
- '-webkit-animation:my_animation 1s 1s, your_animation 1s 2s;' +
- 'animation:my_animation 1s 1s, your_animation 1s 2s;');
-
- ss.addRule('.stagger-animation.ng-enter-stagger, .stagger-animation.ng-leave-stagger',
- '-webkit-animation-delay:0.1s;' +
- 'animation-delay:0.1s;');
-
- var styles = {
- from: { left: '50px' },
- to: { left: '100px' }
- };
- var container = $compile(html('
'))($rootScope);
-
- var elements = [];
- for (var i = 0; i < 4; i++) {
- var newScope = $rootScope.$new();
- var element = $compile('
')(newScope);
- $animate.enter(element, container, null, styles);
- elements.push(element);
- }
-
- $rootScope.$digest();
-
- for (i = 0; i < 4; i++) {
- expect(elements[i]).toHaveClass('ng-enter');
- assertTransitionDuration(elements[i], '2', true);
- assertLeftStyle(elements[i], '50');
- }
-
- $animate.triggerReflow();
-
- expect(elements[0]).toHaveClass('ng-enter-active');
- assertLeftStyle(elements[0], '100');
- assertTransitionDuration(elements[0], '1');
-
- for (i = 1; i < 4; i++) {
- expect(elements[i]).not.toHaveClass('ng-enter-active');
- assertTransitionDuration(elements[i], '1', true);
- assertLeftStyle(elements[i], '100', true);
- }
-
- $timeout.flush(300);
-
- for (i = 1; i < 4; i++) {
- expect(elements[i]).toHaveClass('ng-enter-active');
- assertTransitionDuration(elements[i], '1');
- assertLeftStyle(elements[i], '100');
- }
-
- $timeout.flush();
-
- for (i = 0; i < 4; i++) {
- expect(elements[i]).not.toHaveClass('ng-enter');
- expect(elements[i]).not.toHaveClass('ng-enter-active');
- assertTransitionDuration(elements[i], '1', true);
- assertLeftStyle(elements[i], '100');
- }
-
- function assertLeftStyle(element, val, not) {
- var regex = new RegExp('left: ' + val + 'px');
- var style = element.attr('style');
- not ? expect(style).not.toMatch(regex)
- : expect(style).toMatch(regex);
- }
-
- function assertTransitionDuration(element, val, not) {
- var regex = new RegExp('transition:.*' + val + 's');
- var style = element.attr('style');
- not ? expect(style).not.toMatch(regex)
- : expect(style).toMatch(regex);
- }
- }));
- });
-
-
- describe("Transitions", function() {
-
- it("should skip transitions if disabled and run when enabled",
- inject(function($animate, $rootScope, $compile, $sniffer) {
-
- var style = '-webkit-transition: 1s linear all;' +
- 'transition: 1s linear all;';
-
- ss.addRule('.ng-hide-add', style);
- ss.addRule('.ng-hide-remove', style);
-
- $animate.enabled(false);
- element = $compile(html('
1
'))($rootScope);
-
- element.addClass('ng-hide');
- expect(element).toBeHidden();
- $animate.removeClass(element, 'ng-hide');
- $rootScope.$digest();
- expect(element).toBeShown();
-
- $animate.enabled(true);
-
- element.addClass('ng-hide');
- expect(element).toBeHidden();
-
- $animate.removeClass(element, 'ng-hide');
- $rootScope.$digest();
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
- expect(element).toBeShown();
- }));
-
-
- it("should skip animations if disabled and run when enabled picking the longest specified duration",
- inject(function($animate, $rootScope, $compile, $sniffer) {
-
- var style = '-webkit-transition-duration: 1s, 2000ms, 1s;' +
- '-webkit-transition-property: height, left, opacity;' +
- 'transition-duration: 1s, 2000ms, 1s;' +
- 'transition-property: height, left, opacity;';
-
- ss.addRule('.ng-hide-add', style);
- ss.addRule('.ng-hide-remove', style);
-
- element = $compile(html('
foo
'))($rootScope);
- element.addClass('ng-hide');
-
- $animate.removeClass(element, 'ng-hide');
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- var now = Date.now();
- browserTrigger(element,'transitionend', { timeStamp: now + 1000, elapsedTime: 1 });
- browserTrigger(element,'transitionend', { timeStamp: now + 1000, elapsedTime: 1 });
- browserTrigger(element,'transitionend', { timeStamp: now + 2000, elapsedTime: 2 });
- expect(element.hasClass('ng-animate')).toBe(false);
- }
- expect(element).toBeShown();
- }));
-
-
- it("should skip animations if disabled and run when enabled picking the longest specified duration/delay combination",
- inject(function($animate, $rootScope, $compile, $sniffer) {
- $animate.enabled(false);
- var style = '-webkit-transition-duration: 1s, 0s, 1s; ' +
- '-webkit-transition-delay: 2s, 1000ms, 2s; ' +
- '-webkit-transition-property: height, left, opacity;' +
- 'transition-duration: 1s, 0s, 1s; ' +
- 'transition-delay: 2s, 1000ms, 2s; ' +
- 'transition-property: height, left, opacity;';
-
- ss.addRule('.ng-hide-add', style);
- ss.addRule('.ng-hide-remove', style);
-
- element = $compile(html('
foo
'))($rootScope);
-
- element.addClass('ng-hide');
- $animate.removeClass(element, 'ng-hide');
- $rootScope.$digest();
-
- expect(element).toBeShown();
- $animate.enabled(true);
-
- element.addClass('ng-hide');
- expect(element).toBeHidden();
-
- $animate.removeClass(element, 'ng-hide');
- $rootScope.$digest();
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- var now = Date.now();
- browserTrigger(element,'transitionend', { timeStamp: now + 1000, elapsedTime: 1 });
- browserTrigger(element,'transitionend', { timeStamp: now + 3000, elapsedTime: 3 });
- browserTrigger(element,'transitionend', { timeStamp: now + 3000, elapsedTime: 3 });
- }
- expect(element).toBeShown();
- })
- );
-
-
- it("should NOT overwrite styles with outdated values when animation completes",
- inject(function($animate, $rootScope, $compile, $sniffer) {
-
- if (!$sniffer.transitions) return;
-
- var style = '-webkit-transition-duration: 1s, 2000ms, 1s;' +
- '-webkit-transition-property: height, left, opacity;' +
- 'transition-duration: 1s, 2000ms, 1s;' +
- 'transition-property: height, left, opacity;';
-
- ss.addRule('.ng-hide-add', style);
- ss.addRule('.ng-hide-remove', style);
-
- element = $compile(html('
foo
'))($rootScope);
- element.addClass('ng-hide');
-
- $animate.removeClass(element, 'ng-hide');
- $rootScope.$digest();
-
- $animate.triggerReflow();
-
- var now = Date.now();
- browserTrigger(element,'transitionend', { timeStamp: now + 1000, elapsedTime: 1 });
- browserTrigger(element,'transitionend', { timeStamp: now + 1000, elapsedTime: 1 });
-
- element.css('width', '200px');
- browserTrigger(element,'transitionend', { timeStamp: now + 2000, elapsedTime: 2 });
- expect(element.css('width')).toBe("200px");
- }));
-
- it("should NOT overwrite styles when a transition with a specific property is used",
- inject(function($animate, $rootScope, $compile, $sniffer) {
-
- if (!$sniffer.transitions) return;
-
- var style = '-webkit-transition: border linear .2s;' +
- 'transition: border linear .2s;';
-
- ss.addRule('.on', style);
- element = $compile(html('
'))($rootScope);
- $animate.addClass(element, 'on');
- $rootScope.$digest();
-
- $animate.triggerReflow();
-
- var now = Date.now();
- browserTrigger(element,'transitionend', { timeStamp: now + 200, elapsedTime: 0.2 });
- expect(element.css('height')).toBe("200px");
- }));
-
-
- it("should animate for the highest duration",
- inject(function($animate, $rootScope, $compile, $sniffer) {
- var style = '-webkit-transition:1s linear all 2s;' +
- 'transition:1s linear all 2s;' +
- '-webkit-animation:my_ani 10s 1s;' +
- 'animation:my_ani 10s 1s;';
-
- ss.addRule('.ng-hide-add', style);
- ss.addRule('.ng-hide-remove', style);
-
- element = $compile(html('
foo
'))($rootScope);
-
- element.addClass('ng-hide');
- expect(element).toBeHidden();
-
- $animate.removeClass(element, 'ng-hide');
- $rootScope.$digest();
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- }
- expect(element).toBeShown();
- if ($sniffer.transitions) {
- expect(element.hasClass('ng-hide-remove-active')).toBe(true);
- browserTrigger(element,'animationend', { timeStamp: Date.now() + 11000, elapsedTime: 11 });
- expect(element.hasClass('ng-hide-remove-active')).toBe(false);
- }
- })
- );
-
-
- it("should finish the previous transition when a new animation is started",
- inject(function($animate, $rootScope, $compile, $sniffer) {
- var style = '-webkit-transition: 1s linear all;' +
- 'transition: 1s linear all;';
-
- ss.addRule('.ng-hide-add', style);
- ss.addRule('.ng-hide-remove', style);
-
- element = $compile(html('
1
'))($rootScope);
-
- element.addClass('ng-hide');
- $animate.removeClass(element, 'ng-hide');
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(element.hasClass('ng-hide-remove')).toBe(true);
- expect(element.hasClass('ng-hide-remove-active')).toBe(true);
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
- expect(element.hasClass('ng-hide-remove')).toBe(false);
- expect(element.hasClass('ng-hide-remove-active')).toBe(false);
- expect(element).toBeShown();
-
- $animate.addClass(element, 'ng-hide');
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(element.hasClass('ng-hide-add')).toBe(true);
- expect(element.hasClass('ng-hide-add-active')).toBe(true);
- }
- })
- );
-
- it("should place a hard block when a structural CSS transition is run",
- inject(function($animate, $rootScope, $compile, $sniffer) {
-
- if (!$sniffer.transitions) return;
-
- ss.addRule('.leave-animation.ng-leave',
- '-webkit-transition:5s linear all;' +
- 'transition:5s linear all;' +
- 'opacity:1;');
-
- ss.addRule('.leave-animation.ng-leave.ng-leave-active', 'opacity:1');
-
- element = $compile(html('
1
'))($rootScope);
-
- $animate.leave(element);
- $rootScope.$digest();
-
- expect(element.attr('style')).toMatch(/transition.*?:\s*none/);
-
- $animate.triggerReflow();
-
- expect(element.attr('style')).not.toMatch(/transition.*?:\s*none/);
- }));
-
- it("should not place a hard block when a class-based CSS transition is run",
- inject(function($animate, $rootScope, $compile, $sniffer) {
-
- if (!$sniffer.transitions) return;
-
- ss.addRule('.my-class', '-webkit-transition:5s linear all;' +
- 'transition:5s linear all;');
-
- element = $compile(html('
1
'))($rootScope);
-
- $animate.addClass(element, 'my-class');
- $rootScope.$digest();
-
- expect(element.attr('style')).not.toMatch(/transition.*?:\s*none/);
- expect(element.hasClass('my-class')).toBe(false);
- expect(element.hasClass('my-class-add')).toBe(true);
-
- $animate.triggerReflow();
- $rootScope.$digest();
-
- expect(element.attr('style')).not.toMatch(/transition.*?:\s*none/);
- expect(element.hasClass('my-class')).toBe(true);
- expect(element.hasClass('my-class-add')).toBe(true);
- expect(element.hasClass('my-class-add-active')).toBe(true);
- }));
-
- it("should stagger the items when the correct CSS class is provided",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $browser) {
-
- if (!$sniffer.transitions) return;
-
- $animate.enabled(true);
-
- ss.addRule('.real-animation.ng-enter, .real-animation.ng-leave, .real-animation-fake.ng-enter, .real-animation-fake.ng-leave',
- '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
-
- ss.addRule('.real-animation.ng-enter-stagger, .real-animation.ng-leave-stagger',
- '-webkit-transition-delay:0.1s;' +
- '-webkit-transition-duration:0s;' +
- 'transition-delay:0.1s;' +
- 'transition-duration:0s;');
-
- ss.addRule('.fake-animation.ng-enter-stagger, .fake-animation.ng-leave-stagger',
- '-webkit-transition-delay:0.1s;' +
- '-webkit-transition-duration:1s;' +
- 'transition-delay:0.1s;' +
- 'transition-duration:1s;');
-
- var container = $compile(html('
'))($rootScope);
-
- var newScope, element, elements = [];
- for (var i = 0; i < 5; i++) {
- newScope = $rootScope.$new();
- element = $compile('
')(newScope);
- $animate.enter(element, container);
- elements.push(element);
- }
-
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect($browser.deferredFns.length).toEqual(5); //4 staggers + 1 combined timeout
- $timeout.flush();
-
- for (i = 0; i < 5; i++) {
- dealoc(elements[i]);
- newScope = $rootScope.$new();
- element = $compile('
')(newScope);
- $animate.enter(element, container);
- elements[i] = element;
- }
-
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect($browser.deferredFns.length).toEqual(0); //no animation was triggered
- }));
-
-
- it("should stagger items when multiple transition durations/delays are defined",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $window) {
-
- if (!$sniffer.transitions) return;
-
- $animate.enabled(true);
-
- ss.addRule('.stagger-animation.ng-enter, .ani.ng-leave',
- '-webkit-transition:1s linear color 2s, 3s linear font-size 4s;' +
- 'transition:1s linear color 2s, 3s linear font-size 4s;');
-
- ss.addRule('.stagger-animation.ng-enter-stagger, .ani.ng-leave-stagger',
- '-webkit-transition-delay:0.1s;' +
- 'transition-delay:0.1s;');
-
- var container = $compile(html('
'))($rootScope);
-
- var elements = [];
- for (var i = 0; i < 4; i++) {
- var newScope = $rootScope.$new();
- var element = $compile('
')(newScope);
- $animate.enter(element, container);
- elements.push(element);
- }
-
- $rootScope.$digest();
- $animate.triggerReflow();
-
- for (i = 1; i < 4; i++) {
- expect(elements[i]).not.toHaveClass('ng-enter-active');
- expect(elements[i]).toHaveClass('ng-enter-pending');
- expect(getMaxValue('transitionDelay', elements[i], $window)).toBe(4);
- }
-
- $timeout.flush(300);
-
- for (i = 1; i < 4; i++) {
- expect(elements[i]).toHaveClass('ng-enter-active');
- expect(elements[i]).not.toHaveClass('ng-enter-pending');
- expect(getMaxValue('transitionDelay', elements[i], $window)).toBe(4);
- }
- }));
-
- it("should stagger items, apply directive styles but not apply a transition style when the stagger step kicks in",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $window) {
-
- if (!$sniffer.transitions) return;
-
- $animate.enabled(true);
-
- ss.addRule('.stagger-animation.ng-enter, .ani.ng-leave',
- '-webkit-transition:1s linear color 2s, 3s linear font-size 4s;' +
- 'transition:1s linear color 2s, 3s linear font-size 4s;');
-
- ss.addRule('.stagger-animation.ng-enter-stagger, .ani.ng-leave-stagger',
- '-webkit-transition-delay:0.1s;' +
- 'transition-delay:0.1s;');
-
- var styles = {
- from: { left: '155px' },
- to: { left: '255px' }
- };
- var container = $compile(html('
'))($rootScope);
-
- var elements = [];
- for (var i = 0; i < 4; i++) {
- var newScope = $rootScope.$new();
- var element = $compile('
')(newScope);
- $animate.enter(element, container, null, styles);
- elements.push(element);
- }
-
- $rootScope.$digest();
-
- for (i = 0; i < 4; i++) {
- expect(elements[i]).toHaveClass('ng-enter');
- assertLeftStyle(elements[i], '155');
- }
-
- $animate.triggerReflow();
-
- expect(elements[0]).toHaveClass('ng-enter-active');
- assertLeftStyle(elements[0], '255');
- assertNoTransitionDuration(elements[0]);
-
- for (i = 1; i < 4; i++) {
- expect(elements[i]).not.toHaveClass('ng-enter-active');
- assertLeftStyle(elements[i], '255', true);
- }
-
- $timeout.flush(300);
-
- for (i = 1; i < 4; i++) {
- expect(elements[i]).toHaveClass('ng-enter-active');
- assertNoTransitionDuration(elements[i]);
- assertLeftStyle(elements[i], '255');
- }
-
- $timeout.flush();
-
- for (i = 0; i < 4; i++) {
- expect(elements[i]).not.toHaveClass('ng-enter');
- expect(elements[i]).not.toHaveClass('ng-enter-active');
- assertNoTransitionDuration(elements[i]);
- assertLeftStyle(elements[i], '255');
- }
-
- function assertLeftStyle(element, val, not) {
- var regex = new RegExp('left: ' + val + 'px');
- var style = element.attr('style');
- not ? expect(style).not.toMatch(regex)
- : expect(style).toMatch(regex);
- }
-
- function assertNoTransitionDuration(element) {
- var style = element.attr('style');
- expect(style).not.toMatch(/transition/);
- }
- }));
-
- it("should apply a closing timeout to close all pending transitions",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
-
- if (!$sniffer.transitions) return;
-
- ss.addRule('.animated-element', '-webkit-transition:5s linear all;' +
- 'transition:5s linear all;');
-
- element = $compile(html('
foo
'))($rootScope);
-
- $animate.addClass(element, 'some-class');
- $rootScope.$digest();
-
- $animate.triggerReflow(); //reflow
- expect(element.hasClass('some-class-add-active')).toBe(true);
-
- $timeout.flush(7500); //closing timeout
- expect(element.hasClass('some-class-add-active')).toBe(false);
- }));
-
- it("should intelligently cancel former timeouts and close off a series of elements a final timeout", function() {
- var currentTimestamp, cancellations = 0;
- module(function($provide) {
- $provide.decorator('$timeout', function($delegate) {
- var _cancel = $delegate.cancel;
- $delegate.cancel = function(timer) {
- if (timer) {
- cancellations++;
- return _cancel.apply($delegate, arguments);
- }
- };
- return $delegate;
- });
-
- return function($sniffer) {
- if ($sniffer.transitions) {
- currentTimestamp = Date.now();
- spyOn(Date,'now').andCallFake(function() {
- return currentTimestamp;
- });
- }
- };
- });
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
- if (!$sniffer.transitions) return;
-
- ss.addRule('.animate-me div', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
-
- ss.addRule('.animate-me-longer div', '-webkit-transition:1.5s linear all;' +
- 'transition:1.5s linear all;');
-
- element = $compile(html('
'))($rootScope);
- $rootScope.items = [0];
- $rootScope.$digest();
- $animate.triggerReflow();
-
- currentTimestamp += 2250; //1.5 * 1500 = 2250
-
- element[0].className = 'animate-me';
-
- $rootScope.items = [1,2,3,4,5,6,7,8,9,10];
-
- $rootScope.$digest();
-
- $rootScope.items = [0];
- $animate.triggerReflow();
-
- currentTimestamp += 1500; //1.5 * 1000 = 1500
- $timeout.flush(1500);
-
- expect(cancellations).toBe(1);
- expect(element.children().length).toBe(10);
- cancellations = 0;
-
- $rootScope.items = [1];
- $rootScope.$digest();
-
- $animate.triggerReflow();
- $timeout.flush(1500);
- expect(element.children().length).toBe(1);
- expect(cancellations).toBe(1);
- });
- });
-
- it('should apply a closing timeout to close all parallel class-based animations on the same element',
- inject(function($sniffer, $compile, $rootScope, $rootElement, $animate, $timeout) {
-
- if (!$sniffer.transitions) return;
-
- ss.addRule('.base-class', '-webkit-transition:2s linear all;' +
- 'transition:2s linear all;');
-
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
-
- $animate.addClass(element, 'one');
- $animate.addClass(element, 'two');
-
- $animate.triggerReflow();
-
- $timeout.flush(3000); //2s * 1.5
-
- expect(element.hasClass('one-add')).toBeFalsy();
- expect(element.hasClass('one-add-active')).toBeFalsy();
- expect(element.hasClass('two-add')).toBeFalsy();
- expect(element.hasClass('two-add-active')).toBeFalsy();
- expect(element.hasClass('ng-animate')).toBeFalsy();
- }));
-
- it("apply a closing timeout with respect to a staggering animation",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
-
- if (!$sniffer.transitions) return;
-
- ss.addRule('.entering-element.ng-enter',
- '-webkit-transition:5s linear all;' +
- 'transition:5s linear all;');
-
- ss.addRule('.entering-element.ng-enter-stagger',
- '-webkit-transition-delay:0.5s;' +
- 'transition-delay:0.5s;');
-
- element = $compile(html('
'))($rootScope);
- var kids = [];
- for (var i = 0; i < 5; i++) {
- kids.push(angular.element('
'));
- $animate.enter(kids[i], element);
- }
- $rootScope.$digest();
-
- $animate.triggerReflow(); //reflow
- expect(element.children().length).toBe(5);
-
- for (i = 1; i < 5; i++) {
- expect(kids[i]).not.toHaveClass('ng-enter-active');
- expect(kids[i]).toHaveClass('ng-enter-pending');
- }
-
- $timeout.flush(2000);
-
- for (i = 1; i < 5; i++) {
- expect(kids[i]).toHaveClass('ng-enter-active');
- expect(kids[i]).not.toHaveClass('ng-enter-pending');
- }
-
- //(stagger * index) + (duration + delay) * 150%
- //0.5 * 4 + 5 * 1.5 = 9500;
- //9500 - 2000 - 7499 = 1
- $timeout.flush(7499);
-
- for (i = 0; i < 5; i++) {
- expect(kids[i].hasClass('ng-enter-active')).toBe(true);
- }
-
- $timeout.flush(1); //up to 2000ms
-
- for (i = 0; i < 5; i++) {
- expect(kids[i].hasClass('ng-enter-active')).toBe(false);
- }
- }));
-
- it("should cancel all the existing stagger timers when the animation is cancelled",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $browser) {
-
- if (!$sniffer.transitions) return;
-
- ss.addRule('.entering-element.ng-enter',
- '-webkit-transition:5s linear all;' +
- 'transition:5s linear all;');
-
- ss.addRule('.entering-element.ng-enter-stagger',
- '-webkit-transition-delay:1s;' +
- 'transition-delay:1s;');
-
- var cancellations = [];
- element = $compile(html('
'))($rootScope);
- var kids = [];
- for (var i = 0; i < 5; i++) {
- kids.push(angular.element('
'));
- cancellations.push($animate.enter(kids[i], element));
- }
- $rootScope.$digest();
-
- $animate.triggerReflow(); //reflow
- expect(element.children().length).toBe(5);
-
- for (i = 1; i < 5; i++) {
- expect(kids[i]).not.toHaveClass('ng-enter-active');
- expect(kids[i]).toHaveClass('ng-enter-pending');
- }
-
- expect($browser.deferredFns.length).toEqual(5); //4 staggers + 1 combined timeout
-
- forEach(cancellations, function(promise) {
- $animate.cancel(promise);
- });
-
- for (i = 1; i < 5; i++) {
- expect(kids[i]).not.toHaveClass('ng-enter');
- expect(kids[i]).not.toHaveClass('ng-enter-active');
- expect(kids[i]).not.toHaveClass('ng-enter-pending');
- }
-
- //the staggers are gone, but the global timeout remains
- expect($browser.deferredFns.length).toEqual(1);
- }));
-
-
- it("should not allow the closing animation to close off a successive animation midway",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout) {
-
- if (!$sniffer.transitions) return;
-
- ss.addRule('.some-class-add', '-webkit-transition:5s linear all;' +
- 'transition:5s linear all;');
- ss.addRule('.some-class-remove', '-webkit-transition:10s linear all;' +
- 'transition:10s linear all;');
-
- element = $compile(html('
foo
'))($rootScope);
-
- $animate.addClass(element, 'some-class');
- $rootScope.$digest();
-
- $animate.triggerReflow(); //reflow
- expect(element.hasClass('some-class-add-active')).toBe(true);
-
- $animate.removeClass(element, 'some-class');
- $rootScope.$digest();
-
- $animate.triggerReflow(); //second reflow
-
- $timeout.flush(7500); //closing timeout for the first animation
- expect(element.hasClass('some-class-remove-active')).toBe(true);
-
- $timeout.flush(15000); //closing timeout for the second animation
- expect(element.hasClass('some-class-remove-active')).toBe(false);
-
- $timeout.verifyNoPendingTasks();
- }));
- });
-
-
- it("should apply staggering to both transitions and keyframe animations when used within the same animation",
- inject(function($animate, $rootScope, $compile, $sniffer, $timeout, $document, $rootElement, $browser) {
-
- if (!$sniffer.transitions) return;
-
- $animate.enabled(true);
-
- ss.addRule('.stagger-animation.ng-enter, .stagger-animation.ng-leave',
- '-webkit-animation:my_animation 1s 1s, your_animation 1s 2s;' +
- 'animation:my_animation 1s 1s, your_animation 1s 2s;' +
- '-webkit-transition:1s linear all 1s;' +
- 'transition:1s linear all 1s;');
-
- ss.addRule('.stagger-animation.ng-enter-stagger, .stagger-animation.ng-leave-stagger',
- '-webkit-transition-delay:0.1s;' +
- 'transition-delay:0.1s;' +
- '-webkit-animation-delay:0.2s;' +
- 'animation-delay:0.2s;');
-
- var container = $compile(html('
'))($rootScope);
-
- var elements = [];
- for (var i = 0; i < 3; i++) {
- var newScope = $rootScope.$new();
- var element = $compile('
')(newScope);
- $animate.enter(element, container);
- elements.push(element);
- }
-
- $rootScope.$digest();
- $animate.triggerReflow();
- expect($browser.deferredFns.length).toEqual(3); //2 staggers + 1 combined timeout
-
- expect(elements[0].attr('style')).toBeFalsy();
- expect(elements[1].attr('style')).toMatch(/animation-play-state:\s*paused/);
- expect(elements[2].attr('style')).toMatch(/animation-play-state:\s*paused/);
-
- for (i = 1; i < 3; i++) {
- expect(elements[i]).not.toHaveClass('ng-enter-active');
- expect(elements[i]).toHaveClass('ng-enter-pending');
- }
-
- $timeout.flush(0.4 * 1000);
-
- for (i = 1; i < 3; i++) {
- expect(elements[i]).toHaveClass('ng-enter-active');
- expect(elements[i]).not.toHaveClass('ng-enter-pending');
- }
-
- for (i = 0; i < 3; i++) {
- browserTrigger(elements[i],'transitionend', { timeStamp: Date.now() + 22000, elapsedTime: 22000 });
- expect(elements[i].attr('style')).toBeFalsy();
- }
- }));
-
- it("should create a piggy-back-transition which has a duration the same as the max keyframe duration if any directive styles are provided",
- inject(function($compile, $animate, $rootScope, $sniffer) {
-
- $animate.enabled(true);
- ss.addRule('.on', '-webkit-transition: 1s linear all; transition: 1s linear all;');
-
- element = $compile(html('
1
'))($rootScope);
-
- $animate.addClass(element, 'on', {
- to: {color: 'red'}
- });
-
- $rootScope.$digest();
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(element.attr('style')).toContain('color: red');
- expect(element.attr('style')).not.toContain('transition');
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
-
- expect(element.attr('style')).toContain('color: red');
- }));
- });
-
-
- describe('animation evaluation', function() {
-
- it('should re-evaluate the CSS classes for an animation each time',
- inject(function($animate, $rootScope, $sniffer, $rootElement, $timeout, $compile) {
-
- ss.addRule('.abc.ng-enter', '-webkit-transition:22s linear all;' +
- 'transition:22s linear all;');
- ss.addRule('.xyz.ng-enter', '-webkit-transition:11s linear all;' +
- 'transition:11s linear all;');
-
- var parent = $compile('
')($rootScope);
- var element = parent.find('span');
- $rootElement.append(parent);
- angular.element(document.body).append($rootElement);
-
- $rootScope.klass = 'abc';
- $animate.enter(element, parent);
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(element.hasClass('abc')).toBe(true);
- expect(element.hasClass('ng-enter')).toBe(true);
- expect(element.hasClass('ng-enter-active')).toBe(true);
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 22000, elapsedTime: 22 });
- $animate.triggerCallbackPromise();
- }
- expect(element.hasClass('abc')).toBe(true);
-
- $rootScope.klass = 'xyz';
- $animate.enter(element, parent);
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(element.hasClass('xyz')).toBe(true);
- expect(element.hasClass('ng-enter')).toBe(true);
- expect(element.hasClass('ng-enter-active')).toBe(true);
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 11000, elapsedTime: 11 });
- $animate.triggerCallbackPromise();
- }
- expect(element.hasClass('xyz')).toBe(true);
- }));
-
-
- it('should only append active to the newly append CSS className values',
- inject(function($animate, $rootScope, $sniffer, $rootElement) {
-
- ss.addRule('.ng-enter', '-webkit-transition:9s linear all;' +
- 'transition:9s linear all;');
-
- var parent = jqLite('
');
- var element = parent.find('span');
- $rootElement.append(parent);
- angular.element(document.body).append($rootElement);
-
- element.attr('class','one two');
-
- $animate.enter(element, parent);
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(element.hasClass('one')).toBe(true);
- expect(element.hasClass('two')).toBe(true);
- expect(element.hasClass('ng-enter')).toBe(true);
- expect(element.hasClass('ng-enter-active')).toBe(true);
- expect(element.hasClass('one-active')).toBe(false);
- expect(element.hasClass('two-active')).toBe(false);
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 3000, elapsedTime: 3 });
- }
-
- expect(element.hasClass('one')).toBe(true);
- expect(element.hasClass('two')).toBe(true);
- }));
- });
-
-
- describe("Callbacks", function() {
-
- beforeEach(function() {
- module(function($animateProvider) {
- $animateProvider.register('.custom', function($timeout) {
- return {
- removeClass: function(element, className, done) {
- $timeout(done, 2000);
- }
- };
- });
- $animateProvider.register('.other', function($timeout) {
- return {
- enter: function(element, done) {
- $timeout(done, 10000);
- }
- };
- });
- });
- });
-
-
- it("should fire the enter callback",
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) {
-
- var parent = jqLite('
');
- var element = parent.find('span');
- $rootElement.append(parent);
- body.append($rootElement);
-
- var flag = false;
- $animate.enter(element, parent, null).then(function() {
- flag = true;
- });
- $rootScope.$digest();
-
- $animate.triggerCallbackPromise();
-
- expect(flag).toBe(true);
- }));
-
-
- it("should fire the leave callback",
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) {
-
- var parent = jqLite('
');
- var element = parent.find('span');
- $rootElement.append(parent);
- body.append($rootElement);
-
- var flag = false;
- $animate.leave(element).then(function() {
- flag = true;
- });
- $rootScope.$digest();
-
- $animate.triggerCallbackPromise();
-
- expect(flag).toBe(true);
- }));
-
-
- it("should fire the move callback",
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) {
-
- var parent = jqLite('
');
- var parent2 = jqLite('
');
- var element = parent.find('span');
- $rootElement.append(parent);
- body.append($rootElement);
-
- var flag = false;
- $animate.move(element, parent, parent2).then(function() {
- flag = true;
- });
- $rootScope.$digest();
-
- $animate.triggerCallbackPromise();
-
- expect(flag).toBe(true);
- expect(element.parent().id).toBe(parent2.id);
-
- dealoc(element);
- }));
-
-
- it("should fire the addClass/removeClass callbacks",
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) {
-
- var parent = jqLite('
');
- var element = parent.find('span');
- $rootElement.append(parent);
- body.append($rootElement);
-
- var signature = '';
- $animate.addClass(element, 'on').then(function() {
- signature += 'A';
- });
- $rootScope.$digest();
- $animate.triggerReflow();
-
- $animate.removeClass(element, 'on').then(function() {
- signature += 'B';
- });
- $rootScope.$digest();
- $animate.triggerReflow();
-
- $animate.triggerCallbackPromise();
-
- expect(signature).toBe('AB');
- }));
-
- it("should fire the setClass callback",
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) {
-
- var parent = jqLite('
');
- var element = parent.find('span');
- $rootElement.append(parent);
- body.append($rootElement);
-
- expect(element.hasClass('on')).toBe(false);
- expect(element.hasClass('off')).toBe(true);
-
- var signature = '';
- $animate.setClass(element, 'on', 'off').then(function() {
- signature += 'Z';
- });
- $rootScope.$digest();
-
- $animate.triggerReflow();
- $animate.triggerCallbackPromise();
-
- expect(signature).toBe('Z');
- expect(element.hasClass('on')).toBe(true);
- expect(element.hasClass('off')).toBe(false);
- }));
-
- it('should fire DOM callbacks on the element being animated',
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) {
-
- if (!$sniffer.transitions) return;
-
- $animate.enabled(true);
-
- ss.addRule('.klass-add', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
-
- var element = jqLite('
');
- $rootElement.append(element);
- body.append($rootElement);
-
- var steps = [];
- element.on('$animate:before', function(e, data) {
- steps.push(['before', data.className, data.event]);
- });
-
- element.on('$animate:after', function(e, data) {
- steps.push(['after', data.className, data.event]);
- });
-
- element.on('$animate:close', function(e, data) {
- steps.push(['close', data.className, data.event]);
- });
-
- $animate.addClass(element, 'klass').then(function() {
- steps.push(['done', 'klass', 'addClass']);
- });
- $rootScope.$digest();
-
- $animate.triggerCallbackEvents();
-
- expect(steps.pop()).toEqual(['before', 'klass', 'addClass']);
-
- $animate.triggerReflow();
-
- $animate.triggerCallbackEvents();
-
- expect(steps.pop()).toEqual(['after', 'klass', 'addClass']);
-
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
-
- $animate.triggerCallbackEvents();
-
- expect(steps.shift()).toEqual(['close', 'klass', 'addClass']);
-
- $animate.triggerCallbackPromise();
-
- expect(steps.shift()).toEqual(['done', 'klass', 'addClass']);
- }));
-
- it('should fire the DOM callbacks even if no animation is rendered',
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) {
-
- $animate.enabled(true);
-
- var parent = jqLite('
');
- var element = jqLite('
');
- $rootElement.append(parent);
- body.append($rootElement);
-
- var steps = [];
- element.on('$animate:before', function(e, data) {
- steps.push(['before', data.className, data.event]);
- });
-
- element.on('$animate:after', function(e, data) {
- steps.push(['after', data.className, data.event]);
- });
-
- $animate.enter(element, parent);
- $rootScope.$digest();
-
- $animate.triggerCallbackEvents();
-
- expect(steps.shift()).toEqual(['before', 'ng-enter', 'enter']);
- expect(steps.shift()).toEqual(['after', 'ng-enter', 'enter']);
- }));
-
- it('should not fire DOM callbacks on the element being animated unless registered',
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement, $timeout) {
-
- $animate.enabled(true);
-
- var element = jqLite('
');
- $rootElement.append(element);
- body.append($rootElement);
-
- $animate.addClass(element, 'class');
- $rootScope.$digest();
-
- $timeout.verifyNoPendingTasks();
- }));
-
- it("should fire a done callback when provided with no animation",
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) {
-
- var parent = jqLite('
');
- var element = parent.find('span');
- $rootElement.append(parent);
- body.append($rootElement);
-
- var flag = false;
- $animate.removeClass(element, 'ng-hide').then(function() {
- flag = true;
- });
- $rootScope.$digest();
-
- $animate.triggerCallbackPromise();
- expect(flag).toBe(true);
- }));
-
-
- it("should fire a done callback when provided with a css animation/transition",
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) {
-
- ss.addRule('.ng-hide-add', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
- ss.addRule('.ng-hide-remove', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
-
- var parent = jqLite('
');
- $rootElement.append(parent);
- body.append($rootElement);
- var element = parent.find('span');
-
- var flag = false;
- $animate.addClass(element, 'ng-hide').then(function() {
- flag = true;
- });
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
- $animate.triggerCallbackPromise();
- expect(flag).toBe(true);
- }));
-
-
- it("should fire a done callback when provided with a JS animation",
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) {
-
- var parent = jqLite('
');
- $rootElement.append(parent);
- body.append($rootElement);
- var element = parent.find('span');
- element.addClass('custom');
-
- var flag = false;
- $animate.removeClass(element, 'ng-hide').then(function() {
- flag = true;
- });
- $rootScope.$digest();
-
- $animate.triggerCallbackPromise();
- expect(flag).toBe(true);
- }));
-
-
- it("should fire the callback right away if another animation is called right after",
- inject(function($animate, $rootScope, $compile, $sniffer, $rootElement) {
-
- ss.addRule('.ng-hide-add', '-webkit-transition:9s linear all;' +
- 'transition:9s linear all;');
- ss.addRule('.ng-hide-remove', '-webkit-transition:9s linear all;' +
- 'transition:9s linear all;');
-
- var parent = jqLite('
');
- $rootElement.append(parent);
- body.append($rootElement);
- var element = parent.find('span');
-
- var signature = '';
- $animate.removeClass(element, 'ng-hide').then(function() {
- signature += 'A';
- });
- $rootScope.$digest();
- $animate.addClass(element, 'ng-hide').then(function() {
- signature += 'B';
- });
- $rootScope.$digest();
-
- $animate.addClass(element, 'ng-hide'); //earlier animation cancelled
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 9 });
- }
- $animate.triggerCallbackPromise();
- expect(signature).toBe('AB');
- }));
- });
-
- describe("options", function() {
-
- it('should add and remove the temporary className value is provided', function() {
- var captures = {};
- module(function($animateProvider) {
- $animateProvider.register('.capture', function() {
- return {
- enter: capture('enter'),
- leave: capture('leave'),
- move: capture('move'),
- addClass: capture('addClass'),
- removeClass: capture('removeClass'),
- setClass: capture('setClass')
- };
-
- function capture(event) {
- return function(element, add, remove, styles, done) {
- //some animations only have one extra param
- done = arguments[arguments.length - 2]; //the last one is the styles array
- captures[event]=done;
- };
- }
- });
- });
- inject(function($animate, $rootScope, $compile, $rootElement, $document) {
- var container = jqLite('
');
- var container2 = jqLite('
');
- var element = jqLite('
');
- $rootElement.append(container);
- $rootElement.append(container2);
- angular.element($document[0].body).append($rootElement);
-
- $compile(element)($rootScope);
-
- assertTempClass('enter', 'temp-enter', function() {
- $animate.enter(element, container, null, {
- tempClasses: 'temp-enter'
- });
- });
-
- assertTempClass('move', 'temp-move', function() {
- $animate.move(element, null, container2, {
- tempClasses: 'temp-move'
- });
- });
-
- assertTempClass('addClass', 'temp-add', function() {
- $animate.addClass(element, 'add', {
- tempClasses: 'temp-add'
- });
- });
-
- assertTempClass('removeClass', 'temp-remove', function() {
- $animate.removeClass(element, 'add', {
- tempClasses: 'temp-remove'
- });
- });
-
- element.addClass('remove');
- assertTempClass('setClass', 'temp-set', function() {
- $animate.setClass(element, 'add', 'remove', {
- tempClasses: 'temp-set'
- });
- });
-
- assertTempClass('leave', 'temp-leave', function() {
- $animate.leave(element, {
- tempClasses: 'temp-leave'
- });
- });
-
- function assertTempClass(event, className, animationOperation) {
- expect(element).not.toHaveClass(className);
- animationOperation();
- $rootScope.$digest();
- expect(element).toHaveClass(className);
- $animate.triggerReflow();
- captures[event]();
- $animate.triggerCallbacks();
- expect(element).not.toHaveClass(className);
- }
- });
- });
- });
-
- describe("addClass / removeClass", function() {
-
- var captured;
- beforeEach(function() {
- module(function($animateProvider) {
- $animateProvider.register('.klassy', function($timeout) {
- return {
- addClass: function(element, className, done) {
- captured = 'addClass-' + className;
- $timeout(done, 500, false);
- },
- removeClass: function(element, className, done) {
- captured = 'removeClass-' + className;
- $timeout(done, 3000, false);
- }
- };
- });
- });
- });
-
-
- it("should not perform an animation, and the followup DOM operation, if the class is " +
- "already present during addClass or not present during removeClass on the element",
- inject(function($animate, $rootScope, $sniffer, $rootElement) {
-
- var element = jqLite('
');
- $rootElement.append(element);
- body.append($rootElement);
-
- //skipped animations
- captured = 'none';
- $animate.removeClass(element, 'some-class');
- $rootScope.$digest();
- expect(element.hasClass('some-class')).toBe(false);
- expect(captured).toBe('none');
-
- element.addClass('some-class');
-
- captured = 'nothing';
- $animate.addClass(element, 'some-class');
- $rootScope.$digest();
- expect(captured).toBe('nothing');
- expect(element.hasClass('some-class')).toBe(true);
-
- //actual animations
- captured = 'none';
- $animate.removeClass(element, 'some-class');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(element.hasClass('some-class')).toBe(false);
- expect(captured).toBe('removeClass-some-class');
-
- captured = 'nothing';
- $animate.addClass(element, 'some-class');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(element.hasClass('some-class')).toBe(true);
- expect(captured).toBe('addClass-some-class');
- }));
-
- it("should perform the animation if passed native dom element",
- inject(function($animate, $rootScope, $sniffer, $rootElement) {
-
- var element = jqLite('
');
- $rootElement.append(element);
- body.append($rootElement);
-
- //skipped animations
- captured = 'none';
- $animate.removeClass(element[0], 'some-class');
- $rootScope.$digest();
- expect(element.hasClass('some-class')).toBe(false);
- expect(captured).toBe('none');
-
- element.addClass('some-class');
-
- captured = 'nothing';
- $animate.addClass(element[0], 'some-class');
- $rootScope.$digest();
- expect(captured).toBe('nothing');
- expect(element.hasClass('some-class')).toBe(true);
-
- //actual animations
- captured = 'none';
- $animate.removeClass(element[0], 'some-class');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(element.hasClass('some-class')).toBe(false);
- expect(captured).toBe('removeClass-some-class');
-
- captured = 'nothing';
- $animate.addClass(element[0], 'some-class');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(element.hasClass('some-class')).toBe(true);
- expect(captured).toBe('addClass-some-class');
- }));
-
- it("should add and remove CSS classes after an animation even if no animation is present",
- inject(function($animate, $rootScope, $sniffer, $rootElement) {
-
- var parent = jqLite('
');
- $rootElement.append(parent);
- body.append($rootElement);
- var element = jqLite(parent.find('span'));
-
- $animate.addClass(element,'klass');
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect(element.hasClass('klass')).toBe(true);
-
- $animate.removeClass(element,'klass');
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect(element.hasClass('klass')).toBe(false);
- expect(element.hasClass('klass-remove')).toBe(false);
- expect(element.hasClass('klass-remove-active')).toBe(false);
- }));
-
-
- it("should add and remove CSS classes with a callback",
- inject(function($animate, $rootScope, $sniffer, $rootElement) {
-
- var parent = jqLite('
');
- $rootElement.append(parent);
- body.append($rootElement);
- var element = jqLite(parent.find('span'));
-
- var signature = '';
-
- $animate.addClass(element,'klass').then(function() {
- signature += 'A';
- });
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect(element.hasClass('klass')).toBe(true);
-
- $animate.removeClass(element,'klass').then(function() {
- signature += 'B';
- });
- $rootScope.$digest();
- $animate.triggerReflow();
-
- $animate.triggerCallbackPromise();
- expect(element.hasClass('klass')).toBe(false);
- expect(signature).toBe('AB');
- }));
-
-
- it("should end the current addClass animation, add the CSS class and then run the removeClass animation",
- inject(function($animate, $rootScope, $sniffer, $rootElement) {
-
- ss.addRule('.klass-add', '-webkit-transition:3s linear all;' +
- 'transition:3s linear all;');
- ss.addRule('.klass-remove', '-webkit-transition:3s linear all;' +
- 'transition:3s linear all;');
-
- var parent = jqLite('
');
- $rootElement.append(parent);
- body.append($rootElement);
- var element = jqLite(parent.find('span'));
-
- var signature = '';
-
- $animate.addClass(element,'klass').then(function() {
- signature += '1';
- });
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- expect(element.hasClass('klass-add')).toBe(true);
- $animate.triggerReflow();
- expect(element.hasClass('klass')).toBe(true);
- expect(element.hasClass('klass-add-active')).toBe(true);
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 3000, elapsedTime: 3 });
- }
-
- $animate.triggerCallbackPromise();
-
- //this cancels out the older animation
- $animate.removeClass(element,'klass').then(function() {
- signature += '2';
- });
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- expect(element.hasClass('klass-remove')).toBe(true);
-
- $animate.triggerReflow();
- expect(element.hasClass('klass')).toBe(false);
- expect(element.hasClass('klass-add')).toBe(false);
- expect(element.hasClass('klass-add-active')).toBe(false);
-
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 3000, elapsedTime: 3 });
- }
-
- $animate.triggerCallbackPromise();
-
- expect(element.hasClass('klass')).toBe(false);
- expect(signature).toBe('12');
- }));
-
-
- it("should properly execute JS animations and use callbacks when using addClass / removeClass",
- inject(function($animate, $rootScope, $sniffer, $rootElement, $timeout) {
-
- var parent = jqLite('
');
- $rootElement.append(parent);
- body.append($rootElement);
- var element = jqLite(parent.find('span'));
-
- var signature = '';
-
- $animate.addClass(element,'klassy').then(function() {
- signature += 'X';
- });
- $rootScope.$digest();
- $animate.triggerReflow();
-
- $timeout.flush(500);
-
- expect(element.hasClass('klassy')).toBe(true);
-
- $animate.removeClass(element,'klassy').then(function() {
- signature += 'Y';
- });
- $rootScope.$digest();
- $animate.triggerReflow();
-
- $timeout.flush(3000);
-
- expect(element.hasClass('klassy')).toBe(false);
-
- $animate.triggerCallbackPromise();
- expect(signature).toBe('XY');
- }));
-
- it("should properly execute JS animations if passed native dom element",
- inject(function($animate, $rootScope, $sniffer, $rootElement, $timeout) {
-
- var parent = jqLite('
');
- $rootElement.append(parent);
- body.append($rootElement);
- var element = jqLite(parent.find('span'));
-
- var signature = '';
-
- $animate.addClass(element[0],'klassy').then(function() {
- signature += 'X';
- });
- $rootScope.$digest();
- $animate.triggerReflow();
-
- $timeout.flush(500);
-
- expect(element.hasClass('klassy')).toBe(true);
-
- $animate.removeClass(element[0],'klassy').then(function() {
- signature += 'Y';
- });
- $rootScope.$digest();
- $animate.triggerReflow();
-
- $timeout.flush(3000);
-
- expect(element.hasClass('klassy')).toBe(false);
-
- $animate.triggerCallbackPromise();
- expect(signature).toBe('XY');
- }));
-
- it("should properly execute CSS animations/transitions and use callbacks when using addClass / removeClass",
- inject(function($animate, $rootScope, $sniffer, $rootElement) {
-
- ss.addRule('.klass-add', '-webkit-transition:11s linear all;' +
- 'transition:11s linear all;');
- ss.addRule('.klass-remove', '-webkit-transition:11s linear all;' +
- 'transition:11s linear all;');
-
- var parent = jqLite('
');
- $rootElement.append(parent);
- body.append($rootElement);
- var element = jqLite(parent.find('span'));
-
- var signature = '';
-
- $animate.addClass(element,'klass').then(function() {
- signature += 'd';
- });
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(element.hasClass('klass-add')).toBe(true);
- expect(element.hasClass('klass-add-active')).toBe(true);
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 11000, elapsedTime: 11 });
- expect(element.hasClass('klass-add')).toBe(false);
- expect(element.hasClass('klass-add-active')).toBe(false);
- }
-
- $animate.triggerCallbackPromise();
- expect(element.hasClass('klass')).toBe(true);
-
- $animate.removeClass(element,'klass').then(function() {
- signature += 'b';
- });
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(element.hasClass('klass-remove')).toBe(true);
- expect(element.hasClass('klass-remove-active')).toBe(true);
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 11000, elapsedTime: 11 });
- expect(element.hasClass('klass-remove')).toBe(false);
- expect(element.hasClass('klass-remove-active')).toBe(false);
- }
-
- $animate.triggerCallbackPromise();
- expect(element.hasClass('klass')).toBe(false);
-
- expect(signature).toBe('db');
- }));
-
-
- it("should allow for multiple css classes to be animated plus a callback when added",
- inject(function($animate, $rootScope, $sniffer, $rootElement) {
-
- ss.addRule('.one-add', '-webkit-transition:7s linear all;' +
- 'transition:7s linear all;');
- ss.addRule('.two-add', '-webkit-transition:7s linear all;' +
- 'transition:7s linear all;');
-
- var parent = jqLite('
');
- $rootElement.append(parent);
- body.append($rootElement);
- var element = jqLite(parent.find('span'));
-
- var flag = false;
- $animate.addClass(element,'one two').then(function() {
- flag = true;
- });
-
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(element.hasClass('one-add')).toBe(true);
- expect(element.hasClass('two-add')).toBe(true);
-
- expect(element.hasClass('one-add-active')).toBe(true);
- expect(element.hasClass('two-add-active')).toBe(true);
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 7000, elapsedTime: 7 });
-
- expect(element.hasClass('one-add')).toBe(false);
- expect(element.hasClass('one-add-active')).toBe(false);
- expect(element.hasClass('two-add')).toBe(false);
- expect(element.hasClass('two-add-active')).toBe(false);
- }
-
- $animate.triggerCallbackPromise();
-
- expect(element.hasClass('one')).toBe(true);
- expect(element.hasClass('two')).toBe(true);
-
- expect(flag).toBe(true);
- }));
-
-
- it("should allow for multiple css classes to be animated plus a callback when removed",
- inject(function($animate, $rootScope, $sniffer, $rootElement) {
-
- ss.addRule('.one-remove', '-webkit-transition:9s linear all;' +
- 'transition:9s linear all;');
- ss.addRule('.two-remove', '-webkit-transition:9s linear all;' +
- 'transition:9s linear all;');
-
- var parent = jqLite('
');
- $rootElement.append(parent);
- body.append($rootElement);
- var element = jqLite(parent.find('span'));
-
- element.addClass('one two');
- expect(element.hasClass('one')).toBe(true);
- expect(element.hasClass('two')).toBe(true);
-
- var flag = false;
- $animate.removeClass(element,'one two').then(function() {
- flag = true;
- });
- $rootScope.$digest();
-
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(element.hasClass('one-remove')).toBe(true);
- expect(element.hasClass('two-remove')).toBe(true);
-
- expect(element.hasClass('one-remove-active')).toBe(true);
- expect(element.hasClass('two-remove-active')).toBe(true);
- browserTrigger(element,'transitionend', { timeStamp: Date.now() + 9000, elapsedTime: 9 });
+describe("animations", function() {
- expect(element.hasClass('one-remove')).toBe(false);
- expect(element.hasClass('one-remove-active')).toBe(false);
- expect(element.hasClass('two-remove')).toBe(false);
- expect(element.hasClass('two-remove-active')).toBe(false);
- }
+ beforeEach(module('ngAnimate'));
- $animate.triggerCallbackPromise();
+ var element, applyAnimationClasses;
+ afterEach(inject(function($$jqLite) {
+ applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+ dealoc(element);
+ }));
- expect(element.hasClass('one')).toBe(false);
- expect(element.hasClass('two')).toBe(false);
+ it('should allow animations if the application is bootstrapped on the document node', function() {
+ var capturedAnimation;
- expect(flag).toBe(true);
- }));
+ module(function($provide) {
+ $provide.factory('$rootElement', function($document) {
+ return $document;
+ });
+ $provide.factory('$$animation', function($$AnimateRunner) {
+ return function() {
+ capturedAnimation = arguments;
+ return new $$AnimateRunner();
+ };
});
});
- var $rootElement, $document;
- beforeEach(module(function() {
- return function(_$rootElement_, _$document_, $animate) {
- $rootElement = _$rootElement_;
- $document = _$document_;
- $animate.enabled(true);
- };
- }));
-
- function html(element) {
- var body = jqLite($document[0].body);
- $rootElement.append(element);
- body.append($rootElement);
- return element;
- }
-
-
- it("should properly animate and parse CSS3 transitions",
- inject(function($compile, $rootScope, $animate, $sniffer) {
-
- ss.addRule('.ng-enter', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
+ inject(function($animate, $rootScope, $$body) {
+ $animate.enabled(true);
- var element = html($compile('
...
')($rootScope));
- var child = $compile('
...
')($rootScope);
+ element = jqLite('
');
- $animate.enter(child, element);
+ $animate.enter(element, $$body);
$rootScope.$digest();
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(child.hasClass('ng-enter')).toBe(true);
- expect(child.hasClass('ng-enter-active')).toBe(true);
- browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
-
- expect(child.hasClass('ng-enter')).toBe(false);
- expect(child.hasClass('ng-enter-active')).toBe(false);
- }));
-
-
- it("should properly animate and parse CSS3 animations",
- inject(function($compile, $rootScope, $animate, $sniffer) {
+ expect(capturedAnimation).toBeTruthy();
+ });
+ });
- ss.addRule('.ng-enter', '-webkit-animation: some_animation 4s linear 1s 2 alternate;' +
- 'animation: some_animation 4s linear 1s 2 alternate;');
+ describe('during bootstrap', function() {
+ it('should be enabled only after the first digest is fired and the postDigest queue is empty',
+ inject(function($animate, $rootScope) {
- var element = html($compile('
...
')($rootScope));
- var child = $compile('
...
')($rootScope);
+ var capturedEnabledState;
+ $rootScope.$$postDigest(function() {
+ capturedEnabledState = $animate.enabled();
+ });
- $animate.enter(child, element);
+ expect($animate.enabled()).toBe(false);
$rootScope.$digest();
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(child.hasClass('ng-enter')).toBe(true);
- expect(child.hasClass('ng-enter-active')).toBe(true);
- browserTrigger(child,'transitionend', { timeStamp: Date.now() + 9000, elapsedTime: 9 });
- }
- expect(child.hasClass('ng-enter')).toBe(false);
- expect(child.hasClass('ng-enter-active')).toBe(false);
- }));
-
-
- it("should skip animations if the browser does not support CSS3 transitions and CSS3 animations",
- inject(function($compile, $rootScope, $animate, $sniffer) {
-
- $sniffer.animations = false;
- $sniffer.transitions = false;
-
- ss.addRule('.ng-enter', '-webkit-animation: some_animation 4s linear 1s 2 alternate;' +
- 'animation: some_animation 4s linear 1s 2 alternate;');
-
- var element = html($compile('
...
')($rootScope));
- var child = $compile('
...
')($rootScope);
-
- expect(child.hasClass('ng-enter')).toBe(false);
- $animate.enter(child, element);
- $rootScope.$digest();
- expect(child.hasClass('ng-enter')).toBe(false);
+ expect(capturedEnabledState).toBe(false);
+ expect($animate.enabled()).toBe(true);
}));
+ it('should be disabled until all pending template requests have been downloaded', function() {
+ var mockTemplateRequest = {
+ totalPendingRequests: 2
+ };
- it("should run other defined animations inline with CSS3 animations", function() {
- module(function($animateProvider) {
- $animateProvider.register('.custom', function($timeout) {
- return {
- enter: function(element, done) {
- element.addClass('i-was-animated');
- $timeout(done, 10, false);
- }
- };
- });
+ module(function($provide) {
+ $provide.value('$templateRequest', mockTemplateRequest);
});
- inject(function($compile, $rootScope, $animate, $sniffer) {
-
- ss.addRule('.ng-enter', '-webkit-transition: 1s linear all;' +
- 'transition: 1s linear all;');
+ inject(function($animate, $rootScope) {
+ expect($animate.enabled()).toBe(false);
- var element = html($compile('
...
')($rootScope));
- var child = $compile('
...
')($rootScope);
-
- expect(child.hasClass('i-was-animated')).toBe(false);
-
- child.addClass('custom');
- $animate.enter(child, element);
$rootScope.$digest();
+ expect($animate.enabled()).toBe(false);
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- browserTrigger(child,'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- }
-
- expect(child.hasClass('i-was-animated')).toBe(true);
+ mockTemplateRequest.totalPendingRequests = 0;
+ $rootScope.$digest();
+ expect($animate.enabled()).toBe(true);
});
});
+ it('should stay disabled if set to be disabled even after all templates have been fully downloaded', function() {
+ var mockTemplateRequest = {
+ totalPendingRequests: 2
+ };
- it("should properly cancel CSS transitions or animations if another animation is fired", function() {
- module(function($animateProvider) {
- $animateProvider.register('.usurper', function($timeout) {
- return {
- leave: function(element, done) {
- element.addClass('this-is-mine-now');
- $timeout(done, 55, false);
- }
- };
- });
+ module(function($provide) {
+ $provide.value('$templateRequest', mockTemplateRequest);
});
- inject(function($compile, $rootScope, $animate, $sniffer, $timeout) {
- ss.addRule('.ng-enter', '-webkit-transition: 2s linear all;' +
- 'transition: 2s linear all;');
- ss.addRule('.ng-leave', '-webkit-transition: 2s linear all;' +
- 'transition: 2s linear all;');
-
- var element = html($compile('
...
')($rootScope));
- var child = $compile('
...
')($rootScope);
+ inject(function($animate, $rootScope) {
+ $animate.enabled(false);
+ expect($animate.enabled()).toBe(false);
- $animate.enter(child, element);
$rootScope.$digest();
+ expect($animate.enabled()).toBe(false);
- //this is added/removed right away otherwise
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- expect(child.hasClass('ng-enter')).toBe(true);
- expect(child.hasClass('ng-enter-active')).toBe(true);
- }
-
- expect(child.hasClass('this-is-mine-now')).toBe(false);
- child.addClass('usurper');
- $animate.leave(child);
+ mockTemplateRequest.totalPendingRequests = 0;
$rootScope.$digest();
- $animate.triggerCallbackPromise();
-
- expect(child.hasClass('ng-enter')).toBe(false);
- expect(child.hasClass('ng-enter-active')).toBe(false);
-
- expect(child.hasClass('usurper')).toBe(true);
- expect(child.hasClass('this-is-mine-now')).toBe(true);
-
- $timeout.flush(55);
+ expect($animate.enabled()).toBe(false);
});
});
+ });
+ describe('$animate', function() {
+ var parent;
+ var parent2;
+ var options;
+ var capturedAnimation;
+ var capturedAnimationHistory;
+ var overriddenAnimationRunner;
+ var defaultFakeAnimationRunner;
- it("should not perform the active class animation if the animation has been cancelled before the reflow occurs", function() {
- inject(function($compile, $rootScope, $animate, $sniffer) {
- if (!$sniffer.transitions) return;
-
- ss.addRule('.animated.ng-enter', '-webkit-transition: 2s linear all;' +
- 'transition: 2s linear all;');
-
- var element = html($compile('
...
')($rootScope));
- var child = $compile('
...
')($rootScope);
-
- $animate.enter(child, element);
- $rootScope.$digest();
-
- expect(child.hasClass('ng-enter')).toBe(true);
-
- $animate.leave(child);
- $rootScope.$digest();
-
- $animate.triggerReflow();
- expect(child.hasClass('ng-enter-active')).toBe(false);
- });
- });
+ beforeEach(module(function($provide) {
+ overriddenAnimationRunner = null;
+ capturedAnimation = null;
+ capturedAnimationHistory = [];
- //
- // it("should add and remove CSS classes and perform CSS animations during the process",
- // inject(function($compile, $rootScope, $animate, $sniffer, $timeout) {
- //
- // ss.addRule('.on-add', '-webkit-transition: 10s linear all; ' +
- // 'transition: 10s linear all;');
- // ss.addRule('.on-remove', '-webkit-transition: 10s linear all; ' +
- // 'transition: 10s linear all;');
- //
- // var element = html($compile('
')($rootScope));
- //
- // expect(element.hasClass('on')).toBe(false);
- //
- // $animate.addClass(element, 'on');
- //
- // if($sniffer.transitions) {
- // expect(element.hasClass('on')).toBe(false);
- // expect(element.hasClass('on-add')).toBe(true);
- // $animate.triggerCallbackPromise();
- // }
- //
- // $animate.triggerCallbackPromise();
- //
- // expect(element.hasClass('on')).toBe(true);
- // expect(element.hasClass('on-add')).toBe(false);
- // expect(element.hasClass('on-add-active')).toBe(false);
- //
- // $animate.removeClass(element, 'on');
- // if($sniffer.transitions) {
- // expect(element.hasClass('on')).toBe(true);
- // expect(element.hasClass('on-remove')).toBe(true);
- // $timeout.flush(10000);
- // }
- //
- // $animate.triggerCallbackPromise();
- // expect(element.hasClass('on')).toBe(false);
- // expect(element.hasClass('on-remove')).toBe(false);
- // expect(element.hasClass('on-remove-active')).toBe(false);
- // }));
- //
- //
- // it("should show and hide elements with CSS & JS animations being performed in the process", function() {
- // module(function($animateProvider) {
- // $animateProvider.register('.displayer', function($timeout) {
- // return {
- // removeClass : function(element, className, done) {
- // element.removeClass('hiding');
- // element.addClass('showing');
- // $timeout(done, 25, false);
- // },
- // addClass : function(element, className, done) {
- // element.removeClass('showing');
- // element.addClass('hiding');
- // $timeout(done, 555, false);
- // }
- // }
- // });
- // })
- // inject(function($compile, $rootScope, $animate, $sniffer, $timeout) {
- //
- // ss.addRule('.ng-hide-add', '-webkit-transition: 5s linear all;' +
- // 'transition: 5s linear all;');
- // ss.addRule('.ng-hide-remove', '-webkit-transition: 5s linear all;' +
- // 'transition: 5s linear all;');
- //
- // var element = html($compile('
')($rootScope));
- //
- // element.addClass('displayer');
- //
- // expect(element).toBeShown();
- // expect(element.hasClass('showing')).toBe(false);
- // expect(element.hasClass('hiding')).toBe(false);
- //
- // $animate.addClass(element, 'ng-hide');
- //
- // if($sniffer.transitions) {
- // expect(element).toBeShown(); //still showing
- // $animate.triggerCallbackPromise();
- // expect(element).toBeShown();
- // $timeout.flush(5555);
- // }
- // $animate.triggerCallbackPromise();
- // expect(element).toBeHidden();
- //
- // expect(element.hasClass('showing')).toBe(false);
- // expect(element.hasClass('hiding')).toBe(true);
- // $animate.removeClass(element, 'ng-hide');
- //
- // if($sniffer.transitions) {
- // expect(element).toBeHidden();
- // $animate.triggerCallbackPromise();
- // expect(element).toBeHidden();
- // $timeout.flush(5580);
- // }
- // $animate.triggerCallbackPromise();
- // expect(element).toBeShown();
- //
- // expect(element.hasClass('showing')).toBe(true);
- // expect(element.hasClass('hiding')).toBe(false);
- // });
- // });
-
-
- it("should remove all the previous classes when the next animation is applied before a reflow", function() {
- var fn, interceptedClass;
- module(function($animateProvider) {
- $animateProvider.register('.three', function() {
- return {
- move: function(element, done) {
- fn = function() {
- done();
- };
- return function() {
- interceptedClass = element.attr('class');
- };
- }
- };
- });
+ options = {};
+ $provide.value('$$animation', function() {
+ capturedAnimationHistory.push(capturedAnimation = arguments);
+ return overriddenAnimationRunner || defaultFakeAnimationRunner;
});
- inject(function($compile, $rootScope, $animate) {
- var parent = html($compile('
')($rootScope));
- var one = $compile('
')($rootScope);
- var two = $compile('
')($rootScope);
- var three = $compile('
')($rootScope);
- parent.append(one);
- parent.append(two);
- parent.append(three);
-
- $animate.move(three, null, two);
- $rootScope.$digest();
-
- $animate.move(three, null, one);
- $rootScope.$digest();
+ return function($rootElement, $q, $animate, $$AnimateRunner, $$body) {
+ defaultFakeAnimationRunner = new $$AnimateRunner();
+ $animate.enabled(true);
- //this means that the former animation was cleaned up before the new one starts
- expect(interceptedClass.indexOf('ng-animate') >= 0).toBe(false);
- });
- });
+ element = jqLite('
element
');
+ parent = jqLite('
parent
');
+ parent2 = jqLite('
parent
');
+ $rootElement.append(parent);
+ $rootElement.append(parent2);
+ $$body.append($rootElement);
+ };
+ }));
- it("should provide the correct CSS class to the addClass and removeClass callbacks within a JS animation", function() {
+ it('should animate only the specified CSS className matched within $animateProvider.classNameFilter', function() {
module(function($animateProvider) {
- $animateProvider.register('.classify', function() {
- return {
- removeClass: function(element, className, done) {
- element.data('classify','remove-' + className);
- done();
- },
- addClass: function(element, className, done) {
- element.data('classify','add-' + className);
- done();
- }
- };
- });
+ $animateProvider.classNameFilter(/only-allow-this-animation/);
});
- inject(function($compile, $rootScope, $animate) {
- var element = html($compile('
')($rootScope));
+ inject(function($animate, $rootScope) {
+ expect(element).not.toHaveClass('only-allow-this-animation');
- $animate.addClass(element, 'super');
+ $animate.enter(element, parent);
$rootScope.$digest();
- $animate.triggerReflow();
- expect(element.data('classify')).toBe('add-super');
+ expect(capturedAnimation).toBeFalsy();
- $animate.removeClass(element, 'super');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(element.data('classify')).toBe('remove-super');
+ element.addClass('only-allow-this-animation');
- $animate.addClass(element, 'superguy');
+ $animate.leave(element, parent);
$rootScope.$digest();
- $animate.triggerReflow();
- expect(element.data('classify')).toBe('add-superguy');
+ expect(capturedAnimation).toBeTruthy();
});
});
+ they('should nullify both options.$prop when passed into an animation if it is not a string or an array', ['addClass', 'removeClass'], function(prop) {
+ inject(function($animate, $rootScope) {
+ var options1 = {};
+ options1[prop] = function() {};
+ $animate.enter(element, parent, null, options1);
- it("should not skip ngAnimate animations when any pre-existing CSS transitions are present on the element", function() {
- inject(function($compile, $rootScope, $animate, $timeout, $sniffer) {
- if (!$sniffer.transitions) return;
-
- var element = html($compile('
')($rootScope));
- var child = html($compile('
')($rootScope));
+ expect(options1[prop]).toBeFalsy();
+ $rootScope.$digest();
- ss.addRule('.animated', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
- ss.addRule('.super-add', '-webkit-transition:2s linear all;' +
- 'transition:2s linear all;');
+ var options2 = {};
+ options2[prop] = true;
+ $animate.leave(element, options2);
- $rootElement.append(element);
- jqLite(document.body).append($rootElement);
+ expect(options2[prop]).toBeFalsy();
+ $rootScope.$digest();
- $animate.addClass(element, 'super');
+ capturedAnimation = null;
- var empty = true;
- try {
- $animate.triggerReflow();
- empty = false;
+ var options3 = {};
+ if (prop === 'removeClass') {
+ element.addClass('fatias');
}
- catch (e) {}
- expect(empty).toBe(false);
+ options3[prop] = ['fatias'];
+ $animate.enter(element, parent, null, options3);
+ expect(options3[prop]).toBe('fatias');
});
});
-
- it("should wait until both the duration and delay are complete to close off the animation",
- inject(function($compile, $rootScope, $animate, $timeout, $sniffer) {
-
- if (!$sniffer.transitions) return;
-
- var element = html($compile('
')($rootScope));
- var child = html($compile('
')($rootScope));
-
- ss.addRule('.animated.ng-enter', '-webkit-transition: width 1s, background 1s 1s;' +
- 'transition: width 1s, background 1s 1s;');
-
- $rootElement.append(element);
- jqLite(document.body).append($rootElement);
-
- $animate.enter(child, element);
- $rootScope.$digest();
- $animate.triggerReflow();
-
- expect(child.hasClass('ng-enter')).toBe(true);
- expect(child.hasClass('ng-enter-active')).toBe(true);
-
- browserTrigger(child, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 0 });
-
- expect(child.hasClass('ng-enter')).toBe(true);
- expect(child.hasClass('ng-enter-active')).toBe(true);
-
- browserTrigger(child, 'transitionend', { timeStamp: Date.now() + 2000, elapsedTime: 2 });
-
- expect(child.hasClass('ng-enter')).toBe(false);
- expect(child.hasClass('ng-enter-active')).toBe(false);
-
- expect(element.contents().length).toBe(1);
- }));
-
-
- it("should cancel all child animations when a leave or move animation is triggered on a parent element", function() {
-
- var step, animationState;
+ it('should throw a minErr if a regex value is used which partially contains or fully matches the `ng-animate` CSS class', function() {
module(function($animateProvider) {
- $animateProvider.register('.animan', function() {
- return {
- enter: function(element, done) {
- animationState = 'enter';
- step = done;
- return function(cancelled) {
- animationState = cancelled ? 'enter-cancel' : animationState;
- };
- },
- addClass: function(element, className, done) {
- animationState = 'addClass';
- step = done;
- return function(cancelled) {
- animationState = cancelled ? 'addClass-cancel' : animationState;
- };
- }
- };
- });
- });
-
- inject(function($animate, $compile, $rootScope, $timeout, $sniffer) {
- var element = html($compile('
')($rootScope));
- var container = html($compile('
')($rootScope));
- var child = html($compile('
')($rootScope));
-
- ss.addRule('.animan.ng-enter, .animan.something-add', '-webkit-transition: width 1s, background 1s 1s;' +
- 'transition: width 1s, background 1s 1s;');
-
- $rootElement.append(element);
- jqLite(document.body).append($rootElement);
-
- $animate.enter(child, element);
- $rootScope.$digest();
-
- expect(animationState).toBe('enter');
- if ($sniffer.transitions) {
- expect(child.hasClass('ng-enter')).toBe(true);
- $animate.triggerReflow();
- expect(child.hasClass('ng-enter-active')).toBe(true);
- }
-
- $animate.move(element, container);
- if ($sniffer.transitions) {
- expect(child.hasClass('ng-enter')).toBe(false);
- expect(child.hasClass('ng-enter-active')).toBe(false);
- }
-
- expect(animationState).toBe('enter-cancel');
-
- $rootScope.$digest();
- $animate.triggerCallbacks();
+ assertError(/ng-animate/, true);
+ assertError(/first ng-animate last/, true);
+ assertError(/ng-animate-special/, false);
+ assertError(/first ng-animate-special last/, false);
+ assertError(/first ng-animate ng-animate-special last/, true);
+
+ function assertError(regex, bool) {
+ var expectation = expect(function() {
+ $animateProvider.classNameFilter(regex);
+ });
- $animate.addClass(child, 'something');
- $rootScope.$digest();
- if ($sniffer.transitions) {
- $animate.triggerReflow();
- }
- expect(animationState).toBe('addClass');
- if ($sniffer.transitions) {
- expect(child.hasClass('something-add')).toBe(true);
- expect(child.hasClass('something-add-active')).toBe(true);
- }
+ var message = '$animateProvider.classNameFilter(regex) prohibits accepting a regex value which matches/contains the "ng-animate" CSS class.';
- $animate.leave(container);
- expect(animationState).toBe('addClass-cancel');
- if ($sniffer.transitions) {
- expect(child.hasClass('something-add')).toBe(false);
- expect(child.hasClass('something-add-active')).toBe(false);
+ bool ? expectation.toThrowMinErr('$animate', 'nongcls', message)
+ : expectation.not.toThrowMinErr('$animate', 'nongcls', message);
}
});
-
});
- it('should coalesce all class-based animation calls together into a single animation', function() {
- var log = [];
- var track = function(name) {
- return function() {
- log.push({ name: name, className: arguments[1] });
- };
- };
+ it('should complete the leave DOM operation in case the classNameFilter fails', function() {
module(function($animateProvider) {
- $animateProvider.register('.animate', function() {
- return {
- addClass: track('addClass'),
- removeClass: track('removeClass')
- };
- });
+ $animateProvider.classNameFilter(/memorable-animation/);
});
- inject(function($rootScope, $animate, $compile, $rootElement, $document) {
- $animate.enabled(true);
-
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
- angular.element($document[0].body).append($rootElement);
-
- $animate.addClass(element, 'one');
- $animate.addClass(element, 'two');
- $animate.removeClass(element, 'three');
- $animate.removeClass(element, 'four');
- $animate.setClass(element, 'four five', 'two');
+ inject(function($animate, $rootScope) {
+ expect(element).not.toHaveClass('memorable-animation');
+ parent.append(element);
+ $animate.leave(element);
$rootScope.$digest();
- $animate.triggerReflow();
- expect(log.length).toBe(2);
- expect(log[0]).toEqual({ name: 'addClass', className: 'one four five' });
- expect(log[1]).toEqual({ name: 'removeClass', className: 'three' });
+ expect(capturedAnimation).toBeFalsy();
+ expect(element[0].parentNode).toBeFalsy();
});
});
- it('should intelligently cancel out redundant class-based animations', function() {
- var log = [];
- var track = function(name) {
- return function() {
- log.push({ name: name, className: arguments[1] });
- };
- };
- module(function($animateProvider) {
- $animateProvider.register('.animate', function() {
- return {
- addClass: track('addClass'),
- removeClass: track('removeClass')
- };
- });
- });
- inject(function($rootScope, $animate, $compile, $rootElement, $document) {
- $animate.enabled(true);
-
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
- angular.element($document[0].body).append($rootElement);
-
- $animate.removeClass(element, 'one');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(log.length).toBe(0);
- $animate.triggerCallbacks();
-
- $animate.addClass(element, 'two');
- $animate.addClass(element, 'two');
- $animate.removeClass(element, 'two');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(log.length).toBe(0);
- $animate.triggerCallbacks();
+ describe('enabled()', function() {
+ it("should work for all animations", inject(function($animate) {
- $animate.removeClass(element, 'three');
- $animate.addClass(element, 'three');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(log.length).toBe(0);
- $animate.triggerCallbacks();
+ expect($animate.enabled()).toBe(true);
- $animate.removeClass(element, 'four');
- $animate.addClass(element, 'four');
- $animate.removeClass(element, 'four');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(log.length).toBe(1);
- $animate.triggerCallbacks();
- expect(log[0]).toEqual({ name: 'removeClass', className: 'four' });
-
- $animate.addClass(element, 'five');
- $animate.addClass(element, 'five');
- $animate.addClass(element, 'five');
- $animate.removeClass(element, 'five');
- $animate.addClass(element, 'five');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(log.length).toBe(2);
- $animate.triggerCallbacks();
- expect(log[1]).toEqual({ name: 'addClass', className: 'five' });
- });
- });
+ expect($animate.enabled(0)).toBe(false);
+ expect($animate.enabled()).toBe(false);
- it('should skip class-based animations if the element is removed before the digest occurs', function() {
- var spy = jasmine.createSpy();
- module(function($animateProvider) {
- $animateProvider.register('.animated', function() {
- return {
- beforeAddClass: spy,
- beforeRemoveClass: spy,
- beforeSetClass: spy
- };
- });
- });
- inject(function($rootScope, $animate, $compile, $rootElement, $document) {
- $animate.enabled(true);
+ expect($animate.enabled(1)).toBe(true);
+ expect($animate.enabled()).toBe(true);
+ }));
- var one = $compile('
')($rootScope);
- var two = $compile('
')($rootScope);
- var three = $compile('
')($rootScope);
+ it('should fully disable all animations in the application if false',
+ inject(function($animate, $rootScope) {
- $rootElement.append(one);
- $rootElement.append(two);
- angular.element($document[0].body).append($rootElement);
+ $animate.enabled(false);
- $animate.addClass(one, 'active-class');
- one.remove();
+ $animate.enter(element, parent);
+ expect(capturedAnimation).toBeFalsy();
$rootScope.$digest();
- expect(spy).not.toHaveBeenCalled();
+ expect(capturedAnimation).toBeFalsy();
+ }));
- $animate.addClass(two, 'active-class');
+ it('should disable all animations on the given element',
+ inject(function($animate, $rootScope) {
- $rootScope.$digest();
- expect(spy).toHaveBeenCalled();
+ parent.append(element);
- spy.reset();
- $animate.removeClass(two, 'active-class');
- two.remove();
+ $animate.enabled(element, false);
+ expect($animate.enabled(element)).toBeFalsy();
+ $animate.addClass(element, 'red');
+ expect(capturedAnimation).toBeFalsy();
$rootScope.$digest();
- expect(spy).not.toHaveBeenCalled();
+ expect(capturedAnimation).toBeFalsy();
- $animate.setClass(three, 'active-class', 'three');
- three.remove();
+ $animate.enabled(element, true);
+ expect($animate.enabled(element)).toBeTruthy();
+ $animate.addClass(element, 'blue');
+ expect(capturedAnimation).toBeFalsy();
$rootScope.$digest();
- expect(spy).not.toHaveBeenCalled();
- });
- });
-
- it('should skip class-based animations if ngRepeat has marked the element or its parent for removal', function() {
- var spy = jasmine.createSpy();
- module(function($animateProvider) {
- $animateProvider.register('.animated', function() {
- return {
- beforeAddClass: spy,
- beforeRemoveClass: spy,
- beforeSetClass: spy
- };
- });
- });
- inject(function($rootScope, $animate, $compile, $rootElement, $document) {
- $animate.enabled(true);
+ expect(capturedAnimation).toBeTruthy();
+ }));
- var element = $compile(
- '
' +
- '
' +
- ' {{ $index }} ' +
- '
' +
- '
'
- )($rootScope);
+ it('should disable all animations for a given element\'s children',
+ inject(function($animate, $rootScope) {
- $rootElement.append(element);
- angular.element($document[0].body).append($rootElement);
+ $animate.enabled(parent, false);
- $rootScope.items = [1,2,3];
+ $animate.enter(element, parent);
+ expect(capturedAnimation).toBeFalsy();
$rootScope.$digest();
+ expect(capturedAnimation).toBeFalsy();
- var child = element.find('div');
+ $animate.enabled(parent, true);
- $animate.addClass(child, 'start-animation');
- $rootScope.items = [2,3];
+ $animate.enter(element, parent);
+ expect(capturedAnimation).toBeFalsy();
$rootScope.$digest();
+ expect(capturedAnimation).toBeTruthy();
+ }));
+ });
- expect(spy).not.toHaveBeenCalled();
-
- var innerChild = element.find('span');
+ it('should strip all comment nodes from the animation and not issue an animation if not real elements are found',
+ inject(function($rootScope, $compile) {
- $animate.addClass(innerChild, 'start-animation');
- $rootScope.items = [3];
- $rootScope.$digest();
+ // since the ng-if results to false then only comments will be fed into the animation
+ element = $compile(
+ '
'
+ )($rootScope);
- expect(spy).not.toHaveBeenCalled();
- dealoc(element);
- });
- });
+ parent.append(element);
- it('should call class-based animation callbacks in the correct order when animations are skipped', function() {
- var continueAnimation;
- module(function($animateProvider) {
- $animateProvider.register('.animate', function() {
- return {
- addClass: function(element, className, done) {
- continueAnimation = done;
- }
- };
- });
- });
- inject(function($rootScope, $animate, $compile, $rootElement, $document) {
- $animate.enabled(true);
+ $rootScope.items = [1,2,3,4,5];
+ $rootScope.$digest();
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
- angular.element($document[0].body).append($rootElement);
+ expect(capturedAnimation).toBeFalsy();
+ }));
- var log = '';
- $animate.addClass(element, 'one').then(function() {
- log += 'A';
- });
- $rootScope.$digest();
+ it('should not attempt to perform an animation on a text node element',
+ inject(function($rootScope, $animate) {
- $animate.addClass(element, 'one').then(function() {
- log += 'B';
- });
- $rootScope.$digest();
- $animate.triggerCallbackPromise();
+ element.html('hello there');
+ var textNode = jqLite(element[0].firstChild);
- $animate.triggerReflow();
- continueAnimation();
- $animate.triggerCallbackPromise();
- expect(log).toBe('BA');
- });
- });
+ $animate.addClass(textNode, 'some-class');
+ $rootScope.$digest();
- it('should skip class-based animations when add class and remove class cancel each other out', function() {
- var spy = jasmine.createSpy();
- module(function($animateProvider) {
- $animateProvider.register('.animate', function() {
- return {
- addClass: spy,
- removeClass: spy
- };
- });
- });
- inject(function($rootScope, $animate, $compile) {
- $animate.enabled(true);
+ expect(capturedAnimation).toBeFalsy();
+ }));
- var element = $compile('
')($rootScope);
+ it('should perform the leave domOperation if a text node is used',
+ inject(function($rootScope, $animate) {
- var count = 0;
- var callback = function() {
- count++;
- };
+ element.html('hello there');
+ var textNode = jqLite(element[0].firstChild);
+ var parentNode = textNode[0].parentNode;
+
+ $animate.leave(textNode);
+ $rootScope.$digest();
+ expect(capturedAnimation).toBeFalsy();
+ expect(textNode[0].parentNode).not.toBe(parentNode);
+ }));
- $animate.addClass(element, 'on').then(callback);
- $animate.addClass(element, 'on').then(callback);
- $animate.removeClass(element, 'on').then(callback);
- $animate.removeClass(element, 'on').then(callback);
+ it('should perform the leave domOperation if a comment node is used',
+ inject(function($rootScope, $animate, $document) {
- $rootScope.$digest();
- $animate.triggerCallbackPromise();
+ var doc = $document[0];
- expect(spy).not.toHaveBeenCalled();
- expect(count).toBe(4);
- });
- });
+ element.html('hello there');
+ var commentNode = jqLite(doc.createComment('test comment'));
+ var parentNode = element[0];
+ parentNode.appendChild(commentNode[0]);
- it("should wait until a queue of animations are complete before performing a reflow",
- inject(function($rootScope, $compile, $timeout, $sniffer, $animate) {
+ $animate.leave(commentNode);
+ $rootScope.$digest();
+ expect(capturedAnimation).toBeFalsy();
+ expect(commentNode[0].parentNode).not.toBe(parentNode);
+ }));
- if (!$sniffer.transitions) return;
+ it('enter() should issue an enter animation and fire the DOM operation right away before the animation kicks off', inject(function($animate, $rootScope) {
+ expect(parent.children().length).toBe(0);
- $rootScope.items = [1,2,3,4,5];
- var element = html($compile('
')($rootScope));
+ options.foo = 'bar';
+ $animate.enter(element, parent, null, options);
- ss.addRule('.animated.ng-enter', '-webkit-transition: width 1s, background 1s 1s;' +
- 'transition: width 1s, background 1s 1s;');
+ expect(parent.children().length).toBe(1);
$rootScope.$digest();
- expect(element[0].querySelectorAll('.ng-enter-active').length).toBe(0);
- $animate.triggerReflow();
- expect(element[0].querySelectorAll('.ng-enter-active').length).toBe(5);
-
- forEach(element.children(), function(kid) {
- browserTrigger(kid, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- });
- expect(element[0].querySelectorAll('.ng-enter-active').length).toBe(0);
+ expect(capturedAnimation[0]).toBe(element);
+ expect(capturedAnimation[1]).toBe('enter');
+ expect(capturedAnimation[2].foo).toEqual(options.foo);
}));
+ it('move() should issue an enter animation and fire the DOM operation right away before the animation kicks off', inject(function($animate, $rootScope) {
+ parent.append(element);
- it("should work to disable all child animations for an element", function() {
- var childAnimated = false,
- containerAnimated = false;
- module(function($animateProvider) {
- $animateProvider.register('.child', function() {
- return {
- addClass: function(element, className, done) {
- childAnimated = true;
- done();
- }
- };
- });
- $animateProvider.register('.container', function() {
- return {
- leave: function(element, done) {
- containerAnimated = true;
- done();
- }
- };
- });
- });
-
- inject(function($compile, $rootScope, $animate, $timeout, $rootElement) {
- $animate.enabled(true);
+ expect(parent.children().length).toBe(1);
+ expect(parent2.children().length).toBe(0);
- var element = $compile('
')($rootScope);
- jqLite($document[0].body).append($rootElement);
- $rootElement.append(element);
+ options.foo = 'bar';
+ $animate.move(element, parent2, null, options);
- var child = $compile('
')($rootScope);
- element.append(child);
+ expect(parent.children().length).toBe(0);
+ expect(parent2.children().length).toBe(1);
- $animate.enabled(true, element);
+ $rootScope.$digest();
- $animate.addClass(child, 'awesome');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(childAnimated).toBe(true);
+ expect(capturedAnimation[0]).toBe(element);
+ expect(capturedAnimation[1]).toBe('move');
+ expect(capturedAnimation[2].foo).toEqual(options.foo);
+ }));
- childAnimated = false;
- $animate.enabled(false, element);
+ they('$prop() should insert the element adjacent to the after element if provided',
+ ['enter', 'move'], function(event) {
- $animate.addClass(child, 'super');
+ inject(function($animate, $rootScope) {
+ parent.append(element);
+ assertCompareNodes(parent2.next(), element, true);
+ $animate[event](element, null, parent2, options);
+ assertCompareNodes(parent2.next(), element);
$rootScope.$digest();
- $animate.triggerReflow();
- expect(childAnimated).toBe(false);
+ expect(capturedAnimation[1]).toBe(event);
+ });
+ });
- $animate.leave(element);
+ they('$prop() should append to the parent incase the after element is destroyed before the DOM operation is issued',
+ ['enter', 'move'], function(event) {
+ inject(function($animate, $rootScope) {
+ parent2.remove();
+ $animate[event](element, parent, parent2, options);
+ expect(parent2.next()).not.toEqual(element);
$rootScope.$digest();
- expect(containerAnimated).toBe(true);
+ expect(capturedAnimation[1]).toBe(event);
});
});
+ it('leave() should issue a leave animation with the correct DOM operation', inject(function($animate, $rootScope) {
+ parent.append(element);
+ options.foo = 'bar';
+ $animate.leave(element, options);
+ $rootScope.$digest();
- it("should disable all child animations on structural animations until the post animation " +
- "timeout has passed as well as all structural animations", function() {
- var intercepted, continueAnimation;
- module(function($animateProvider) {
- $animateProvider.register('.animated', function() {
- return {
- enter: ani('enter'),
- leave: ani('leave'),
- move: ani('move'),
- addClass: ani('addClass'),
- removeClass: ani('removeClass')
- };
-
- function ani(type) {
- return function(element, className, done) {
- intercepted = type;
- continueAnimation = function() {
- continueAnimation = angular.noop;
- (done || className)();
- };
- };
- }
- });
- });
+ expect(capturedAnimation[0]).toBe(element);
+ expect(capturedAnimation[1]).toBe('leave');
+ expect(capturedAnimation[2].foo).toEqual(options.foo);
- inject(function($animate, $rootScope, $sniffer, $timeout, $compile, _$rootElement_) {
- $rootElement = _$rootElement_;
+ expect(element.parent().length).toBe(1);
+ capturedAnimation[2].domOperation();
+ expect(element.parent().length).toBe(0);
+ }));
- $animate.enabled(true);
- $rootScope.$digest();
+ it('should remove all element and comment nodes during leave animation',
+ inject(function($compile, $rootScope, $$rAF, $$AnimateRunner) {
+
+ element = $compile(
+ '
' +
+ '
start
' +
+ '
end
' +
+ '
'
+ )($rootScope);
- var element = $compile('
...
')($rootScope);
- var child1 = $compile('
...
')($rootScope);
- var child2 = $compile('
...
')($rootScope);
- var container = $compile('
...
')($rootScope);
+ parent.append(element);
- var body = angular.element($document[0].body);
- body.append($rootElement);
- $rootElement.append(container);
- element.append(child1);
- element.append(child2);
+ $rootScope.items = [1,2,3,4,5];
+ $rootScope.$digest();
- $animate.enter(element, container);
- $rootScope.$digest();
+ // all the start/end repeat anchors + their adjacent comments
+ expect(element[0].childNodes.length).toBe(22);
- expect(intercepted).toBe('enter');
- continueAnimation();
+ var runner = new $$AnimateRunner();
+ overriddenAnimationRunner = runner;
- $animate.addClass(child1, 'test');
- $rootScope.$digest();
- $animate.triggerReflow();
- expect(child1.hasClass('test')).toBe(true);
+ $rootScope.items.length = 0;
+ $rootScope.$digest();
+ runner.end();
+ $$rAF.flush();
- expect(element.children().length).toBe(2);
+ // we're left with a text node and a comment node
+ expect(element[0].childNodes.length).toBeLessThan(3);
+ }));
- expect(intercepted).toBe('enter');
- $animate.leave(child1);
- $rootScope.$digest();
- expect(element.children().length).toBe(1);
+ it('addClass() should issue an addClass animation with the correct DOM operation', inject(function($animate, $rootScope) {
+ parent.append(element);
+ options.foo = 'bar';
+ $animate.addClass(element, 'red', options);
+ $rootScope.$digest();
- expect(intercepted).toBe('enter');
+ expect(capturedAnimation[0]).toBe(element);
+ expect(capturedAnimation[1]).toBe('addClass');
+ expect(capturedAnimation[2].foo).toEqual(options.foo);
- $animate.move(element, null, container);
- $rootScope.$digest();
+ expect(element).not.toHaveClass('red');
+ applyAnimationClasses(element, capturedAnimation[2]);
+ expect(element).toHaveClass('red');
+ }));
- expect(intercepted).toBe('move');
+ it('removeClass() should issue a removeClass animation with the correct DOM operation', inject(function($animate, $rootScope) {
+ parent.append(element);
+ element.addClass('blue');
- //flush the POST enter callback
- $animate.triggerCallbacks();
+ options.foo = 'bar';
+ $animate.removeClass(element, 'blue', options);
+ $rootScope.$digest();
- $animate.addClass(child2, 'testing');
- $rootScope.$digest();
- expect(intercepted).toBe('move');
+ expect(capturedAnimation[0]).toBe(element);
+ expect(capturedAnimation[1]).toBe('removeClass');
+ expect(capturedAnimation[2].foo).toEqual(options.foo);
- continueAnimation();
+ expect(element).toHaveClass('blue');
+ applyAnimationClasses(element, capturedAnimation[2]);
+ expect(element).not.toHaveClass('blue');
+ }));
- //flush the POST move callback
- $animate.triggerCallbacks();
+ it('setClass() should issue a setClass animation with the correct DOM operation', inject(function($animate, $rootScope) {
+ parent.append(element);
+ element.addClass('green');
- $animate.leave(child2);
- $rootScope.$digest();
- expect(intercepted).toBe('leave');
- });
- });
+ options.foo = 'bar';
+ $animate.setClass(element, 'yellow', 'green', options);
+ $rootScope.$digest();
+ expect(capturedAnimation[0]).toBe(element);
+ expect(capturedAnimation[1]).toBe('setClass');
+ expect(capturedAnimation[2].foo).toEqual(options.foo);
- it("should not disable any child animations when any parent class-based animations are run", function() {
- var intercepted;
- module(function($animateProvider) {
- $animateProvider.register('.animated', function() {
- return {
- enter: function(element, done) {
- intercepted = true;
- done();
- }
- };
- });
- });
+ expect(element).not.toHaveClass('yellow');
+ expect(element).toHaveClass('green');
+ applyAnimationClasses(element, capturedAnimation[2]);
+ expect(element).toHaveClass('yellow');
+ expect(element).not.toHaveClass('green');
+ }));
- inject(function($animate, $rootScope, $sniffer, $timeout, $compile, $document, $rootElement) {
+ they('should apply the $prop CSS class to the element before digest for the given event and remove when complete',
+ {'ng-enter': 'enter', 'ng-leave': 'leave', 'ng-move': 'move'}, function(event) {
+
+ inject(function($animate, $rootScope, $document, $rootElement) {
$animate.enabled(true);
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
+ var element = jqLite('
');
+ var parent = jqLite('
');
+
+ $rootElement.append(parent);
jqLite($document[0].body).append($rootElement);
- $rootScope.bool = true;
- $rootScope.$digest();
+ var runner;
+ if (event === 'leave') {
+ parent.append(element);
+ runner = $animate[event](element);
+ } else {
+ runner = $animate[event](element, parent);
+ }
- expect(intercepted).toBe(true);
- });
- });
+ var expectedClassName = 'ng-' + event;
+ expect(element).toHaveClass(expectedClassName);
- it("should cache the response from getComputedStyle if each successive element has the same className value and parent until the first reflow hits", function() {
- var count = 0;
- module(function($provide) {
- $provide.value('$window', {
- document: jqLite(window.document),
- getComputedStyle: function(element) {
- count++;
- return window.getComputedStyle(element);
- }
- });
+ $rootScope.$digest();
+ expect(element).toHaveClass(expectedClassName);
+
+ runner.end();
+ expect(element).not.toHaveClass(expectedClassName);
+
+ dealoc(parent);
});
+ });
- inject(function($animate, $rootScope, $compile, $rootElement, $timeout, $document, $sniffer) {
- if (!$sniffer.transitions) return;
+ they('should add CSS classes with the $prop suffix when depending on the event and remove when complete',
+ {'-add': 'add', '-remove': 'remove'}, function(event) {
+ inject(function($animate, $rootScope, $document, $rootElement) {
$animate.enabled(true);
- var kid, element = $compile('
')($rootScope);
+ var element = jqLite('
');
+
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
- for (var i = 0; i < 20; i++) {
- kid = $compile('
')($rootScope);
- $animate.enter(kid, element);
+ var classes = 'one two';
+ var expectedClasses = ['one-',event,' ','two-', event].join('');
+
+ var runner;
+ if (event === 'add') {
+ runner = $animate.addClass(element, classes);
+ } else {
+ element.addClass(classes);
+ runner = $animate.removeClass(element, classes);
}
+
+ expect(element).toHaveClass(expectedClasses);
+
$rootScope.$digest();
+ expect(element).toHaveClass(expectedClasses);
- //called three times since the classname is the same
- expect(count).toBe(2);
+ runner.end();
+ expect(element).not.toHaveClass(expectedClasses);
dealoc(element);
- count = 0;
+ });
+ });
+
+ they('$prop() should operate using a native DOM element',
+ ['enter', 'move', 'leave', 'addClass', 'removeClass', 'setClass', 'animate'], function(event) {
- for (i = 0; i < 20; i++) {
- kid = $compile('
')($rootScope);
- $animate.enter(kid, element);
+ inject(function($animate, $rootScope, $document) {
+ var element = $document[0].createElement('div');
+ element.setAttribute('id', 'crazy-man');
+ if (event !== 'enter' && event !== 'move') {
+ parent.append(element);
}
- $rootScope.$digest();
+ switch (event) {
+ case 'enter':
+ case 'move':
+ $animate[event](element, parent, parent2, options);
+ break;
+
+ case 'addClass':
+ $animate.addClass(element, 'klass', options);
+ break;
+
+ case 'removeClass':
+ element.className = 'klass';
+ $animate.removeClass(element, 'klass', options);
+ break;
+
+ case 'setClass':
+ element.className = 'two';
+ $animate.setClass(element, 'one', 'two', options);
+ break;
+
+ case 'leave':
+ $animate.leave(element, options);
+ break;
+
+ case 'animate':
+ var toStyles = { color: 'red' };
+ $animate.animate(element, {}, toStyles, 'klass', options);
+ break;
+ }
- expect(count).toBe(20);
+ $rootScope.$digest();
+ expect(capturedAnimation[0].attr('id')).toEqual(element.getAttribute('id'));
});
});
- it("should cache getComputedStyle with similar className values but with respect to the parent node",
- inject(function($compile, $rootScope, $animate, $sniffer) {
+ describe('addClass / removeClass', function() {
+ it('should not perform an animation if there are no valid CSS classes to add',
+ inject(function($animate, $rootScope) {
- if (!$sniffer.transitions) return;
+ parent.append(element);
- $animate.enabled();
+ $animate.removeClass(element, 'something-to-remove');
+ $rootScope.$digest();
+ expect(capturedAnimation).toBeFalsy();
- var html = '
first
' +
- '
';
+ element.addClass('something-to-add');
- ss.addRule('.second .on', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
+ $animate.addClass(element, 'something-to-add');
+ $rootScope.$digest();
+ expect(capturedAnimation).toBeFalsy();
+ }));
+ });
- var element = $compile(html)($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ describe('animate()', function() {
+ they('should not perform an animation if $prop is provided as a `to` style',
+ { '{}': {},
+ 'null': null,
+ 'false': false,
+ '""': "",
+ '[]': [] }, function(toStyle) {
- $rootScope.$apply(function() {
- $rootScope.one = true;
- $rootScope.two = true;
+ inject(function($animate, $rootScope) {
+ parent.append(element);
+ $animate.animate(element, null, toStyle);
+ $rootScope.$digest();
+ expect(capturedAnimation).toBeFalsy();
+ });
});
- $animate.triggerReflow();
-
- var inner = jqLite(jqLite(element[1]).find('div'));
+ it('should not perform an animation if only from styles are provided',
+ inject(function($animate, $rootScope) {
- expect(inner.hasClass('on-add')).toBe(true);
- expect(inner.hasClass('on-add-active')).toBe(true);
-
- browserTrigger(inner, 'animationend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
+ var fromStyle = { color: 'pink' };
+ parent.append(element);
+ $animate.animate(element, fromStyle);
+ $rootScope.$digest();
+ expect(capturedAnimation).toBeFalsy();
+ }));
- expect(inner.hasClass('on-add')).toBe(false);
- expect(inner.hasClass('on-add-active')).toBe(false);
- }));
+ it('should perform an animation if only from styles are provided as well as any valid classes',
+ inject(function($animate, $rootScope) {
- it("should reset the getComputedStyle lookup cache even when no animation is found",
- inject(function($compile, $rootScope, $animate, $sniffer, $document) {
+ parent.append(element);
- if (!$sniffer.transitions) return;
+ var fromStyle = { color: 'red' };
+ var options = { removeClass: 'goop' };
+ $animate.animate(element, fromStyle, null, null, options);
+ $rootScope.$digest();
+ expect(capturedAnimation).toBeFalsy();
- $animate.enabled();
+ fromStyle = { color: 'blue' };
+ options = { addClass: 'goop' };
+ $animate.animate(element, fromStyle, null, null, options);
+ $rootScope.$digest();
+ expect(capturedAnimation).toBeTruthy();
+ }));
+ });
- var html = '
';
+ describe('parent animations', function() {
+ they('should not cancel a pre-digest parent class-based animation if a child $prop animation is set to run',
+ ['structural', 'class-based'], function(animationType) {
- ss.addRule('.activated .toggle', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
+ inject(function($rootScope, $animate, $$rAF) {
+ parent.append(element);
+ var child = jqLite('
');
- var child, element = $compile(html)($rootScope);
+ if (animationType === 'structural') {
+ $animate.enter(child, element);
+ } else {
+ element.append(child);
+ $animate.addClass(child, 'test');
+ }
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ $animate.addClass(parent, 'abc');
+ expect(capturedAnimationHistory.length).toBe(0);
+ $rootScope.$digest();
+ expect(capturedAnimationHistory.length).toBe(2);
+ });
+ });
- $rootScope.onOff = true;
- $rootScope.$digest();
+ they('should not cancel a post-digest parent class-based animation if a child $prop animation is set to run',
+ ['structural', 'class-based'], function(animationType) {
- child = element.find('div');
- expect(child).not.toHaveClass('ng-enter');
- expect(child.parent()[0]).toEqual(element[0]);
- $animate.triggerReflow();
+ inject(function($rootScope, $animate, $$rAF) {
+ parent.append(element);
+ var child = jqLite('
');
- $rootScope.onOff = false;
- $rootScope.$digest();
+ $animate.addClass(parent, 'abc');
+ $rootScope.$digest();
- child = element.find('div');
- expect(child.parent().length).toBe(0);
- $animate.triggerReflow();
+ if (animationType === 'structural') {
+ $animate.enter(child, element);
+ } else {
+ element.append(child);
+ $animate.addClass(child, 'test');
+ }
- element.addClass('activated');
- $rootScope.$digest();
- $animate.triggerReflow();
+ expect(capturedAnimationHistory.length).toBe(1);
- $rootScope.onOff = true;
- $rootScope.$digest();
+ $rootScope.$digest();
- child = element.find('div');
- expect(child).toHaveClass('ng-enter');
- $animate.triggerReflow();
- expect(child).toHaveClass('ng-enter-active');
+ expect(capturedAnimationHistory.length).toBe(2);
+ });
+ });
- browserTrigger(child, 'transitionend',
- { timeStamp: Date.now() + 1000, elapsedTime: 2000 });
+ they('should not cancel a post-digest $prop child animation if a class-based parent animation is set to run',
+ ['structural', 'class-based'], function(animationType) {
- $animate.triggerCallbacks();
+ inject(function($rootScope, $animate, $$rAF) {
+ parent.append(element);
- $rootScope.onOff = false;
- $rootScope.$digest();
+ var child = jqLite('
');
+ if (animationType === 'structural') {
+ $animate.enter(child, element);
+ } else {
+ element.append(child);
+ $animate.addClass(child, 'test');
+ }
- expect(child).toHaveClass('ng-leave');
- $animate.triggerReflow();
- expect(child).toHaveClass('ng-leave-active');
- }));
+ $rootScope.$digest();
- it("should cancel and perform the dom operation only after the reflow has run",
- inject(function($compile, $rootScope, $animate, $sniffer) {
+ $animate.addClass(parent, 'abc');
- if (!$sniffer.transitions) return;
+ expect(capturedAnimationHistory.length).toBe(1);
+ $rootScope.$digest();
- ss.addRule('.green-add', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
+ expect(capturedAnimationHistory.length).toBe(2);
+ });
+ });
+ });
- ss.addRule('.red-add', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
+ it("should NOT clobber all data on an element when animation is finished",
+ inject(function($animate, $rootScope) {
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ element.data('foo', 'bar');
- $animate.addClass(element, 'green');
+ $animate.removeClass(element, 'ng-hide');
$rootScope.$digest();
- expect(element.hasClass('green-add')).toBe(true);
-
- $animate.addClass(element, 'red');
+ $animate.addClass(element, 'ng-hide');
$rootScope.$digest();
- expect(element.hasClass('red-add')).toBe(true);
-
- expect(element.hasClass('green')).toBe(false);
- expect(element.hasClass('red')).toBe(false);
- $animate.triggerReflow();
-
- expect(element.hasClass('green')).toBe(true);
- expect(element.hasClass('red')).toBe(true);
+ expect(element.data('foo')).toEqual('bar');
}));
- it("should properly add and remove CSS classes when multiple classes are applied",
- inject(function($compile, $rootScope, $animate) {
-
- $animate.enabled();
+ describe('child animations', function() {
+ it('should skip animations if the element is not attached to the $rootElement',
+ inject(function($compile, $rootScope, $animate) {
- var exp = "{{ className ? 'before ' + className + ' after' : '' }}";
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
-
- function assertClasses(str) {
- var className = element.attr('class');
- if (str.length === 0) {
- expect(className.length).toBe(0);
- } else {
- expect(className.split(/\s+/)).toEqual(str.split(' '));
- }
- }
+ $animate.enabled(true);
- $rootScope.className = '';
- $rootScope.$digest();
- $animate.triggerReflow();
+ var elm1 = $compile('
')($rootScope);
- assertClasses('');
+ expect(capturedAnimation).toBeFalsy();
+ $animate.addClass(elm1, 'klass2');
+ expect(capturedAnimation).toBeFalsy();
+ $rootScope.$digest();
+ expect(capturedAnimation).toBeFalsy();
+ }));
- $rootScope.className = 'one';
- $rootScope.$digest();
- $animate.triggerReflow();
+ it('should skip animations if the element is attached to the $rootElement, but not apart of the body',
+ inject(function($compile, $rootScope, $animate, $rootElement) {
- assertClasses('before one after');
+ $animate.enabled(true);
- $rootScope.className = 'two';
- $rootScope.$digest();
- $animate.triggerReflow();
+ var elm1 = $compile('
')($rootScope);
- assertClasses('before after two');
+ var newParent = $compile('
')($rootScope);
+ newParent.append($rootElement);
+ $rootElement.append(elm1);
- $rootScope.className = '';
- $rootScope.$digest();
- //intentionally avoiding the triggerReflow operation
+ expect(capturedAnimation).toBeFalsy();
+ $animate.addClass(elm1, 'klass2');
+ expect(capturedAnimation).toBeFalsy();
+ $rootScope.$digest();
+ expect(capturedAnimation).toBeFalsy();
+ }));
- assertClasses('');
- }));
+ it('should skip the animation if the element is removed from the DOM before the post digest kicks in',
+ inject(function($animate, $rootScope) {
- it("should avoid mixing up substring classes during add and remove operations", function() {
- var currentAnimation, currentFn;
- module(function($animateProvider) {
- $animateProvider.register('.on', function() {
- return {
- beforeAddClass: function(element, className, done) {
- currentAnimation = 'addClass';
- currentFn = done;
- return function(cancelled) {
- currentAnimation = cancelled ? null : currentAnimation;
- };
- },
- beforeRemoveClass: function(element, className, done) {
- currentAnimation = 'removeClass';
- currentFn = done;
- return function(cancelled) {
- currentAnimation = cancelled ? null : currentAnimation;
- };
- }
- };
- });
- });
- inject(function($compile, $rootScope, $animate) {
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ $animate.enter(element, parent);
+ expect(capturedAnimation).toBeFalsy();
- $animate.addClass(element, 'on');
+ element.remove();
$rootScope.$digest();
- expect(currentAnimation).toBe('addClass');
- currentFn();
+ expect(capturedAnimation).toBeFalsy();
+ }));
- currentAnimation = null;
+ it('should be blocked when there is an ongoing structural parent animation occurring',
+ inject(function($rootScope, $rootElement, $animate) {
- $animate.removeClass(element, 'on');
- $rootScope.$digest();
+ parent.append(element);
- $animate.addClass(element, 'on');
+ expect(capturedAnimation).toBeFalsy();
+ $animate.move(parent, parent2);
$rootScope.$digest();
- expect(currentAnimation).toBe('addClass');
- });
- });
-
- it('should enable and disable animations properly on the root element', function() {
- var count = 0;
- module(function($animateProvider) {
- $animateProvider.register('.animated', function() {
- return {
- addClass: function(element, className, done) {
- count++;
- done();
- }
- };
- });
- });
- inject(function($compile, $rootScope, $animate, $sniffer, $rootElement) {
+ // yes the animation is going on
+ expect(capturedAnimation[0]).toBe(parent);
+ capturedAnimation = null;
- $rootElement.addClass('animated');
- $animate.addClass($rootElement, 'green');
+ $animate.addClass(element, 'blue');
$rootScope.$digest();
- $animate.triggerReflow();
+ expect(capturedAnimation).toBeFalsy();
+ }));
- expect(count).toBe(1);
+ it("should disable all child animations for atleast one RAF when a structural animation is issued",
+ inject(function($animate, $rootScope, $compile, $$body, $rootElement, $$rAF, $$AnimateRunner) {
- $animate.addClass($rootElement, 'red');
- $rootScope.$digest();
- $animate.triggerReflow();
+ element = $compile(
+ '
' +
+ '
' +
+ ' {{ item }}' +
+ '
' +
+ '
'
+ )($rootScope);
- expect(count).toBe(2);
- });
- });
+ $$body.append($rootElement);
+ $rootElement.append(element);
+ var runner = new $$AnimateRunner();
+ overriddenAnimationRunner = runner;
- it('should perform pre and post animations', function() {
- var steps = [];
- module(function($animateProvider) {
- $animateProvider.register('.class-animate', function() {
- return {
- beforeAddClass: function(element, className, done) {
- steps.push('before');
- done();
- },
- addClass: function(element, className, done) {
- steps.push('after');
- done();
- }
- };
- });
- });
- inject(function($animate, $rootScope, $compile, $rootElement) {
- $animate.enabled(true);
+ $rootScope.items = [1];
+ $rootScope.$digest();
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
+ expect(capturedAnimation[0]).toHaveClass('if-animation');
+ expect(capturedAnimationHistory.length).toBe(1);
+ expect(element[0].querySelectorAll('.repeat-animation').length).toBe(1);
- $animate.addClass(element, 'red');
+ $rootScope.items = [1, 2];
$rootScope.$digest();
- $animate.triggerReflow();
+ expect(capturedAnimation[0]).toHaveClass('if-animation');
+ expect(capturedAnimationHistory.length).toBe(1);
+ expect(element[0].querySelectorAll('.repeat-animation').length).toBe(2);
- expect(steps).toEqual(['before','after']);
- });
- });
+ runner.end();
+ $$rAF.flush();
+
+ $rootScope.items = [1, 2, 3];
+ $rootScope.$digest();
+ expect(capturedAnimation[0]).toHaveClass('repeat-animation');
+ expect(capturedAnimationHistory.length).toBe(2);
+ expect(element[0].querySelectorAll('.repeat-animation').length).toBe(3);
+ }));
- it('should treat the leave event always as a before event and discard the beforeLeave function', function() {
- var parentID, steps = [];
- module(function($animateProvider) {
- $animateProvider.register('.animate', function() {
- return {
- beforeLeave: function(element, done) {
- steps.push('before');
- done();
- },
- leave: function(element, done) {
- parentID = element.parent().attr('id');
- steps.push('after');
- done();
- }
- };
- });
- });
- inject(function($animate, $rootScope, $compile, $rootElement) {
- $animate.enabled(true);
+ it('should not be blocked when there is an ongoing class-based parent animation occurring',
+ inject(function($rootScope, $rootElement, $animate) {
- var element = $compile('
')($rootScope);
- var child = $compile('
')($rootScope);
- $rootElement.append(element);
- element.append(child);
+ parent.append(element);
- $animate.leave(child);
+ expect(capturedAnimation).toBeFalsy();
+ $animate.addClass(parent, 'rogers');
$rootScope.$digest();
- expect(steps).toEqual(['after']);
- expect(parentID).toEqual('parentGuy');
- });
- });
+ // yes the animation is going on
+ expect(capturedAnimation[0]).toBe(parent);
+ capturedAnimation = null;
+ $animate.addClass(element, 'blue');
+ $rootScope.$digest();
+ expect(capturedAnimation[0]).toBe(element);
+ }));
- it('should only perform the DOM operation once',
- inject(function($sniffer, $compile, $rootScope, $rootElement, $animate) {
+ it('should skip all pre-digest queued child animations when a parent structural animation is triggered',
+ inject(function($rootScope, $rootElement, $animate) {
- if (!$sniffer.transitions) return;
+ parent.append(element);
- ss.addRule('.base-class', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
+ $animate.addClass(element, 'rumlow');
+ $animate.move(parent, null, parent2);
- $animate.enabled(true);
+ expect(capturedAnimation).toBeFalsy();
+ expect(capturedAnimationHistory.length).toBe(0);
+ $rootScope.$digest();
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ expect(capturedAnimation[0]).toBe(parent);
+ expect(capturedAnimationHistory.length).toBe(1);
+ }));
- $animate.removeClass(element, 'base-class one two');
- $rootScope.$digest();
+ it('should end all ongoing post-digest child animations when a parent structural animation is triggered',
+ inject(function($rootScope, $rootElement, $animate) {
- //still true since we're before the reflow
- expect(element.hasClass('base-class')).toBe(true);
+ parent.append(element);
- //this will cancel the remove animation
- $animate.addClass(element, 'base-class one two');
- $rootScope.$digest();
+ $animate.addClass(element, 'rumlow');
+ var isCancelled = false;
+ overriddenAnimationRunner = extend(defaultFakeAnimationRunner, {
+ end: function() {
+ isCancelled = true;
+ }
+ });
- //the cancellation was a success and the class was removed right away
- expect(element.hasClass('base-class')).toBe(false);
+ $rootScope.$digest();
+ expect(capturedAnimation[0]).toBe(element);
+ expect(isCancelled).toBe(false);
- //the reflow...
- $animate.triggerReflow();
+ // restore the default
+ overriddenAnimationRunner = defaultFakeAnimationRunner;
+ $animate.move(parent, null, parent2);
+ $rootScope.$digest();
+ expect(capturedAnimation[0]).toBe(parent);
- //the reflow DOM operation was commenced...
- expect(element.hasClass('base-class')).toBe(true);
- }));
+ expect(isCancelled).toBe(true);
+ }));
+ it('should not end any child animations if a parent class-based animation is issued',
+ inject(function($rootScope, $rootElement, $animate) {
- it('should block and unblock transitions before the dom operation occurs',
- inject(function($rootScope, $compile, $rootElement, $document, $animate, $sniffer) {
+ parent.append(element);
- if (!$sniffer.transitions) return;
+ var element2 = jqLite('
element2
');
+ $animate.enter(element2, parent);
- $animate.enabled(true);
+ var isCancelled = false;
+ overriddenAnimationRunner = extend(defaultFakeAnimationRunner, {
+ end: function() {
+ isCancelled = true;
+ }
+ });
- ss.addRule('.cross-animation', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
+ $rootScope.$digest();
+ expect(capturedAnimation[0]).toBe(element2);
+ expect(isCancelled).toBe(false);
- var capturedProperty = 'none';
+ // restore the default
+ overriddenAnimationRunner = defaultFakeAnimationRunner;
+ $animate.addClass(parent, 'peter');
+ $rootScope.$digest();
+ expect(capturedAnimation[0]).toBe(parent);
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ expect(isCancelled).toBe(false);
+ }));
- var node = element[0];
- node._setAttribute = node.setAttribute;
- node.setAttribute = function(prop, val) {
- if (prop == 'class' && val.indexOf('trigger-class') >= 0) {
- var propertyKey = ($sniffer.vendorPrefix == 'Webkit' ? '-webkit-' : '') + 'transition-property';
- capturedProperty = element.css(propertyKey);
- }
- node._setAttribute(prop, val);
- };
+ it('should allow follow-up class-based animations to run in parallel on the same element',
+ inject(function($rootScope, $animate, $$rAF) {
- expect(capturedProperty).toBe('none');
- $animate.addClass(element, 'trigger-class');
- $rootScope.$digest();
+ parent.append(element);
- $animate.triggerReflow();
+ var runner1done = false;
+ var runner1 = $animate.addClass(element, 'red');
+ runner1.done(function() {
+ runner1done = true;
+ });
- expect(capturedProperty).not.toBe('none');
- }));
+ $rootScope.$digest();
+ $$rAF.flush();
+ expect(capturedAnimation).toBeTruthy();
+ expect(runner1done).toBeFalsy();
+ capturedAnimation = null;
- it('should not block keyframe animations around the reflow operation',
- inject(function($rootScope, $compile, $rootElement, $document, $animate, $sniffer) {
+ // make sure it's a different runner
+ overriddenAnimationRunner = extend(defaultFakeAnimationRunner, {
+ end: function() {
+ // this code will still end the animation, just not at any deeper level
+ }
+ });
- if (!$sniffer.animations) return;
+ var runner2done = false;
+ var runner2 = $animate.addClass(element, 'blue');
+ runner2.done(function() {
+ runner2done = true;
+ });
- $animate.enabled(true);
+ $rootScope.$digest();
+ $$rAF.flush();
+ expect(capturedAnimation).toBeTruthy();
+ expect(runner2done).toBeFalsy();
- ss.addRule('.cross-animation', '-webkit-animation:1s my_animation;' +
- 'animation:1s my_animation;');
+ expect(runner1done).toBeFalsy();
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ runner2.end();
- var node = element[0];
- var animationKey = $sniffer.vendorPrefix == 'Webkit' ? 'WebkitAnimation' : 'animation';
+ expect(runner2done).toBeTruthy();
+ expect(runner1done).toBeFalsy();
+ }));
- $animate.addClass(element, 'trigger-class');
- $rootScope.$digest();
+ it('should remove the animation block on child animations once the parent animation is complete',
+ inject(function($rootScope, $rootElement, $animate, $$AnimateRunner, $$rAF) {
- expect(node.style[animationKey]).not.toContain('none');
+ var runner = new $$AnimateRunner();
+ overriddenAnimationRunner = runner;
+ parent.append(element);
- $animate.triggerReflow();
+ $animate.enter(parent, null, parent2);
+ $rootScope.$digest();
+ expect(capturedAnimationHistory.length).toBe(1);
- expect(node.style[animationKey]).not.toContain('none');
+ $animate.addClass(element, 'tony');
+ $rootScope.$digest();
+ expect(capturedAnimationHistory.length).toBe(1);
- browserTrigger(element, 'animationend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
+ runner.end();
+ $$rAF.flush();
- expect(node.style[animationKey]).not.toContain('none');
- }));
+ $animate.addClass(element, 'stark');
+ $rootScope.$digest();
+ expect(capturedAnimationHistory.length).toBe(2);
+ }));
+ });
+ describe('cancellations', function() {
+ it('should cancel the previous animation if a follow-up structural animation takes over',
+ inject(function($animate, $rootScope) {
- it('should not block keyframe animations at anytime before a followup JS animation occurs', function() {
- module(function($animateProvider) {
- $animateProvider.register('.special', function($sniffer, $window) {
- var prop = $sniffer.vendorPrefix == 'Webkit' ? 'WebkitAnimation' : 'animation';
- return {
- beforeAddClass: function(element, className, done) {
- expect(element[0].style[prop]).not.toContain('none');
- expect($window.getComputedStyle(element[0])[prop + 'Duration']).toBe('1s');
- done();
- },
- addClass: function(element, className, done) {
- expect(element[0].style[prop]).not.toContain('none');
- expect($window.getComputedStyle(element[0])[prop + 'Duration']).toBe('1s');
- done();
- }
- };
+ var enterComplete = false;
+ overriddenAnimationRunner = extend(defaultFakeAnimationRunner, {
+ end: function() {
+ enterComplete = true;
+ }
});
- });
- inject(function($rootScope, $compile, $rootElement, $document, $animate, $sniffer, $timeout, $window) {
- if (!$sniffer.animations) return;
-
- $animate.enabled(true);
- ss.addRule('.special', '-webkit-animation:1s special_animation;' +
- 'animation:1s special_animation;');
+ parent.append(element);
+ $animate.move(element, parent2);
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ $rootScope.$digest();
+ expect(enterComplete).toBe(false);
- $animate.addClass(element, 'some-klass');
+ $animate.leave(element);
$rootScope.$digest();
+ expect(enterComplete).toBe(true);
+ }));
- var prop = $sniffer.vendorPrefix == 'Webkit' ? 'WebkitAnimation' : 'animation';
+ it('should cancel the previous structural animation if a follow-up structural animation takes over before the postDigest',
+ inject(function($animate, $$rAF) {
- expect(element[0].style[prop]).not.toContain('none');
- expect($window.getComputedStyle(element[0])[prop + 'Duration']).toBe('1s');
+ var enterDone = jasmine.createSpy('enter animation done');
+ $animate.enter(element, parent).done(enterDone);
+ expect(enterDone).not.toHaveBeenCalled();
- $animate.triggerReflow();
- });
- });
+ $animate.leave(element);
+ $$rAF.flush();
+ expect(enterDone).toHaveBeenCalled();
+ }));
+ it('should cancel the previously running addClass animation if a follow-up removeClass animation is using the same class value',
+ inject(function($animate, $rootScope, $$rAF) {
- it('should round up long elapsedTime values to close off a CSS3 animation',
- inject(function($rootScope, $compile, $rootElement, $document, $animate, $sniffer) {
- if (!$sniffer.animations) return;
+ parent.append(element);
+ var runner = $animate.addClass(element, 'active-class');
+ $rootScope.$digest();
- ss.addRule('.millisecond-transition.ng-leave', '-webkit-transition:510ms linear all;' +
- 'transition:510ms linear all;');
+ var doneHandler = jasmine.createSpy('addClass done');
+ runner.done(doneHandler);
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ $$rAF.flush();
- $animate.leave(element);
+ expect(doneHandler).not.toHaveBeenCalled();
+
+ $animate.removeClass(element, 'active-class');
$rootScope.$digest();
- $animate.triggerReflow();
+ expect(doneHandler).toHaveBeenCalled();
+ }));
+
+ it('should cancel the previously running removeClass animation if a follow-up addClass animation is using the same class value',
+ inject(function($animate, $rootScope, $$rAF) {
- browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 0.50999999991 });
+ element.addClass('active-class');
+ parent.append(element);
+ var runner = $animate.removeClass(element, 'active-class');
+ $rootScope.$digest();
- expect($rootElement.children().length).toBe(0);
- })
- );
+ var doneHandler = jasmine.createSpy('addClass done');
+ runner.done(doneHandler);
+ $$rAF.flush();
- it('should properly animate elements with compound directives', function() {
- var capturedAnimation;
- module(function($animateProvider) {
- $animateProvider.register('.special', function() {
- return {
- enter: function(element, done) {
- capturedAnimation = 'enter';
- done();
- },
- leave: function(element, done) {
- capturedAnimation = 'leave';
- done();
- }
- };
- });
- });
- inject(function($rootScope, $compile, $rootElement, $document, $timeout, $templateCache, $sniffer, $animate) {
- if (!$sniffer.transitions) return;
+ expect(doneHandler).not.toHaveBeenCalled();
- $templateCache.put('item-template', 'item: #{{ item }} ');
- var element = $compile('
')($rootScope);
+ $animate.addClass(element, 'active-class');
+ $rootScope.$digest();
- ss.addRule('.special', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
+ expect(doneHandler).toHaveBeenCalled();
+ }));
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ it('should immediately skip the class-based animation if there is an active structural animation',
+ inject(function($animate, $rootScope) {
- $rootScope.tpl = 'item-template';
- $rootScope.items = [1,2,3];
+ $animate.enter(element, parent);
$rootScope.$digest();
- $animate.triggerReflow();
+ expect(capturedAnimation).toBeTruthy();
+
+ capturedAnimation = null;
+ $animate.addClass(element, 'red');
+ expect(element).toHaveClass('red');
+ }));
+
+ it('should join the class-based animation into the structural animation if the structural animation is pre-digest',
+ inject(function($animate, $rootScope) {
- expect(capturedAnimation).toBe('enter');
- expect(element.text()).toContain('item: #1');
+ $animate.enter(element, parent);
+ expect(capturedAnimation).toBeFalsy();
- forEach(element.children(), function(kid) {
- browserTrigger(kid, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
- });
- $animate.triggerCallbackPromise();
+ $animate.addClass(element, 'red');
+ expect(element).not.toHaveClass('red');
- $rootScope.items = [];
+ expect(capturedAnimation).toBeFalsy();
$rootScope.$digest();
- $animate.triggerReflow();
- expect(capturedAnimation).toBe('leave');
- });
- });
+ expect(capturedAnimation[1]).toBe('enter');
+ expect(capturedAnimation[2].addClass).toBe('red');
+ }));
- it('should animate only the specified CSS className', function() {
- var captures = {};
- module(function($animateProvider) {
- $animateProvider.classNameFilter(/prefixed-animation/);
- $animateProvider.register('.capture', function() {
- return {
- enter: buildFn('enter'),
- leave: buildFn('leave')
- };
-
- function buildFn(key) {
- return function(element, className, done) {
- captures[key] = true;
- (done || className)();
- };
- }
- });
- });
- inject(function($rootScope, $compile, $rootElement, $document, $timeout, $templateCache, $sniffer, $animate) {
- if (!$sniffer.transitions) return;
+ it('should issue a new runner instance if a previous structural animation was cancelled',
+ inject(function($animate, $rootScope) {
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ parent.append(element);
- var enterDone = false;
- $animate.enter(element, $rootElement).then(function() {
- enterDone = true;
- });
+ var runner1 = $animate.move(element, parent2);
+ $rootScope.$digest();
+ var runner2 = $animate.leave(element);
$rootScope.$digest();
- $animate.triggerCallbackPromise();
- expect(captures['enter']).toBeUndefined();
- expect(enterDone).toBe(true);
+ expect(runner1).not.toBe(runner2);
+ }));
- element.addClass('prefixed-animation');
+ it('should properly cancel out animations when the same class is added/removed within the same digest',
+ inject(function($animate, $rootScope) {
- var leaveDone = false;
- $animate.leave(element).then(function() {
- leaveDone = true;
- });
+ parent.append(element);
+ $animate.addClass(element, 'red');
+ $animate.removeClass(element, 'red');
+ $rootScope.$digest();
+
+ expect(capturedAnimation).toBeFalsy();
+ $animate.addClass(element, 'blue');
$rootScope.$digest();
- $animate.triggerCallbackPromise();
- expect(captures['leave']).toBe(true);
- expect(leaveDone).toBe(true);
- });
+ expect(capturedAnimation[2].addClass).toBe('blue');
+ }));
});
- it('should animate only the specified CSS className inside ng-if', function() {
- var captures = {};
- module(function($animateProvider) {
- $animateProvider.classNameFilter(/prefixed-animation/);
- $animateProvider.register('.capture', function() {
- return {
- enter: buildFn('enter'),
- leave: buildFn('leave')
- };
-
- function buildFn(key) {
- return function(element, className, done) {
- captures[key] = true;
- (done || className)();
- };
- }
- });
- });
- inject(function($rootScope, $compile, $rootElement, $document, $sniffer, $animate) {
- if (!$sniffer.transitions) return;
+ describe('should merge', function() {
+ it('multiple class-based animations together into one before the digest passes', inject(function($animate, $rootScope) {
+ parent.append(element);
+ element.addClass('green');
- var upperElement = $compile('
')($rootScope);
- $rootElement.append(upperElement);
- jqLite($document[0].body).append($rootElement);
+ $animate.addClass(element, 'red');
+ $animate.addClass(element, 'blue');
+ $animate.removeClass(element, 'green');
$rootScope.$digest();
- $animate.triggerCallbacks();
- var element = upperElement.find('span');
+ expect(capturedAnimation[0]).toBe(element);
+ expect(capturedAnimation[1]).toBe('setClass');
- var leaveDone = false;
- $animate.leave(element).then(function() {
- leaveDone = true;
- });
+ options = capturedAnimation[2];
+ expect(options.addClass).toEqual('red blue');
+ expect(options.removeClass).toEqual('green');
- $rootScope.$digest();
- $animate.triggerCallbacks();
+ expect(element).not.toHaveClass('red');
+ expect(element).not.toHaveClass('blue');
+ expect(element).toHaveClass('green');
- expect(captures.leave).toBe(true);
- expect(leaveDone).toBe(true);
- });
- });
+ applyAnimationClasses(element, capturedAnimation[2]);
+
+ expect(element).toHaveClass('red');
+ expect(element).toHaveClass('blue');
+ expect(element).not.toHaveClass('green');
+ }));
- it('should respect the most relevant CSS transition property if defined in multiple classes',
- inject(function($sniffer, $compile, $rootScope, $rootElement, $animate) {
+ it('multiple class-based animations together into a single structural event before the digest passes', inject(function($animate, $rootScope) {
+ element.addClass('green');
- if (!$sniffer.transitions) return;
+ expect(element.parent().length).toBe(0);
+ $animate.enter(element, parent);
+ expect(element.parent().length).toBe(1);
- ss.addRule('.base-class', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
+ $animate.addClass(element, 'red');
+ $animate.removeClass(element, 'green');
- ss.addRule('.base-class.on', '-webkit-transition:5s linear all;' +
- 'transition:5s linear all;');
+ $rootScope.$digest();
- $animate.enabled(true);
+ expect(capturedAnimation[0]).toBe(element);
+ expect(capturedAnimation[1]).toBe('enter');
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ options = capturedAnimation[2];
+ expect(options.addClass).toEqual('red');
+ expect(options.removeClass).toEqual('green');
- var ready = false;
- $animate.addClass(element, 'on').then(function() {
- ready = true;
- });
- $rootScope.$digest();
+ expect(element).not.toHaveClass('red');
+ expect(element).toHaveClass('green');
- $animate.triggerReflow();
- browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 1 });
- expect(ready).toBe(false);
+ applyAnimationClasses(element, capturedAnimation[2]);
- browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 5 });
- $animate.triggerReflow();
- $animate.triggerCallbackPromise();
- expect(ready).toBe(true);
+ expect(element).toHaveClass('red');
+ expect(element).not.toHaveClass('green');
+ }));
- ready = false;
- $animate.removeClass(element, 'on').then(function() {
- ready = true;
- });
- $rootScope.$digest();
+ it('should automatically cancel out class-based animations if the element already contains or doesn\' contain the applied classes',
+ inject(function($animate, $rootScope) {
- $animate.triggerReflow();
- browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 1 });
- $animate.triggerCallbackPromise();
- expect(ready).toBe(true);
- }));
+ parent.append(element);
+ element.addClass('one three');
- it('should not apply a transition upon removal of a class that has a transition',
- inject(function($sniffer, $compile, $rootScope, $rootElement, $animate) {
+ $animate.addClass(element, 'one');
+ $animate.addClass(element, 'two');
+ $animate.removeClass(element, 'three');
+ $animate.removeClass(element, 'four');
- if (!$sniffer.transitions) return;
+ $rootScope.$digest();
- ss.addRule('.base-class.on', '-webkit-transition:5s linear all;' +
- 'transition:5s linear all;');
+ options = capturedAnimation[2];
+ expect(options.addClass).toEqual('two');
+ expect(options.removeClass).toEqual('three');
+ }));
- $animate.enabled(true);
+ it('and skip the animation entirely if no class-based animations remain and if there is no structural animation applied',
+ inject(function($animate, $rootScope) {
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ parent.append(element);
+ element.addClass('one three');
- var ready = false;
- $animate.removeClass(element, 'on').then(function() {
- ready = true;
- });
- $rootScope.$digest();
+ $animate.addClass(element, 'one');
+ $animate.removeClass(element, 'four');
- $animate.triggerReflow();
- $animate.triggerCallbackPromise();
- expect(ready).toBe(true);
- }));
+ $rootScope.$digest();
+ expect(capturedAnimation).toBeFalsy();
+ }));
- it('should immediately close the former animation if the same CSS class is added/removed',
- inject(function($sniffer, $compile, $rootScope, $rootElement, $animate) {
+ it('but not skip the animation if it is a structural animation and if there are no classes to be animated',
+ inject(function($animate, $rootScope) {
- if (!$sniffer.transitions) return;
+ element.addClass('one three');
- ss.addRule('.water-class', '-webkit-transition:2s linear all;' +
- 'transition:2s linear all;');
+ $animate.addClass(element, 'one');
+ $animate.removeClass(element, 'four');
+ $animate.enter(element, parent);
- $animate.enabled(true);
+ $rootScope.$digest();
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ expect(capturedAnimation[1]).toBe('enter');
+ }));
- var signature = '';
- $animate.removeClass(element, 'on').then(function() {
- signature += 'A';
- });
- $rootScope.$digest();
+ it('class-based animations, however it should also cancel former structural animations in the process',
+ inject(function($animate, $rootScope) {
- $animate.addClass(element, 'on').then(function() {
- signature += 'B';
- });
- $rootScope.$digest();
+ element.addClass('green lime');
- $animate.triggerReflow();
- $animate.triggerCallbackPromise();
- expect(signature).toBe('A');
+ $animate.enter(element, parent);
+ $animate.addClass(element, 'red');
+ $animate.removeClass(element, 'green');
- browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 2000 });
- $animate.triggerCallbackPromise();
+ $animate.leave(element);
+ $animate.addClass(element, 'pink');
+ $animate.removeClass(element, 'lime');
- expect(signature).toBe('AB');
- }));
+ expect(element).toHaveClass('red');
+ expect(element).not.toHaveClass('green');
+ expect(element).not.toHaveClass('pink');
+ expect(element).toHaveClass('lime');
- it('should cancel the previous reflow when new animations are added', function() {
- var cancelReflowCallback = jasmine.createSpy('callback');
- module(function($provide) {
- $provide.value('$$animateReflow', function() {
- return cancelReflowCallback;
- });
- });
- inject(function($animate, $sniffer, $rootScope, $compile) {
- if (!$sniffer.transitions) return;
+ $rootScope.$digest();
- ss.addRule('.fly', '-webkit-transition:2s linear all;' +
- 'transition:2s linear all;');
+ expect(capturedAnimation[0]).toBe(element);
+ expect(capturedAnimation[1]).toBe('leave');
- $animate.enabled(true);
+ // $$hashKey causes comparison issues
+ expect(element.parent()[0]).toEqual(parent[0]);
- var element = $compile('
')($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ options = capturedAnimation[2];
+ expect(options.addClass).toEqual('pink');
+ expect(options.removeClass).toEqual('lime');
+ }));
- expect(cancelReflowCallback).not.toHaveBeenCalled();
+ it('should retain the instance to the very first runner object when multiple element-level animations are issued',
+ inject(function($animate, $rootScope) {
- $animate.addClass(element, 'fast');
- $rootScope.$digest();
+ element.addClass('green');
- $animate.addClass(element, 'smooth');
- $rootScope.$digest();
- $animate.triggerReflow();
+ var r1 = $animate.enter(element, parent);
+ var r2 = $animate.addClass(element, 'red');
+ var r3 = $animate.removeClass(element, 'green');
- expect(cancelReflowCallback).toHaveBeenCalled();
- });
- });
+ expect(r1).toBe(r2);
+ expect(r2).toBe(r3);
+ }));
- it('should immediately close off a leave animation if the element is removed from the DOM', function() {
- var stat;
- module(function($animateProvider) {
- $animateProvider.register('.going', function() {
- return {
- leave: function() {
- //left blank so it hangs
- stat = 'leaving';
- return function(cancelled) {
- stat = cancelled && 'gone';
- };
- }
- };
- });
- });
- inject(function($sniffer, $compile, $rootScope, $rootElement, $animate) {
+ it('should not skip or miss the animations when animations are executed sequential',
+ inject(function($animate, $rootScope, $$rAF, $rootElement) {
- $animate.enabled(true);
+ element = jqLite('
');
- var element = $compile('
')($rootScope);
- var child = $compile('
')($rootScope);
$rootElement.append(element);
- element.append(child);
- $animate.leave(child);
+ $animate.addClass(element, 'rclass');
+ $animate.removeClass(element, 'rclass');
+ $animate.addClass(element, 'rclass');
+ $animate.removeClass(element, 'rclass');
+
$rootScope.$digest();
+ $$rAF.flush();
- expect(stat).toBe('leaving');
+ expect(element).not.toHaveClass('rclass');
+ }));
+ });
+ });
- child.remove();
+ they('should allow an animation to run on the $prop element', ['$rootElement', 'body'], function(name) {
+ var capturedAnimation;
- expect(stat).toBe('gone');
+ module(function($provide) {
+ $provide.factory('$rootElement', function($document) {
+ return jqLite($document[0].querySelector('html'));
+ });
+ $provide.factory('$$animation', function($$AnimateRunner) {
+ return function(element, method, options) {
+ capturedAnimation = arguments;
+ return new $$AnimateRunner();
+ };
});
});
+ inject(function($animate, $rootScope, $document, $rootElement) {
+ $animate.enabled(true);
- it('should remove all element and comment nodes during leave animation',
- inject(function($compile, $rootScope) {
-
- $rootScope.items = [1,2,3,4,5];
-
- var element = html($compile(
- '
' +
- '
start
' +
- '
end
' +
- '
'
- )($rootScope));
+ var body = jqLite($document[0].body);
+ var targetElement = name === 'body' ? body : $rootElement;
+ $animate.addClass(targetElement, 'red');
$rootScope.$digest();
- $rootScope.items = [];
-
- $rootScope.$digest();
+ expect(capturedAnimation[0]).toBe(targetElement);
+ expect(capturedAnimation[1]).toBe('addClass');
+ });
+ });
- expect(element.children().length).toBe(0);
+ describe('[ng-animate-children]', function() {
+ var parent, element, child, capturedAnimation, captureLog;
+ beforeEach(module(function($provide) {
+ capturedAnimation = null;
+ captureLog = [];
+ $provide.factory('$$animation', function($$AnimateRunner) {
+ return function(element, method, options) {
+ options.domOperation();
+ captureLog.push(capturedAnimation = arguments);
+ return new $$AnimateRunner();
+ };
+ });
+ return function($rootElement, $$body, $animate) {
+ $$body.append($rootElement);
+ parent = jqLite('
');
+ element = jqLite('
');
+ child = jqLite('
');
+ $animate.enabled(true);
+ };
}));
- it('should not throw an error when only comment nodes are rendered in the animation',
- inject(function($rootScope, $compile) {
+ it('should allow child animations to run when the attribute is used',
+ inject(function($animate, $rootScope, $rootElement, $compile) {
- $rootScope.items = [1,2,3,4,5];
+ $animate.enter(parent, $rootElement);
+ $animate.enter(element, parent);
+ $animate.enter(child, element);
+ $rootScope.$digest();
+ expect(captureLog.length).toBe(1);
- var element = html($compile('
')($rootScope));
+ captureLog = [];
+ parent.attr('ng-animate-children', '');
+ $compile(parent)($rootScope);
$rootScope.$digest();
- $rootScope.items = [];
-
+ $animate.enter(parent, $rootElement);
$rootScope.$digest();
+ expect(captureLog.length).toBe(1);
- expect(element.children().length).toBe(0);
+ $animate.enter(element, parent);
+ $animate.enter(child, element);
+ $rootScope.$digest();
+ expect(captureLog.length).toBe(3);
}));
- describe('ngAnimateChildren', function() {
- var spy;
-
- beforeEach(module(function($animateProvider) {
- spy = jasmine.createSpy();
- $animateProvider.register('.parent', mockAnimate);
- $animateProvider.register('.child', mockAnimate);
- return function($animate) {
- $animate.enabled(true);
- };
+ it('should fully disallow all parallel child animations from running if `off` is used',
+ inject(function($animate, $rootScope, $rootElement, $compile) {
- function mockAnimate() {
- return {
- enter: spy,
- leave: spy,
- addClass: spy,
- removeClass: spy
- };
- }
- }));
+ $rootElement.append(parent);
+ parent.append(element);
+ element.append(child);
- it('should animate based on a boolean flag', inject(function($animate, $sniffer, $rootScope, $compile) {
- var html = '
';
+ parent.attr('ng-animate-children', 'off');
+ element.attr('ng-animate-children', 'on');
- var element = $compile(html)($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ $compile(parent)($rootScope);
+ $compile(element)($rootScope);
+ $rootScope.$digest();
- var scope = $rootScope;
+ $animate.leave(parent);
+ $animate.leave(element);
+ $animate.leave(child);
+ $rootScope.$digest();
- scope.bool = true;
- scope.$digest();
+ expect(captureLog.length).toBe(1);
- scope.on1 = true;
- scope.on2 = true;
- scope.$digest();
+ dealoc(element);
+ dealoc(child);
+ }));
- $animate.triggerReflow();
+ it('should watch to see if the ng-animate-children attribute changes',
+ inject(function($animate, $rootScope, $rootElement, $compile) {
- expect(spy).toHaveBeenCalled();
- expect(spy.callCount).toBe(2);
+ $rootElement.append(parent);
+ $rootScope.val = 'on';
+ parent.attr('ng-animate-children', '{{ val }}');
+ $compile(parent)($rootScope);
+ $rootScope.$digest();
- scope.bool = false;
- scope.$digest();
+ $animate.enter(parent, $rootElement);
+ $animate.enter(element, parent);
+ $animate.enter(child, element);
+ $rootScope.$digest();
+ expect(captureLog.length).toBe(3);
- scope.on1 = false;
- scope.$digest();
+ captureLog = [];
- scope.on2 = false;
- scope.$digest();
+ $rootScope.val = 'off';
+ $rootScope.$digest();
- $animate.triggerReflow();
+ $animate.leave(parent);
+ $animate.leave(element);
+ $animate.leave(child);
+ $rootScope.$digest();
- expect(spy.callCount).toBe(3);
- }));
+ expect(captureLog.length).toBe(1);
- it('should default to true when no expression is provided',
- inject(function($animate, $sniffer, $rootScope, $compile) {
+ dealoc(element);
+ dealoc(child);
+ }));
+ });
- var html = '
';
+ describe('.pin()', function() {
+ var capturedAnimation;
- var element = $compile(html)($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ beforeEach(module(function($provide) {
+ capturedAnimation = null;
+ $provide.factory('$$animation', function($$AnimateRunner) {
+ return function() {
+ capturedAnimation = arguments;
+ return new $$AnimateRunner();
+ };
+ });
+ }));
- $rootScope.on1 = true;
- $rootScope.$digest();
+ it('should allow an element to pinned elsewhere and still be available in animations',
+ inject(function($animate, $compile, $$body, $rootElement, $rootScope) {
- $rootScope.on2 = true;
- $rootScope.$digest();
+ var innerParent = jqLite('
');
+ $$body.append(innerParent);
+ innerParent.append($rootElement);
- $animate.triggerReflow();
+ var element = jqLite('
');
+ $$body.append(element);
- expect(spy).toHaveBeenCalled();
- expect(spy.callCount).toBe(2);
- }));
+ $animate.addClass(element, 'red');
+ $rootScope.$digest();
+ expect(capturedAnimation).toBeFalsy();
- it('should not perform inherited animations if any parent restricts it',
- inject(function($animate, $sniffer, $rootScope, $compile) {
+ $animate.pin(element, $rootElement);
- var html = '
';
+ $animate.addClass(element, 'blue');
+ $rootScope.$digest();
+ expect(capturedAnimation).toBeTruthy();
- var element = $compile(html)($rootScope);
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ dealoc(element);
+ }));
- $rootScope.$digest();
+ it('should adhere to the disabled state of the hosted parent when an element is pinned',
+ inject(function($animate, $compile, $$body, $rootElement, $rootScope) {
- $rootScope.on = true;
- $rootScope.$digest();
+ var innerParent = jqLite('
');
+ $$body.append(innerParent);
+ innerParent.append($rootElement);
+ var innerChild = jqLite('
');
+ $rootElement.append(innerChild);
- $animate.triggerReflow();
+ var element = jqLite('
');
+ $$body.append(element);
- expect(spy).toHaveBeenCalled();
- expect(spy.callCount).toBe(1);
- }));
+ $animate.pin(element, innerChild);
- it('should permit class-based animations when ng-animate-children is true for a structural animation', function() {
- var spy = jasmine.createSpy();
-
- module(function($animateProvider) {
- $animateProvider.register('.inner', function() {
- return {
- beforeAddClass: function(element, className, done) {
- spy();
- done();
- },
- beforeRemoveClass: function(element, className, done) {
- spy();
- done();
- }
- };
- });
- });
+ $animate.enabled(innerChild, false);
- inject(function($animate, $sniffer, $rootScope, $compile) {
+ $animate.addClass(element, 'blue');
+ $rootScope.$digest();
+ expect(capturedAnimation).toBeFalsy();
- $animate.enabled(true);
+ $animate.enabled(innerChild, true);
- var html = '
';
+ $animate.addClass(element, 'red');
+ $rootScope.$digest();
+ expect(capturedAnimation).toBeTruthy();
- var element = angular.element(html);
- $compile(element)($rootScope);
- var body = angular.element($document[0].body);
- body.append($rootElement);
+ dealoc(element);
+ }));
+ });
- $rootScope.$watch('bool', function(bool) {
- if (bool) {
- $animate.enter(element, $rootElement);
- } else if (element.parent().length) {
- $animate.leave(element);
- }
- });
+ describe('callbacks', function() {
+ var captureLog = [];
+ var capturedAnimation = [];
+ var runner;
+ var body;
+ beforeEach(module(function($provide) {
+ runner = null;
+ capturedAnimation = null;
+ $provide.factory('$$animation', function($$AnimateRunner) {
+ return function() {
+ captureLog.push(capturedAnimation = arguments);
+ return runner = new $$AnimateRunner();
+ };
+ });
- $rootScope.$digest();
- expect(spy.callCount).toBe(0);
+ return function($$body, $rootElement, $animate) {
+ $$body.append($rootElement);
+ $animate.enabled(true);
+ };
+ }));
- $rootScope.bool = true;
- $rootScope.$digest();
- $animate.triggerReflow();
- $animate.triggerCallbacks();
- expect(spy.callCount).toBe(1);
+ it('should trigger a callback for an enter animation',
+ inject(function($animate, $rootScope, $$rAF, $rootElement, $$body) {
- $rootScope.bool = false;
- $rootScope.$digest();
- $animate.triggerReflow();
- $animate.triggerCallbacks();
- expect(spy.callCount).toBe(2);
- });
+ var callbackTriggered = false;
+ $animate.on('enter', $$body, function() {
+ callbackTriggered = true;
});
- });
- describe('SVG', function() {
- it('should properly apply transitions on an SVG element',
- inject(function($animate, $rootScope, $compile, $rootElement, $sniffer) {
+ element = jqLite('
');
+ $animate.enter(element, $rootElement);
+ $rootScope.$digest();
- //jQuery doesn't handle SVG elements natively. Instead, an add-on library
- //is required which is called jquery.svg.js. Therefore, when jQuery is
- //active here there is no point to test this since it won't work by default.
- if (!$sniffer.transitions) return;
+ $$rAF.flush();
- ss.addRule('circle.ng-enter', '-webkit-transition:1s linear all;' +
- 'transition:1s linear all;');
+ expect(callbackTriggered).toBe(true);
+ }));
- var element = $compile('
' +
- ' ' +
- ' ')($rootScope);
+ it('should fire the callback with the signature of (element, phase, data)',
+ inject(function($animate, $rootScope, $$rAF, $rootElement, $$body) {
- $rootElement.append(element);
- jqLite($document[0].body).append($rootElement);
+ var capturedElement;
+ var capturedPhase;
+ var capturedData;
+ $animate.on('enter', $$body,
+ function(element, phase, data) {
- $rootScope.$digest();
+ capturedElement = element;
+ capturedPhase = phase;
+ capturedData = data;
+ });
- $rootScope.on = true;
- $rootScope.$digest();
- $animate.triggerReflow();
+ element = jqLite('
');
+ $animate.enter(element, $rootElement);
+ $rootScope.$digest();
+ $$rAF.flush();
- var child = element.find('circle');
+ expect(capturedElement).toBe(element);
+ expect(isString(capturedPhase)).toBe(true);
+ expect(isObject(capturedData)).toBe(true);
+ }));
- expect(jqLiteHasClass(child[0], 'ng-enter')).toBe(true);
- expect(jqLiteHasClass(child[0], 'ng-enter-active')).toBe(true);
+ it('should not fire a callback if the element is outside of the given container',
+ inject(function($animate, $rootScope, $$rAF, $rootElement) {
- browserTrigger(child, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 });
+ var callbackTriggered = false;
+ var innerContainer = jqLite('
');
+ $rootElement.append(innerContainer);
- expect(jqLiteHasClass(child[0], 'ng-enter')).toBe(false);
- expect(jqLiteHasClass(child[0], 'ng-enter-active')).toBe(false);
- }));
+ $animate.on('enter', innerContainer,
+ function(element, phase, data) {
+ callbackTriggered = true;
+ });
- it('should properly remove classes from SVG elements', inject(function($animate, $rootScope) {
- var element = jqLite('
');
- var child = element.find('rect');
- $animate.removeClass(child, 'class-of-doom');
+ element = jqLite('
');
+ $animate.enter(element, $rootElement);
+ $rootScope.$digest();
+ $$rAF.flush();
- $rootScope.$digest();
- expect(child.attr('class')).toBe('');
+ expect(callbackTriggered).toBe(false);
+ }));
- dealoc(element);
- }));
- });
- });
+ it('should fire a callback if the element is the given container',
+ inject(function($animate, $rootScope, $$rAF, $rootElement) {
+ element = jqLite('
');
- describe('CSS class DOM manipulation', function() {
- var element;
- var addClass;
- var removeClass;
+ var callbackTriggered = false;
+ $animate.on('enter', element,
+ function(element, phase, data) {
- beforeEach(module(provideLog));
+ callbackTriggered = true;
+ });
- afterEach(function() {
- dealoc(element);
- });
+ $animate.enter(element, $rootElement);
+ $rootScope.$digest();
+ $$rAF.flush();
- function setupClassManipulationSpies() {
- inject(function($animate) {
- addClass = spyOn($originalAnimate, '$$addClassImmediately').andCallThrough();
- removeClass = spyOn($originalAnimate, '$$removeClassImmediately').andCallThrough();
- });
- }
-
- function setupClassManipulationLogger(log) {
- inject(function($animate) {
- var addClassImmediately = $originalAnimate.$$addClassImmediately;
- var removeClassImmediately = $originalAnimate.$$removeClassImmediately;
- addClass = spyOn($originalAnimate, '$$addClassImmediately').andCallFake(function(element, classes) {
- var names = classes;
- if (Object.prototype.toString.call(classes) === '[object Array]') names = classes.join(' ');
- log('addClass(' + names + ')');
- return addClassImmediately.call($originalAnimate, element, classes);
- });
- removeClass = spyOn($originalAnimate, '$$removeClassImmediately').andCallFake(function(element, classes) {
- var names = classes;
- if (Object.prototype.toString.call(classes) === '[object Array]') names = classes.join(' ');
- log('removeClass(' + names + ')');
- return removeClassImmediately.call($originalAnimate, element, classes);
- });
- });
- }
+ expect(callbackTriggered).toBe(true);
+ }));
+
+ it('should remove all the event-based event listeners when $animate.off(event) is called',
+ inject(function($animate, $rootScope, $$rAF, $rootElement, $$body) {
+ element = jqLite('
');
+
+ var count = 0;
+ $animate.on('enter', element, counter);
+ $animate.on('enter', $$body, counter);
- it('should defer class manipulation until end of digest', inject(function($rootScope, $animate, log) {
- setupClassManipulationLogger(log);
- element = jqLite('
test
');
+ function counter(element, phase) {
+ count++;
+ }
- $rootScope.$apply(function() {
- $animate.addClass(element, 'test-class1');
- expect(element).not.toHaveClass('test-class1');
+ $animate.enter(element, $rootElement);
+ $rootScope.$digest();
+ $$rAF.flush();
- $animate.removeClass(element, 'test-class1');
+ expect(count).toBe(2);
- $animate.addClass(element, 'test-class2');
- expect(element).not.toHaveClass('test-class2');
+ $animate.off('enter');
- $animate.setClass(element, 'test-class3', 'test-class4');
- expect(element).not.toHaveClass('test-class3');
- expect(element).not.toHaveClass('test-class4');
- expect(log).toEqual([]);
- });
+ $animate.enter(element, $rootElement);
+ $rootScope.$digest();
+ $$rAF.flush();
- expect(element).not.toHaveClass('test-class1');
- expect(element).not.toHaveClass('test-class4');
- expect(element).toHaveClass('test-class2');
- expect(element).toHaveClass('test-class3');
- expect(log).toEqual(['addClass(test-class2 test-class3)']);
- expect(addClass.callCount).toBe(1);
- expect(removeClass.callCount).toBe(0);
+ expect(count).toBe(2);
}));
+ it('should remove the container-based event listeners when $animate.off(event, container) is called',
+ inject(function($animate, $rootScope, $$rAF, $rootElement, $$body) {
- it('should defer class manipulation until postDigest when outside of digest', inject(function($rootScope, $animate, log) {
- setupClassManipulationLogger(log);
- element = jqLite('
test
');
+ element = jqLite('
');
- $animate.addClass(element, 'test-class1');
- $animate.removeClass(element, 'test-class1');
- $animate.addClass(element, 'test-class2');
- $animate.setClass(element, 'test-class3', 'test-class4');
+ var count = 0;
+ $animate.on('enter', element, counter);
+ $animate.on('enter', $$body, counter);
- expect(log).toEqual([]);
- $rootScope.$digest();
+ function counter(element, phase) {
+ if (phase === 'start') {
+ count++;
+ }
+ }
- expect(log).toEqual(['addClass(test-class2 test-class3)', 'removeClass(test-class4)']);
- expect(element).not.toHaveClass('test-class1');
- expect(element).toHaveClass('test-class2');
- expect(element).toHaveClass('test-class3');
- expect(addClass.callCount).toBe(1);
- expect(removeClass.callCount).toBe(1);
- }));
+ $animate.enter(element, $rootElement);
+ $rootScope.$digest();
+ $$rAF.flush();
+ expect(count).toBe(2);
- it('should perform class manipulation in expected order at end of digest', inject(function($rootScope, $animate, log) {
- element = jqLite('
test
');
+ $animate.off('enter', $$body);
- setupClassManipulationLogger(log);
+ $animate.enter(element, $rootElement);
+ $rootScope.$digest();
+ $$rAF.flush();
- $rootScope.$apply(function() {
- $animate.addClass(element, 'test-class1');
- $animate.addClass(element, 'test-class2');
- $animate.removeClass(element, 'test-class1');
- $animate.removeClass(element, 'test-class3');
- $animate.addClass(element, 'test-class3');
- });
- expect(log).toEqual(['addClass(test-class2)']);
+ expect(count).toBe(3);
}));
+ it('should remove the callback-based event listener when $animate.off(event, container, callback) is called',
+ inject(function($animate, $rootScope, $$rAF, $rootElement) {
- it('should return a promise which is resolved on a different turn', inject(function(log, $animate, $browser, $rootScope) {
- element = jqLite('
test
');
+ element = jqLite('
');
- $animate.addClass(element, 'test1').then(log.fn('addClass(test1)'));
- $animate.removeClass(element, 'test2').then(log.fn('removeClass(test2)'));
+ var count = 0;
+ $animate.on('enter', element, counter1);
+ $animate.on('enter', element, counter2);
- $rootScope.$digest();
- expect(log).toEqual([]);
- $browser.defer.flush();
- expect(log).toEqual(['addClass(test1)', 'removeClass(test2)']);
+ function counter1(element, phase) {
+ if (phase === 'start') {
+ count++;
+ }
+ }
- log.reset();
- element = jqLite('
test
');
+ function counter2(element, phase) {
+ if (phase === 'start') {
+ count++;
+ }
+ }
- $rootScope.$apply(function() {
- $animate.addClass(element, 'test3').then(log.fn('addClass(test3)'));
- $animate.removeClass(element, 'test4').then(log.fn('removeClass(test4)'));
- expect(log).toEqual([]);
- });
+ $animate.enter(element, $rootElement);
+ $rootScope.$digest();
+ $$rAF.flush();
- $browser.defer.flush();
- expect(log).toEqual(['addClass(test3)', 'removeClass(test4)']);
- }));
+ expect(count).toBe(2);
+ $animate.off('enter', element, counter2);
- it('should defer class manipulation until end of digest for SVG', inject(function($rootScope, $animate) {
- if (!window.SVGElement) return;
- setupClassManipulationSpies();
- element = jqLite('
');
- var target = element.children().eq(0);
+ $animate.enter(element, $rootElement);
+ $rootScope.$digest();
+ $$rAF.flush();
- $rootScope.$apply(function() {
- $animate.addClass(target, 'test-class1');
- expect(target).not.toHaveClass('test-class1');
+ expect(count).toBe(3);
+ }));
- $animate.removeClass(target, 'test-class1');
+ it('should fire a `start` callback when the animation starts with the matching element',
+ inject(function($animate, $rootScope, $$rAF, $rootElement, $$body) {
- $animate.addClass(target, 'test-class2');
- expect(target).not.toHaveClass('test-class2');
+ element = jqLite('
');
- $animate.setClass(target, 'test-class3', 'test-class4');
- expect(target).not.toHaveClass('test-class3');
- expect(target).not.toHaveClass('test-class4');
+ var capturedState;
+ var capturedElement;
+ $animate.on('enter', $$body, function(element, phase) {
+ capturedState = phase;
+ capturedElement = element;
});
- expect(target).not.toHaveClass('test-class1');
- expect(target).toHaveClass('test-class2');
- expect(addClass.callCount).toBe(1);
- expect(removeClass.callCount).toBe(0);
+ $animate.enter(element, $rootElement);
+ $rootScope.$digest();
+ $$rAF.flush();
+
+ expect(capturedState).toBe('start');
+ expect(capturedElement).toBe(element);
}));
+ it('should fire a `close` callback when the animation ends with the matching element',
+ inject(function($animate, $rootScope, $$rAF, $rootElement, $$body) {
- it('should defer class manipulation until postDigest when outside of digest for SVG', inject(function($rootScope, $animate, log) {
- if (!window.SVGElement) return;
- setupClassManipulationLogger(log);
- element = jqLite('
');
- var target = element.children().eq(0);
+ element = jqLite('
');
- $animate.addClass(target, 'test-class1');
- $animate.removeClass(target, 'test-class1');
- $animate.addClass(target, 'test-class2');
- $animate.setClass(target, 'test-class3', 'test-class4');
+ var capturedState;
+ var capturedElement;
+ $animate.on('enter', $$body, function(element, phase) {
+ capturedState = phase;
+ capturedElement = element;
+ });
- expect(log).toEqual([]);
+ var runner = $animate.enter(element, $rootElement);
$rootScope.$digest();
+ runner.end();
+ $$rAF.flush();
- expect(log).toEqual(['addClass(test-class2 test-class3)', 'removeClass(test-class4)']);
- expect(target).not.toHaveClass('test-class1');
- expect(target).toHaveClass('test-class2');
- expect(target).toHaveClass('test-class3');
- expect(addClass.callCount).toBe(1);
- expect(removeClass.callCount).toBe(1);
+ expect(capturedState).toBe('close');
+ expect(capturedElement).toBe(element);
}));
+ it('should remove the event listener if the element is removed',
+ inject(function($animate, $rootScope, $$rAF, $rootElement) {
- it('should perform class manipulation in expected order at end of digest for SVG', inject(function($rootScope, $animate, log) {
- if (!window.SVGElement) return;
- element = jqLite('
');
- var target = element.children().eq(0);
+ element = jqLite('
');
- setupClassManipulationLogger(log);
+ var count = 0;
+ $animate.on('enter', element, counter);
+ $animate.on('addClass', element, counter);
- $rootScope.$apply(function() {
- $animate.addClass(target, 'test-class1');
- $animate.addClass(target, 'test-class2');
- $animate.removeClass(target, 'test-class1');
- $animate.removeClass(target, 'test-class3');
- $animate.addClass(target, 'test-class3');
- });
- expect(log).toEqual(['addClass(test-class2)']);
+ function counter(element, phase) {
+ if (phase === 'start') {
+ count++;
+ }
+ }
+
+ $animate.enter(element, $rootElement);
+ $rootScope.$digest();
+ $$rAF.flush();
+
+ expect(count).toBe(1);
+ element.remove();
+
+ $animate.addClass(element, 'viljami');
+ $rootScope.$digest();
+ $$rAF.flush();
+ expect(count).toBe(1);
}));
+
});
});
diff --git a/test/ngAnimate/animationHelperFunctionsSpec.js b/test/ngAnimate/animationHelperFunctionsSpec.js
new file mode 100644
index 000000000000..eb66e6205555
--- /dev/null
+++ b/test/ngAnimate/animationHelperFunctionsSpec.js
@@ -0,0 +1,135 @@
+'use strict';
+
+describe("animation option helper functions", function() {
+
+ beforeEach(module('ngAnimate'));
+
+ var element, applyAnimationClasses;
+ beforeEach(inject(function($$jqLite) {
+ applyAnimationClasses = applyAnimationClassesFactory($$jqLite);
+ element = jqLite('
');
+ }));
+
+ describe('prepareAnimationOptions', function() {
+ it('should construct an options wrapper from the provided options',
+ inject(function() {
+
+ var options = prepareAnimationOptions({
+ value: 'hello'
+ });
+
+ expect(options.value).toBe('hello');
+ }));
+
+ it('should return the same instance it already instantiated as an options object with the given element',
+ inject(function() {
+
+ var options = prepareAnimationOptions({});
+ expect(prepareAnimationOptions(options)).toBe(options);
+
+ var options2 = {};
+ expect(prepareAnimationOptions(options2)).not.toBe(options);
+ }));
+ });
+
+ describe('applyAnimationStyles', function() {
+ it('should apply the provided `from` styles', inject(function() {
+ var options = prepareAnimationOptions({
+ from: { color: 'maroon' },
+ to: { color: 'blue' }
+ });
+
+ applyAnimationFromStyles(element, options);
+ expect(element.attr('style')).toContain('maroon');
+ }));
+
+ it('should apply the provided `to` styles', inject(function() {
+ var options = prepareAnimationOptions({
+ from: { color: 'red' },
+ to: { color: 'black' }
+ });
+
+ applyAnimationToStyles(element, options);
+ expect(element.attr('style')).toContain('black');
+ }));
+
+ it('should apply the both provided `from` and `to` styles', inject(function() {
+ var options = prepareAnimationOptions({
+ from: { color: 'red', 'font-size':'50px' },
+ to: { color: 'green' }
+ });
+
+ applyAnimationStyles(element, options);
+ expect(element.attr('style')).toContain('green');
+ expect(element.css('font-size')).toBe('50px');
+ }));
+
+ it('should only apply the options once', inject(function() {
+ var options = prepareAnimationOptions({
+ from: { color: 'red', 'font-size':'50px' },
+ to: { color: 'blue' }
+ });
+
+ applyAnimationStyles(element, options);
+ expect(element.attr('style')).toContain('blue');
+
+ element.attr('style', '');
+
+ applyAnimationStyles(element, options);
+ expect(element.attr('style') || '').toBe('');
+ }));
+ });
+
+ describe('applyAnimationClasses', function() {
+ it('should add/remove the provided CSS classes', inject(function() {
+ element.addClass('four six');
+ var options = prepareAnimationOptions({
+ addClass: 'one two three',
+ removeClass: 'four'
+ });
+
+ applyAnimationClasses(element, options);
+ expect(element).toHaveClass('one two three');
+ expect(element).toHaveClass('six');
+ expect(element).not.toHaveClass('four');
+ }));
+
+ it('should add/remove the provided CSS classes only once', inject(function() {
+ element.attr('class', 'blue');
+ var options = prepareAnimationOptions({
+ addClass: 'black',
+ removeClass: 'blue'
+ });
+
+ applyAnimationClasses(element, options);
+ element.attr('class', 'blue');
+
+ applyAnimationClasses(element, options);
+ expect(element).toHaveClass('blue');
+ expect(element).not.toHaveClass('black');
+ }));
+ });
+
+ describe('mergeAnimationOptions', function() {
+ it('should merge in new options', inject(function() {
+ element.attr('class', 'blue');
+ var options = prepareAnimationOptions({
+ name: 'matias',
+ age: 28,
+ addClass: 'black',
+ removeClass: 'blue gold'
+ });
+
+ mergeAnimationOptions(element, options, {
+ age: 29,
+ addClass: 'gold brown',
+ removeClass: 'orange'
+ });
+
+ expect(options.name).toBe('matias');
+ expect(options.age).toBe(29);
+ expect(options.addClass).toBe('black brown');
+ expect(options.removeClass).toBe('blue');
+ }));
+ });
+});
diff --git a/test/ngAnimate/animationSpec.js b/test/ngAnimate/animationSpec.js
new file mode 100644
index 000000000000..4eab910a1cbb
--- /dev/null
+++ b/test/ngAnimate/animationSpec.js
@@ -0,0 +1,951 @@
+'use strict';
+
+describe('$$animation', function() {
+
+ beforeEach(module('ngAnimate'));
+
+ var element;
+ afterEach(function() {
+ dealoc(element);
+ });
+
+ beforeEach(module(function($$animationProvider) {
+ $$animationProvider.drivers.length = 0;
+ }));
+
+ it("should not run an animation if there are no drivers",
+ inject(function($$animation, $$rAF, $rootScope) {
+
+ element = jqLite('
');
+ var done = false;
+ $$animation(element, 'someEvent').then(function() {
+ done = true;
+ });
+ $$rAF.flush();
+ $rootScope.$digest();
+ expect(done).toBe(true);
+ }));
+
+ it("should not run an animation if no drivers return an animation step function", function() {
+ module(function($$animationProvider, $provide) {
+ $$animationProvider.drivers.push('matiasDriver');
+ $provide.value('matiasDriver', function() {
+ return false;
+ });
+ });
+ inject(function($$animation, $$rAF, $rootScope) {
+ element = jqLite('
');
+ var done = false;
+ $$animation(element, 'someEvent').then(function() {
+ done = true;
+ });
+ $rootScope.$digest();
+ $$rAF.flush();
+ $rootScope.$digest();
+ expect(done).toBe(true);
+ });
+ });
+
+ describe("drivers", function() {
+ it("should use the first driver that returns a step function", function() {
+ var count = 0;
+ var activeDriver;
+ module(function($$animationProvider, $provide) {
+ $$animationProvider.drivers.push('1');
+ $$animationProvider.drivers.push('2');
+ $$animationProvider.drivers.push('3');
+
+ var runner;
+
+ $provide.value('1', function() {
+ count++;
+ });
+
+ $provide.value('2', function() {
+ count++;
+ return {
+ start: function() {
+ activeDriver = '2';
+ return runner;
+ }
+ };
+ });
+
+ $provide.value('3', function() {
+ count++;
+ });
+
+ return function($$AnimateRunner) {
+ runner = new $$AnimateRunner();
+ };
+ });
+
+ inject(function($$animation, $rootScope, $rootElement) {
+ element = jqLite('
');
+ $rootElement.append(element);
+
+ $$animation(element, 'enter');
+ $rootScope.$digest();
+
+ expect(count).toBe(2);
+ expect(activeDriver).toBe('2');
+ });
+ });
+
+ describe('step function', function() {
+ var capturedAnimation;
+ beforeEach(module(function($$animationProvider, $provide) {
+ element = jqLite('
');
+
+ $$animationProvider.drivers.push('stepper');
+ $provide.factory('stepper', function($$AnimateRunner) {
+ return function() {
+ capturedAnimation = arguments;
+ return {
+ start: function() {
+ return new $$AnimateRunner();
+ }
+ };
+ };
+ });
+ }));
+
+ it("should obtain the element, event, the provided options and the domOperation",
+ inject(function($$animation, $rootScope, $rootElement) {
+ $rootElement.append(element);
+
+ var options = {};
+ options.foo = 'bar';
+ options.domOperation = function() {
+ domOperationCalled = true;
+ };
+ var domOperationCalled = false;
+ $$animation(element, 'megaEvent', options);
+ $rootScope.$digest();
+
+ var details = capturedAnimation[0];
+ expect(details.element).toBe(element);
+ expect(details.event).toBe('megaEvent');
+ expect(details.options.foo).toBe(options.foo);
+
+ // the function is wrapped inside of $$animation, but it is still a function
+ expect(domOperationCalled).toBe(false);
+ details.options.domOperation();
+ expect(domOperationCalled).toBe(true);
+ }));
+
+ it("should obtain the classes string which is a combination of className, addClass and removeClass",
+ inject(function($$animation, $rootScope, $rootElement) {
+
+ element.addClass('blue red');
+ $rootElement.append(element);
+
+ $$animation(element, 'enter', {
+ addClass: 'green',
+ removeClass: 'orange',
+ tempClasses: 'pink'
+ });
+
+ $rootScope.$digest();
+
+ var classes = capturedAnimation[0].classes;
+ expect(classes).toBe('blue red green orange pink');
+ }));
+ });
+
+ it("should traverse the drivers in reverse order", function() {
+ var log = [];
+ module(function($$animationProvider, $provide) {
+ $$animationProvider.drivers.push('first');
+ $$animationProvider.drivers.push('second');
+
+ $provide.value('first', function() {
+ log.push('first');
+ return false;
+ });
+
+ $provide.value('second', function() {
+ log.push('second');
+ return false;
+ });
+ });
+
+ inject(function($$animation, $rootScope, $rootElement) {
+ element = jqLite('
');
+ $rootElement.append(element);
+ $$animation(element, 'enter');
+ $rootScope.$digest();
+ expect(log).toEqual(['second', 'first']);
+ });
+ });
+
+ they("should $prop the animation call if the driver $proped the returned promise",
+ ['resolve', 'reject'], function(event) {
+
+ module(function($$animationProvider, $provide) {
+ $$animationProvider.drivers.push('resolvingAnimation');
+ $provide.factory('resolvingAnimation', function($$AnimateRunner) {
+ return function() {
+ return {
+ start: function() {
+ return new $$AnimateRunner();
+ }
+ };
+ };
+ });
+ });
+
+ inject(function($$animation, $rootScope, $$rAF) {
+ var status, element = jqLite('
');
+ var runner = $$animation(element, 'enter');
+ runner.then(function() {
+ status = 'resolve';
+ }, function() {
+ status = 'reject';
+ });
+
+ // the animation is started
+ $rootScope.$digest();
+
+ event === 'resolve' ? runner.end() : runner.cancel();
+
+ // the resolve/rejection digest
+ $$rAF.flush();
+ $rootScope.$digest();
+
+ expect(status).toBe(event);
+ });
+ });
+
+ they("should $prop the driver animation when runner.$prop() is called",
+ ['cancel', 'end'], function(method) {
+
+ var log = [];
+
+ module(function($$animationProvider, $provide) {
+ $$animationProvider.drivers.push('actualDriver');
+ $provide.factory('actualDriver', function($$AnimateRunner) {
+ return function() {
+ return {
+ start: function() {
+ log.push('start');
+ return new $$AnimateRunner({
+ end: function() {
+ log.push('end');
+ },
+ cancel: function() {
+ log.push('cancel');
+ }
+ });
+ }
+ };
+ };
+ });
+ });
+
+ inject(function($$animation, $rootScope, $rootElement) {
+ element = jqLite('
');
+ $rootElement.append(element);
+
+ var runner = $$animation(element, 'enter');
+ $rootScope.$digest();
+
+ runner[method]();
+ expect(log).toEqual(['start', method]);
+ });
+ });
+ });
+
+ describe('when', function() {
+ var captureLog;
+ var runnerLog;
+ var capturedAnimation;
+
+ beforeEach(module(function($$animationProvider, $provide) {
+ captureLog = [];
+ runnerLog = [];
+ capturedAnimation = null;
+
+ $$animationProvider.drivers.push('interceptorDriver');
+ $provide.factory('interceptorDriver', function($$AnimateRunner) {
+ return function(details) {
+ captureLog.push(capturedAnimation = details); //only one param is passed into the driver
+ return {
+ start: function() {
+ return new $$AnimateRunner({
+ end: runnerEvent('end'),
+ cancel: runnerEvent('cancel')
+ });
+ }
+ };
+ };
+ });
+
+ function runnerEvent(token) {
+ return function() {
+ runnerLog.push(token);
+ };
+ }
+ }));
+
+ describe("singular", function() {
+ beforeEach(module(function($provide) {
+ element = jqLite('
');
+ return function($rootElement) {
+ $rootElement.append(element);
+ };
+ }));
+
+ they('should return a runner that object that contains a $prop() function',
+ ['end', 'cancel', 'then'], function(method) {
+ inject(function($$animation) {
+ var runner = $$animation(element, 'someEvent');
+ expect(isFunction(runner[method])).toBe(true);
+ });
+ });
+
+ they('should close the animation if runner.$prop() is called before the $postDigest phase kicks in',
+ ['end', 'cancel'], function(method) {
+ inject(function($$animation, $rootScope, $$rAF) {
+ var status;
+ var runner = $$animation(element, 'someEvent');
+ runner.then(function() { status = 'end'; },
+ function() { status = 'cancel'; });
+
+ runner[method]();
+ $rootScope.$digest();
+ expect(runnerLog).toEqual([]);
+
+ $$rAF.flush();
+ expect(status).toBe(method);
+ });
+ });
+
+ they('should update the runner methods to the ones provided by the driver when the animation starts',
+ ['end', 'cancel'], function(method) {
+
+ var spy = jasmine.createSpy();
+ module(function($$animationProvider, $provide) {
+ $$animationProvider.drivers.push('animalDriver');
+ $provide.factory('animalDriver', function($$AnimateRunner) {
+ return function() {
+ return {
+ start: function() {
+ var data = {};
+ data[method] = spy;
+ return new $$AnimateRunner(data);
+ }
+ };
+ };
+ });
+ });
+ inject(function($$animation, $rootScope, $rootElement) {
+ var r1 = $$animation(element, 'someEvent');
+ r1[method]();
+ expect(spy).not.toHaveBeenCalled();
+ $rootScope.$digest(); // this clears the digest which cleans up the mess
+
+ var r2 = $$animation(element, 'otherEvent');
+ $rootScope.$digest();
+ r2[method]();
+ expect(spy).toHaveBeenCalled();
+ });
+ });
+
+ it('should not start the animation if the element is removed from the DOM before the postDigest kicks in',
+ inject(function($$animation) {
+
+ var runner = $$animation(element, 'someEvent');
+
+ expect(capturedAnimation).toBeFalsy();
+ element.remove();
+ expect(capturedAnimation).toBeFalsy();
+ }));
+
+ it('should immediately end the animation if the element is removed from the DOM during the animation',
+ inject(function($$animation, $$rAF, $rootScope) {
+
+ var runner = $$animation(element, 'someEvent');
+ $rootScope.$digest();
+ $$rAF.flush(); //the animation is "animating"
+
+ expect(capturedAnimation).toBeTruthy();
+ expect(runnerLog).toEqual([]);
+ element.remove();
+ expect(runnerLog).toEqual(['end']);
+ }));
+
+ it('should not end the animation when the leave animation removes the element from the DOM',
+ inject(function($$animation, $$rAF, $rootScope) {
+
+ var runner = $$animation(element, 'leave', {}, function() {
+ element.remove();
+ });
+
+ $rootScope.$digest();
+ $$rAF.flush(); //the animation is "animating"
+
+ expect(runnerLog).toEqual([]);
+ capturedAnimation.options.domOperation(); //this removes the element
+ element.remove();
+ expect(runnerLog).toEqual([]);
+ }));
+
+ it('should remove the $destroy event listener when the animation is closed',
+ inject(function($$animation, $$rAF, $rootScope) {
+
+ var addListen = spyOn(element, 'on').andCallThrough();
+ var removeListen = spyOn(element, 'off').andCallThrough();
+ var runner = $$animation(element, 'someEvent');
+
+ var args = addListen.mostRecentCall.args[0];
+ expect(args).toBe('$destroy');
+
+ runner.end();
+
+ args = removeListen.mostRecentCall.args[0];
+ expect(args).toBe('$destroy');
+ }));
+
+ it('should always sort parent-element animations to run in order of parent-to-child DOM structure',
+ inject(function($$animation, $$rAF, $rootScope) {
+
+ var child = jqLite('
');
+ var grandchild = jqLite('
');
+
+ element.append(child);
+ child.append(grandchild);
+
+ $$animation(grandchild, 'enter');
+ $$animation(child, 'enter');
+ $$animation(element, 'enter');
+
+ expect(captureLog.length).toBe(0);
+
+ $rootScope.$digest();
+
+ expect(captureLog[0].element).toBe(element);
+ expect(captureLog[1].element).toBe(child);
+ expect(captureLog[2].element).toBe(grandchild);
+ }));
+ });
+
+ describe("grouped", function() {
+ var fromElement;
+ var toElement;
+ var fromAnchors;
+ var toAnchors;
+ beforeEach(module(function($provide) {
+ fromElement = jqLite('
');
+ toElement = jqLite('
');
+ fromAnchors = [
+ jqLite('
1
'),
+ jqLite('
2
'),
+ jqLite('
3
')
+ ];
+ toAnchors = [
+ jqLite('
a
'),
+ jqLite('
b
'),
+ jqLite('
c
')
+ ];
+
+ return function($rootElement) {
+ $rootElement.append(fromElement);
+ $rootElement.append(toElement);
+ forEach(fromAnchors, function(a) {
+ fromElement.append(a);
+ });
+ forEach(toAnchors, function(a) {
+ toElement.append(a);
+ });
+ };
+ }));
+
+ it("should group animations together when they have shared anchors and a shared CSS class",
+ inject(function($$animation, $rootScope) {
+
+ fromElement.addClass('shared-class');
+ $$animation(fromElement, 'leave');
+
+ toElement.addClass('shared-class');
+ $$animation(toElement, 'enter');
+
+ fromAnchors[0].attr('ng-animate-ref', '1');
+ toAnchors[0].attr('ng-animate-ref', '1');
+ $rootScope.$digest();
+
+ expect(captureLog.length).toBe(1);
+
+ var fromAnimation = capturedAnimation.from;
+ expect(fromAnimation.element).toEqual(fromElement);
+ expect(fromAnimation.event).toBe('leave');
+
+ var toAnimation = capturedAnimation.to;
+ expect(toAnimation.element).toBe(toElement);
+ expect(toAnimation.event).toBe('enter');
+
+ var fromElm = fromAnchors[0];
+ var toElm = toAnchors[0];
+
+ var anchors = capturedAnimation.anchors[0];
+ assertCompareNodes(fromElm, anchors['out']);
+ assertCompareNodes(toElm, anchors['in']);
+ }));
+
+ it("should group animations together and properly match up multiple anchors based on their references",
+ inject(function($$animation, $rootScope) {
+
+ var attr = 'ng-animate-ref';
+
+ fromAnchors[0].attr(attr, '1');
+ fromAnchors[1].attr(attr, '2');
+ fromAnchors[2].attr(attr, '3');
+
+ toAnchors[0].attr(attr, '1');
+ toAnchors[1].attr(attr, '3');
+ toAnchors[2].attr(attr, '2');
+
+ fromElement.addClass('shared-class');
+ $$animation(fromElement, 'leave');
+
+ toElement.addClass('shared-class');
+ $$animation(toElement, 'enter');
+
+ $rootScope.$digest();
+
+ var anchors = capturedAnimation.anchors;
+ assertCompareNodes(fromAnchors[0], anchors[0]['out']);
+ assertCompareNodes(toAnchors[0], anchors[0]['in']);
+
+ assertCompareNodes(fromAnchors[1], anchors[1]['out']);
+ assertCompareNodes(toAnchors[2], anchors[1]['in']);
+
+ assertCompareNodes(fromAnchors[2], anchors[2]['out']);
+ assertCompareNodes(toAnchors[1], anchors[2]['in']);
+ }));
+
+ it("should group animations together on the from and to elements if their both contain matching anchors",
+ inject(function($$animation, $rootScope) {
+
+ fromElement.addClass('shared-class');
+ fromElement.attr('ng-animate-ref', '1');
+ $$animation(fromElement, 'leave');
+
+ toElement.addClass('shared-class');
+ toElement.attr('ng-animate-ref', '1');
+ $$animation(toElement, 'enter');
+
+ $rootScope.$digest();
+
+ var anchors = capturedAnimation.anchors[0];
+ assertCompareNodes(fromElement, anchors['out']);
+ assertCompareNodes(toElement, anchors['in']);
+ }));
+
+ it("should not group animations into an anchored animation if enter/leave events are NOT used",
+ inject(function($$animation, $rootScope) {
+
+ fromElement.addClass('shared-class');
+ fromElement.attr('ng-animate-ref', '1');
+ $$animation(fromElement, 'addClass', {
+ addClass: 'red'
+ });
+
+ toElement.addClass('shared-class');
+ toElement.attr('ng-animate-ref', '1');
+ $$animation(toElement, 'removeClass', {
+ removeClass: 'blue'
+ });
+
+ $rootScope.$digest();
+ expect(captureLog.length).toBe(2);
+ }));
+
+ it("should not group animations together if a matching pair of anchors is not detected",
+ inject(function($$animation, $rootScope) {
+
+ fromElement.addClass('shared-class');
+ $$animation(fromElement, 'leave');
+
+ toElement.addClass('shared-class');
+ $$animation(toElement, 'enter');
+
+ fromAnchors[0].attr('ng-animate-ref', '6');
+ toAnchors[0].attr('ng-animate-ref', '3');
+ $rootScope.$digest();
+
+ expect(captureLog.length).toBe(2);
+ }));
+
+ it("should not group animations together if a matching CSS class is not detected",
+ inject(function($$animation, $rootScope) {
+
+ fromElement.addClass('even-class');
+ $$animation(fromElement, 'leave');
+
+ toElement.addClass('odd-class');
+ $$animation(toElement, 'enter');
+
+ fromAnchors[0].attr('ng-animate-ref', '9');
+ toAnchors[0].attr('ng-animate-ref', '9');
+ $rootScope.$digest();
+
+ expect(captureLog.length).toBe(2);
+ }));
+
+ it("should expose the shared CSS class in the options provided to the driver",
+ inject(function($$animation, $rootScope) {
+
+ fromElement.addClass('fresh-class');
+ $$animation(fromElement, 'leave');
+
+ toElement.addClass('fresh-class');
+ $$animation(toElement, 'enter');
+
+ fromAnchors[0].attr('ng-animate-ref', '9');
+ toAnchors[0].attr('ng-animate-ref', '9');
+ $rootScope.$digest();
+
+ expect(capturedAnimation.classes).toBe('fresh-class');
+ }));
+
+ it("should update the runner methods to the grouped runner methods handled by the driver",
+ inject(function($$animation, $rootScope) {
+
+ fromElement.addClass('group-1');
+ var runner1 = $$animation(fromElement, 'leave');
+
+ toElement.addClass('group-1');
+ var runner2 = $$animation(toElement, 'enter');
+
+ expect(runner1).not.toBe(runner2);
+
+ fromAnchors[0].attr('ng-animate-ref', 'abc');
+ toAnchors[0].attr('ng-animate-ref', 'abc');
+ $rootScope.$digest();
+
+ expect(runner1).not.toBe(runner2);
+ expect(runner1.end).toBe(runner2.end);
+ expect(runner1.cancel).toBe(runner2.cancel);
+ }));
+
+ they("should end the animation if the $prop element is prematurely removed from the DOM during the animation", ['from', 'to'], function(event) {
+ inject(function($$animation, $rootScope) {
+ fromElement.addClass('group-1');
+ $$animation(fromElement, 'leave');
+
+ toElement.addClass('group-1');
+ $$animation(toElement, 'enter');
+
+ fromAnchors[0].attr('ng-animate-ref', 'abc');
+ toAnchors[0].attr('ng-animate-ref', 'abc');
+ $rootScope.$digest();
+
+ expect(runnerLog).toEqual([]);
+
+ ('from' ? fromElement : toElement).remove();
+ expect(runnerLog).toEqual(['end']);
+ });
+ });
+
+ it("should not end the animation when the `from` animation calls its own leave dom operation",
+ inject(function($$animation, $rootScope, $$rAF) {
+
+ fromElement.addClass('group-1');
+ var elementRemoved = false;
+ $$animation(fromElement, 'leave', {
+ domOperation: function() {
+ elementRemoved = true;
+ fromElement.remove();
+ }
+ });
+
+ toElement.addClass('group-1');
+ $$animation(toElement, 'enter');
+
+ fromAnchors[0].attr('ng-animate-ref', 'abc');
+ toAnchors[0].attr('ng-animate-ref', 'abc');
+ $rootScope.$digest();
+
+ var leaveAnimation = capturedAnimation.from;
+ expect(leaveAnimation.event).toBe('leave');
+
+ // this removes the element and this code is run normally
+ // by the driver when it is time for the element to be removed
+ leaveAnimation.options.domOperation();
+
+ expect(elementRemoved).toBe(true);
+ expect(runnerLog).toEqual([]);
+ }));
+
+ it("should not end the animation if any of the anchor elements are removed from the DOM during the animation",
+ inject(function($$animation, $rootScope, $$rAF) {
+
+ fromElement.addClass('group-1');
+ var elementRemoved = false;
+ $$animation(fromElement, 'leave', {}, function() {
+ elementRemoved = true;
+ fromElement.remove();
+ });
+
+ toElement.addClass('group-1');
+ $$animation(toElement, 'enter');
+
+ fromAnchors[0].attr('ng-animate-ref', 'abc');
+ toAnchors[0].attr('ng-animate-ref', 'abc');
+ $rootScope.$digest();
+
+ fromAnchors[0].remove();
+ toAnchors[0].remove();
+
+ expect(runnerLog).toEqual([]);
+ }));
+
+ it('should prepare a parent-element animation to run first before the anchored animation',
+ inject(function($$animation, $$rAF, $rootScope, $rootElement) {
+
+ fromAnchors[0].attr('ng-animate-ref', 'shared');
+ toAnchors[0].attr('ng-animate-ref', 'shared');
+
+ var parent = jqLite('
');
+ parent.append(fromElement);
+ parent.append(toElement);
+ $rootElement.append(parent);
+
+ fromElement.addClass('group-1');
+ toElement.addClass('group-1');
+
+ // issued first
+ $$animation(toElement, 'enter');
+ $$animation(fromElement, 'leave');
+
+ // issued second
+ $$animation(parent, 'addClass', { addClass: 'red' });
+
+ expect(captureLog.length).toBe(0);
+
+ $rootScope.$digest();
+
+ expect(captureLog[0].element).toBe(parent);
+ expect(captureLog[1].from.element).toBe(fromElement);
+ expect(captureLog[1].to.element).toBe(toElement);
+ }));
+ });
+ });
+
+ describe('[options]', function() {
+ var runner;
+ var defered;
+ var parent;
+ var mockedDriverFn;
+ var mockedPlayerFn;
+
+ beforeEach(module(function($$animationProvider, $provide) {
+ $$animationProvider.drivers.push('mockedTestDriver');
+ $provide.factory('mockedTestDriver', function() {
+ return mockedDriverFn;
+ });
+
+ element = jqLite('
');
+ parent = jqLite('
');
+
+ return function($$AnimateRunner, $q, $rootElement, $$body) {
+ $$body.append($rootElement);
+ $rootElement.append(parent);
+
+ mockedDriverFn = function(element, method, options, domOperation) {
+ return {
+ start: function() {
+ return runner = new $$AnimateRunner();
+ }
+ };
+ };
+ };
+ }));
+
+ it('should temporarily assign the provided CSS class for the duration of the animation',
+ inject(function($rootScope, $$animation) {
+
+ parent.append(element);
+
+ $$animation(element, 'enter', {
+ tempClasses: 'temporary fudge'
+ });
+ $rootScope.$digest();
+
+ expect(element).toHaveClass('temporary');
+ expect(element).toHaveClass('fudge');
+
+ runner.end();
+ $rootScope.$digest();
+
+ expect(element).not.toHaveClass('temporary');
+ expect(element).not.toHaveClass('fudge');
+ }));
+
+ it('should add and remove the ng-animate CSS class when the animation is active',
+ inject(function($$animation, $rootScope) {
+
+ parent.append(element);
+
+ $$animation(element, 'enter');
+ $rootScope.$digest();
+ expect(element).toHaveClass('ng-animate');
+
+ runner.end();
+ $rootScope.$digest();
+
+ expect(element).not.toHaveClass('ng-animate');
+ }));
+
+
+ it('should apply the `ng-animate` and temporary CSS classes before the driver is invoked', function() {
+ var capturedElementClasses;
+
+ parent.append(element);
+
+ module(function($provide) {
+ $provide.factory('mockedTestDriver', function() {
+ return function(details) {
+ capturedElementClasses = details.element.attr('class');
+ };
+ });
+ });
+
+ inject(function($$animation, $rootScope) {
+ parent.append(element);
+
+ $$animation(element, 'enter', {
+ tempClasses: 'temp-class-name'
+ });
+ $rootScope.$digest();
+
+ expect(capturedElementClasses).toMatch(/\bng-animate\b/);
+ expect(capturedElementClasses).toMatch(/\btemp-class-name\b/);
+ });
+ });
+
+ it('should perform the DOM operation at the end of the animation if the driver doesn\'t run it already',
+ inject(function($$animation, $rootScope) {
+
+ parent.append(element);
+
+ var domOperationFired = false;
+ $$animation(element, 'enter', {
+ domOperation: function() {
+ domOperationFired = true;
+ }
+ });
+
+ $rootScope.$digest();
+
+ expect(domOperationFired).toBeFalsy();
+ runner.end();
+ $rootScope.$digest();
+
+ expect(domOperationFired).toBeTruthy();
+ }));
+
+ it('should still apply the `from` and `to` styling even if no driver was detected', function() {
+ module(function($$animationProvider) {
+ $$animationProvider.drivers.length = 0;
+ });
+ inject(function($$animation, $rootScope) {
+ $$animation(element, 'event', {
+ from: { background: 'red' },
+ to: { background: 'blue' }
+ });
+
+ expect(element.css('background')).toContain('blue');
+ });
+ });
+
+ it('should still apply the `from` and `to` styling even if the driver does not do the job', function() {
+ module(function($$animationProvider, $provide) {
+ $$animationProvider.drivers[0] = 'dumbDriver';
+ $provide.factory('dumbDriver', function($q) {
+ return function stepFn() {
+ return $q.when(true);
+ };
+ });
+ });
+ inject(function($$animation, $rootScope, $$rAF) {
+ element.addClass('four');
+
+ var completed = false;
+ $$animation(element, 'event', {
+ from: { background: 'red' },
+ to: { background: 'blue', 'font-size': '50px' }
+ }).then(function() {
+ completed = true;
+ });
+
+ $rootScope.$digest(); //runs the animation
+ $rootScope.$digest(); //flushes the step code
+ $$rAF.flush(); //runs the $$animation promise
+ $rootScope.$digest(); //the runner promise
+
+ expect(completed).toBe(true);
+ expect(element.css('background')).toContain('blue');
+ expect(element.css('font-size')).toBe('50px');
+ });
+ });
+
+ it('should still resolve the `addClass` and `removeClass` classes even if no driver was detected', function() {
+ module(function($$animationProvider) {
+ $$animationProvider.drivers.length = 0;
+ });
+ inject(function($$animation, $rootScope) {
+ element.addClass('four');
+
+ $$animation(element, 'event', {
+ addClass: 'one two three',
+ removeClass: 'four'
+ });
+
+ expect(element).toHaveClass('one');
+ expect(element).toHaveClass('two');
+ expect(element).toHaveClass('three');
+ expect(element).not.toHaveClass('four');
+ });
+ });
+
+ it('should still resolve the `addClass` and `removeClass` classes even if the driver does not do the job', function() {
+ module(function($$animationProvider, $provide) {
+ $$animationProvider.drivers[0] = 'dumbDriver';
+ $provide.factory('dumbDriver', function($$AnimateRunner) {
+ return function initFn() {
+ return function stepFn() {
+ return new $$AnimateRunner();
+ };
+ };
+ });
+ });
+ inject(function($$animation, $rootScope, $$rAF) {
+ element.addClass('four');
+
+ var completed = false;
+ var runner = $$animation(element, 'event', {
+ addClass: 'one two three',
+ removeClass: 'four'
+ });
+ runner.then(function() {
+ completed = true;
+ });
+
+ $rootScope.$digest(); //runs the animation
+ $rootScope.$digest(); //flushes the step code
+
+ runner.end();
+ $$rAF.flush(); //runs the $$animation promise
+ $rootScope.$digest(); //the runner promise
+
+ expect(completed).toBe(true);
+ expect(element).toHaveClass('one');
+ expect(element).toHaveClass('two');
+ expect(element).toHaveClass('three');
+ expect(element).not.toHaveClass('four');
+ });
+ });
+ });
+});
diff --git a/test/ngAnimate/bodySpec.js b/test/ngAnimate/bodySpec.js
new file mode 100644
index 000000000000..f87dabf1e355
--- /dev/null
+++ b/test/ngAnimate/bodySpec.js
@@ -0,0 +1,9 @@
+'use strict';
+
+describe('$$body', function() {
+ beforeEach(module('ngAnimate'));
+
+ it("should inject $document", inject(function($$body, $document) {
+ expect($$body).toEqual(jqLite($document[0].body));
+ }));
+});
diff --git a/test/ngAnimate/integrationSpec.js b/test/ngAnimate/integrationSpec.js
new file mode 100644
index 000000000000..b56bb78c4866
--- /dev/null
+++ b/test/ngAnimate/integrationSpec.js
@@ -0,0 +1,572 @@
+'use strict';
+
+describe('ngAnimate integration tests', function() {
+
+ beforeEach(module('ngAnimate'));
+
+ var element, html, ss;
+ beforeEach(module(function() {
+ return function($rootElement, $document, $$body, $window, $animate) {
+ $animate.enabled(true);
+
+ ss = createMockStyleSheet($document, $window);
+
+ var body = $$body;
+ html = function(element) {
+ body.append($rootElement);
+ $rootElement.append(element);
+ };
+ };
+ }));
+
+ afterEach(function() {
+ dealoc(element);
+ ss.destroy();
+ });
+
+ describe('CSS animations', function() {
+ if (!browserSupportsCssAnimations()) return;
+
+ they('should render an $prop animation',
+ ['enter', 'leave', 'move', 'addClass', 'removeClass', 'setClass'], function(event) {
+
+ inject(function($animate, $compile, $rootScope, $rootElement, $$rAF) {
+ element = jqLite('
');
+ $compile(element)($rootScope);
+
+ var className = 'klass';
+ var addClass, removeClass;
+ var parent = jqLite('
');
+ html(parent);
+
+ var setupClass, activeClass;
+ var args;
+ var classRuleSuffix = '';
+
+ switch (event) {
+ case 'enter':
+ case 'move':
+ setupClass = 'ng-' + event;
+ activeClass = 'ng-' + event + '-active';
+ args = [element, parent];
+ break;
+
+ case 'leave':
+ parent.append(element);
+ setupClass = 'ng-' + event;
+ activeClass = 'ng-' + event + '-active';
+ args = [element];
+ break;
+
+ case 'addClass':
+ parent.append(element);
+ classRuleSuffix = '.add';
+ setupClass = className + '-add';
+ activeClass = className + '-add-active';
+ addClass = className;
+ args = [element, className];
+ break;
+
+ case 'removeClass':
+ parent.append(element);
+ setupClass = className + '-remove';
+ activeClass = className + '-remove-active';
+ element.addClass(className);
+ args = [element, className];
+ break;
+
+ case 'setClass':
+ parent.append(element);
+ addClass = className;
+ removeClass = 'removing-class';
+ setupClass = addClass + '-add ' + removeClass + '-remove';
+ activeClass = addClass + '-add-active ' + removeClass + '-remove-active';
+ element.addClass(removeClass);
+ args = [element, addClass, removeClass];
+ break;
+ }
+
+ ss.addRule('.animate-me', 'transition:2s linear all;');
+
+ var runner = $animate[event].apply($animate, args);
+ $rootScope.$digest();
+
+ var animationCompleted = false;
+ runner.then(function() {
+ animationCompleted = true;
+ });
+
+ expect(element).toHaveClass(setupClass);
+ $$rAF.flush();
+ expect(element).toHaveClass(activeClass);
+
+ browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 2 });
+
+ expect(element).not.toHaveClass(setupClass);
+ expect(element).not.toHaveClass(activeClass);
+
+ $rootScope.$digest();
+
+ expect(animationCompleted).toBe(true);
+ });
+ });
+
+ it('should not throw an error if the element is orphaned before the CSS animation starts',
+ inject(function($rootScope, $rootElement, $animate, $$rAF) {
+
+ ss.addRule('.animate-me', 'transition:2s linear all;');
+
+ var parent = jqLite('
');
+ html(parent);
+
+ var element = jqLite('
DOING
');
+ parent.append(element);
+
+ $animate.addClass(parent, 'on');
+ $animate.addClass(element, 'on');
+ $rootScope.$digest();
+
+ // this will run the first class-based animation
+ $$rAF.flush();
+
+ element.remove();
+
+ expect(function() {
+ $$rAF.flush();
+ }).not.toThrow();
+
+ dealoc(element);
+ }));
+
+ it('should always synchronously add css classes in order for child animations to animate properly',
+ inject(function($animate, $compile, $rootScope, $rootElement, $$rAF, $document) {
+
+ ss.addRule('.animations-enabled .animate-me.ng-enter', 'transition:2s linear all;');
+
+ element = jqLite('
');
+ var child = jqLite('
');
+
+ element.append(child);
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ $compile(element)($rootScope);
+
+ $rootScope.exp = true;
+ $rootScope.$digest();
+
+ child = element.find('div');
+
+ expect(element).toHaveClass('animations-enabled');
+ expect(child).toHaveClass('ng-enter');
+
+ $$rAF.flush();
+
+ expect(child).toHaveClass('ng-enter-active');
+
+ browserTrigger(child, 'transitionend', { timeStamp: Date.now(), elapsedTime: 2 });
+
+ expect(child).not.toHaveClass('ng-enter-active');
+ expect(child).not.toHaveClass('ng-enter');
+ }));
+
+ it('should synchronously add/remove ng-class expressions in time for other animations to run on the same element',
+ inject(function($animate, $compile, $rootScope, $rootElement, $$rAF, $document) {
+
+ ss.addRule('.animate-me.ng-enter.on', 'transition:2s linear all;');
+
+ element = jqLite('
');
+
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ $compile(element)($rootScope);
+
+ $rootScope.exp = true;
+ $rootScope.$digest();
+ $$rAF.flush();
+
+ var child = element.find('div');
+
+ expect(child).not.toHaveClass('on');
+ expect(child).not.toHaveClass('ng-enter');
+
+ $rootScope.exp = false;
+ $rootScope.$digest();
+
+ $rootScope.exp = true;
+ $rootScope.exp2 = true;
+ $rootScope.$digest();
+
+ child = element.find('div');
+
+ expect(child).toHaveClass('on');
+ expect(child).toHaveClass('ng-enter');
+
+ $$rAF.flush();
+
+ expect(child).toHaveClass('ng-enter-active');
+
+ browserTrigger(child, 'transitionend', { timeStamp: Date.now(), elapsedTime: 2 });
+
+ expect(child).not.toHaveClass('ng-enter-active');
+ expect(child).not.toHaveClass('ng-enter');
+ }));
+
+ it('should animate ng-class and a structural animation in parallel on the same element',
+ inject(function($animate, $compile, $rootScope, $rootElement, $$rAF, $document) {
+
+ ss.addRule('.animate-me.ng-enter', 'transition:2s linear all;');
+ ss.addRule('.animate-me.expand', 'transition:5s linear all; font-size:200px;');
+
+ element = jqLite('
');
+
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ $compile(element)($rootScope);
+
+ $rootScope.exp = true;
+ $rootScope.exp2 = true;
+ $rootScope.$digest();
+
+ var child = element.find('div');
+
+ expect(child).toHaveClass('ng-enter');
+ expect(child).toHaveClass('expand-add');
+ expect(child).toHaveClass('expand');
+
+ $$rAF.flush();
+
+ expect(child).toHaveClass('ng-enter-active');
+ expect(child).toHaveClass('expand-add-active');
+
+ browserTrigger(child, 'transitionend', { timeStamp: Date.now(), elapsedTime: 2 });
+
+ expect(child).not.toHaveClass('ng-enter-active');
+ expect(child).not.toHaveClass('ng-enter');
+ expect(child).not.toHaveClass('expand-add-active');
+ expect(child).not.toHaveClass('expand-add');
+ }));
+
+ it('should only issue a reflow for each parent CSS class change that contains ready-to-fire child animations', function() {
+ module('ngAnimateMock');
+ inject(function($animate, $compile, $rootScope, $rootElement, $$rAF, $document) {
+ element = jqLite(
+ '
' +
+ '
' +
+ '
' +
+ '{{ item }}' +
+ '
' +
+ '
' +
+ '
'
+ );
+
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ $compile(element)($rootScope);
+ $rootScope.$digest();
+ expect($animate.reflows).toBe(0);
+
+ $rootScope.exp = true;
+ $rootScope.items = [1,2,3,4,5,6,7,8,9,10];
+
+ $rootScope.$digest();
+ expect($animate.reflows).toBe(2);
+ });
+ });
+
+ it('should issue a reflow for each parent class-based animation that contains active child animations', function() {
+ module('ngAnimateMock');
+ inject(function($animate, $compile, $rootScope, $rootElement, $$rAF, $document) {
+ element = jqLite(
+ '
' +
+ '
' +
+ '
' +
+ '
'
+ );
+
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ $compile(element)($rootScope);
+ $rootScope.$digest();
+ expect($animate.reflows).toBe(0);
+
+ $rootScope.exp = true;
+ $rootScope.$digest();
+ expect($animate.reflows).toBe(2);
+ });
+ });
+
+ it('should only issue one reflow for class-based animations if none of them have children with queued animations', function() {
+ module('ngAnimateMock');
+ inject(function($animate, $compile, $rootScope, $rootElement, $$rAF, $document) {
+ element = jqLite(
+ '
' +
+ '
' +
+ '
'
+ );
+
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ $compile(element)($rootScope);
+ $rootScope.$digest();
+ expect($animate.reflows).toBe(0);
+
+ $rootScope.exp = true;
+ $rootScope.$digest();
+ expect($animate.reflows).toBe(1);
+
+ $rootScope.exp2 = true;
+ $rootScope.$digest();
+ expect($animate.reflows).toBe(2);
+ });
+ });
+
+ it('should always issue atleast one reflow incase there are no parent class-based animations', function() {
+ module('ngAnimateMock');
+ inject(function($animate, $compile, $rootScope, $rootElement, $$rAF, $document) {
+ element = jqLite(
+ '
' +
+ '{{ item }}' +
+ '
'
+ );
+
+ $rootElement.append(element);
+ jqLite($document[0].body).append($rootElement);
+
+ $compile(element)($rootScope);
+ $rootScope.$digest();
+ expect($animate.reflows).toBe(0);
+
+ $rootScope.exp = true;
+ $rootScope.items = [1,2,3,4,5,6,7,8,9,10];
+ $rootScope.$digest();
+
+ expect($animate.reflows).toBe(1);
+ });
+ });
+ });
+
+ describe('JS animations', function() {
+ they('should render an $prop animation',
+ ['enter', 'leave', 'move', 'addClass', 'removeClass', 'setClass'], function(event) {
+
+ var endAnimation;
+ var animateCompleteCallbackFired = true;
+
+ module(function($animateProvider) {
+ $animateProvider.register('.animate-me', function() {
+ var animateFactory = {};
+ animateFactory[event] = function(element, addClass, removeClass, done) {
+ endAnimation = arguments[arguments.length - 2]; // the done method is the 2nd last one
+ return function(status) {
+ animateCompleteCallbackFired = status === false;
+ };
+ };
+ return animateFactory;
+ });
+ });
+
+ inject(function($animate, $compile, $rootScope, $rootElement, $$rAF) {
+ element = jqLite('
');
+ $compile(element)($rootScope);
+
+ var className = 'klass';
+ var addClass, removeClass;
+ var parent = jqLite('
');
+ html(parent);
+
+ var args;
+ switch (event) {
+ case 'enter':
+ case 'move':
+ args = [element, parent];
+ break;
+
+ case 'leave':
+ parent.append(element);
+ args = [element];
+ break;
+
+ case 'addClass':
+ parent.append(element);
+ args = [element, className];
+ break;
+
+ case 'removeClass':
+ parent.append(element);
+ element.addClass(className);
+ args = [element, className];
+ break;
+
+ case 'setClass':
+ parent.append(element);
+ addClass = className;
+ removeClass = 'removing-class';
+ element.addClass(removeClass);
+ args = [element, addClass, removeClass];
+ break;
+ }
+
+ var runner = $animate[event].apply($animate, args);
+ var animationCompleted = false;
+ runner.then(function() {
+ animationCompleted = true;
+ });
+
+ $rootScope.$digest();
+
+ expect(isFunction(endAnimation)).toBe(true);
+
+ endAnimation();
+ $$rAF.flush();
+ expect(animateCompleteCallbackFired).toBe(true);
+
+ $rootScope.$digest();
+ expect(animationCompleted).toBe(true);
+ });
+ });
+
+ they('should not wait for a parent\'s classes to resolve if a $prop is animation used for children',
+ ['beforeAddClass', 'beforeRemoveClass', 'beforeSetClass'], function(phase) {
+
+ var capturedChildClasses;
+ var endParentAnimationFn;
+
+ module(function($animateProvider) {
+ $animateProvider.register('.parent-man', function() {
+ var animateFactory = {};
+ animateFactory[phase] = function(element, addClass, removeClass, done) {
+ // this will wait until things are over
+ endParentAnimationFn = done;
+ };
+ return animateFactory;
+ });
+
+ $animateProvider.register('.child-man', function() {
+ return {
+ enter: function(element, done) {
+ capturedChildClasses = element.parent().attr('class');
+ done();
+ }
+ };
+ });
+ });
+
+ inject(function($animate, $compile, $rootScope, $rootElement) {
+ element = jqLite('
');
+ var child = jqLite('
');
+
+ html(element);
+ $compile(element)($rootScope);
+
+ $animate.enter(child, element);
+ switch (phase) {
+ case 'beforeAddClass':
+ $animate.addClass(element, 'cool');
+ break;
+
+ case 'beforeSetClass':
+ $animate.setClass(element, 'cool');
+ break;
+
+ case 'beforeRemoveClass':
+ element.addClass('cool');
+ $animate.removeClass(element, 'cool');
+ break;
+ }
+
+ $rootScope.$digest();
+
+ expect(endParentAnimationFn).toBeTruthy();
+
+ // the spaces are used so that ` cool ` can be matched instead
+ // of just a substring like `cool-add`.
+ var safeClassMatchString = ' ' + capturedChildClasses + ' ';
+ if (phase === 'beforeRemoveClass') {
+ expect(safeClassMatchString).toContain(' cool ');
+ } else {
+ expect(safeClassMatchString).not.toContain(' cool ');
+ }
+ });
+ });
+
+ they('should have the parent\'s classes already applied in time for the children if $prop is used',
+ ['addClass', 'removeClass', 'setClass'], function(phase) {
+
+ var capturedChildClasses;
+ var endParentAnimationFn;
+
+ module(function($animateProvider) {
+ $animateProvider.register('.parent-man', function() {
+ var animateFactory = {};
+ animateFactory[phase] = function(element, addClass, removeClass, done) {
+ // this will wait until things are over
+ endParentAnimationFn = done;
+ };
+ return animateFactory;
+ });
+
+ $animateProvider.register('.child-man', function() {
+ return {
+ enter: function(element, done) {
+ capturedChildClasses = element.parent().attr('class');
+ done();
+ }
+ };
+ });
+ });
+
+ inject(function($animate, $compile, $rootScope, $rootElement) {
+ element = jqLite('
');
+ var child = jqLite('
');
+
+ html(element);
+ $compile(element)($rootScope);
+
+ $animate.enter(child, element);
+ switch (phase) {
+ case 'addClass':
+ $animate.addClass(element, 'cool');
+ break;
+
+ case 'setClass':
+ $animate.setClass(element, 'cool');
+ break;
+
+ case 'removeClass':
+ element.addClass('cool');
+ $animate.removeClass(element, 'cool');
+ break;
+ }
+
+ $rootScope.$digest();
+
+ expect(endParentAnimationFn).toBeTruthy();
+
+ // the spaces are used so that ` cool ` can be matched instead
+ // of just a substring like `cool-add`.
+ var safeClassMatchString = ' ' + capturedChildClasses + ' ';
+ if (phase === 'removeClass') {
+ expect(safeClassMatchString).not.toContain(' cool ');
+ } else {
+ expect(safeClassMatchString).toContain(' cool ');
+ }
+ });
+ });
+ });
+});
diff --git a/test/ngAria/ariaSpec.js b/test/ngAria/ariaSpec.js
index 905196340f8a..864f856ed271 100644
--- a/test/ngAria/ariaSpec.js
+++ b/test/ngAria/ariaSpec.js
@@ -5,6 +5,10 @@ describe('$aria', function() {
beforeEach(module('ngAria'));
+ afterEach(function() {
+ dealoc(element);
+ });
+
function injectScopeAndCompiler() {
return inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
@@ -12,7 +16,7 @@ describe('$aria', function() {
});
}
- function compileInput(inputHtml) {
+ function compileElement(inputHtml) {
element = $compile(inputHtml)(scope);
scope.$digest();
}
@@ -21,7 +25,7 @@ describe('$aria', function() {
beforeEach(injectScopeAndCompiler);
it('should attach aria-hidden to ng-show', function() {
- compileInput('
');
+ compileElement('
');
scope.$apply('val = false');
expect(element.attr('aria-hidden')).toBe('true');
@@ -30,7 +34,7 @@ describe('$aria', function() {
});
it('should attach aria-hidden to ng-hide', function() {
- compileInput('
');
+ compileElement('
');
scope.$apply('val = false');
expect(element.attr('aria-hidden')).toBe('false');
@@ -39,7 +43,7 @@ describe('$aria', function() {
});
it('should not change aria-hidden if it is already present on ng-show', function() {
- compileInput('
');
+ compileElement('
');
expect(element.attr('aria-hidden')).toBe('userSetValue');
scope.$apply('val = true');
@@ -47,12 +51,37 @@ describe('$aria', function() {
});
it('should not change aria-hidden if it is already present on ng-hide', function() {
- compileInput('
');
+ compileElement('
');
expect(element.attr('aria-hidden')).toBe('userSetValue');
scope.$apply('val = true');
expect(element.attr('aria-hidden')).toBe('userSetValue');
});
+
+ it('should always set aria-hidden to a boolean value', function() {
+ compileElement('
');
+
+ scope.$apply('val = "test angular"');
+ expect(element.attr('aria-hidden')).toBe('true');
+
+ scope.$apply('val = null');
+ expect(element.attr('aria-hidden')).toBe('false');
+
+ scope.$apply('val = {}');
+ expect(element.attr('aria-hidden')).toBe('true');
+
+
+ compileElement('
');
+
+ scope.$apply('val = "test angular"');
+ expect(element.attr('aria-hidden')).toBe('false');
+
+ scope.$apply('val = null');
+ expect(element.attr('aria-hidden')).toBe('true');
+
+ scope.$apply('val = {}');
+ expect(element.attr('aria-hidden')).toBe('false');
+ });
});
@@ -64,10 +93,10 @@ describe('$aria', function() {
it('should not attach aria-hidden', function() {
scope.$apply('val = false');
- compileInput('
');
+ compileElement('
');
expect(element.attr('aria-hidden')).toBeUndefined();
- compileInput('
');
+ compileElement('
');
expect(element.attr('aria-hidden')).toBeUndefined();
});
});
@@ -76,7 +105,7 @@ describe('$aria', function() {
beforeEach(injectScopeAndCompiler);
it('should attach itself to input type="checkbox"', function() {
- compileInput('
');
+ compileElement('
');
scope.$apply('val = true');
expect(element.attr('aria-checked')).toBe('true');
@@ -151,27 +180,39 @@ describe('$aria', function() {
});
it('should attach itself to role="radio"', function() {
- scope.$apply("val = 'one'");
- compileInput('
');
+ scope.val = 'one';
+ compileElement('
');
expect(element.attr('aria-checked')).toBe('true');
+
+ scope.$apply("val = 'two'");
+ expect(element.attr('aria-checked')).toBe('false');
});
it('should attach itself to role="checkbox"', function() {
scope.val = true;
- compileInput('
');
+ compileElement('
');
expect(element.attr('aria-checked')).toBe('true');
+
+ scope.$apply('val = false');
+ expect(element.attr('aria-checked')).toBe('false');
});
it('should attach itself to role="menuitemradio"', function() {
scope.val = 'one';
- compileInput('
');
+ compileElement('
');
expect(element.attr('aria-checked')).toBe('true');
+
+ scope.$apply("val = 'two'");
+ expect(element.attr('aria-checked')).toBe('false');
});
it('should attach itself to role="menuitemcheckbox"', function() {
scope.val = true;
- compileInput('
');
+ compileElement('
');
expect(element.attr('aria-checked')).toBe('true');
+
+ scope.$apply('val = false');
+ expect(element.attr('aria-checked')).toBe('false');
});
it('should not attach itself if an aria-checked value is already present', function() {
@@ -188,6 +229,50 @@ describe('$aria', function() {
});
});
+ describe('roles for custom inputs', function() {
+ beforeEach(injectScopeAndCompiler);
+
+ it('should add missing role="button" to custom input', function() {
+ compileElement('
');
+ expect(element.attr('role')).toBe('button');
+ });
+
+ it('should not add role="button" to anchor', function() {
+ compileElement('
');
+ expect(element.attr('role')).not.toBe('button');
+ });
+
+ it('should add missing role="checkbox" to custom input', function() {
+ compileElement('
');
+ expect(element.attr('role')).toBe('checkbox');
+ });
+
+ it('should not add a role to a native checkbox', function() {
+ compileElement('
');
+ expect(element.attr('role')).toBe(undefined);
+ });
+
+ it('should add missing role="radio" to custom input', function() {
+ compileElement('
');
+ expect(element.attr('role')).toBe('radio');
+ });
+
+ it('should not add a role to a native radio button', function() {
+ compileElement('