'use strict';
describe('ngAnimate integration tests', function() {
beforeEach(module('ngAnimate'));
beforeEach(module('ngAnimateMock'));
var element, html, ss;
beforeEach(module(function() {
return function($rootElement, $document, $animate) {
$animate.enabled(true);
ss = createMockStyleSheet($document);
var body = jqLite($document[0].body);
html = function(element) {
body.append($rootElement);
$rootElement.append(element);
};
};
}));
afterEach(function() {
dealoc(element);
ss.destroy();
});
it('should cancel a running and started removeClass animation when a follow-up addClass animation adds the same class',
inject(function($animate, $rootScope, $$rAF, $document, $rootElement) {
jqLite($document[0].body).append($rootElement);
element = jqLite('
');
$rootElement.append(element);
element.addClass('active-class');
var runner = $animate.removeClass(element, 'active-class');
$rootScope.$digest();
var doneHandler = jasmine.createSpy('addClass done');
runner.done(doneHandler);
$$rAF.flush(); // Trigger the actual animation
expect(doneHandler).not.toHaveBeenCalled();
$animate.addClass(element, 'active-class');
$rootScope.$digest();
// Cancelling the removeClass animation triggers the done callback
expect(doneHandler).toHaveBeenCalled();
}));
it('should remove a class that is currently being added by a running animation when another class is added in before in the same digest',
inject(function($animate, $rootScope, $$rAF, $document, $rootElement) {
jqLite($document[0].body).append($rootElement);
element = jqLite('');
$rootElement.append(element);
var runner = $animate.addClass(element, 'red');
$rootScope.$digest();
$animate.addClass(element, 'blue');
$animate.removeClass(element, 'red');
$rootScope.$digest();
$$rAF.flush();
expect(element).not.toHaveClass('red');
expect(element).toHaveClass('blue');
}));
it('should add a class that is currently being removed by a running animation when another class is removed before in the same digest',
inject(function($animate, $rootScope, $$rAF, $document, $rootElement) {
jqLite($document[0].body).append($rootElement);
element = jqLite('');
$rootElement.append(element);
element.addClass('red blue');
var runner = $animate.removeClass(element, 'red');
$rootScope.$digest();
$animate.removeClass(element, 'blue');
$animate.addClass(element, 'red');
$rootScope.$digest();
$$rAF.flush();
expect(element).not.toHaveClass('blue');
expect(element).toHaveClass('red');
}));
describe('CSS animations', function() {
if (!browserSupportsCssAnimations()) return;
it('should only create a single copy of the provided animation options',
inject(function($rootScope, $rootElement, $animate) {
ss.addRule('.animate-me', 'transition:2s linear all;');
var element = jqLite('');
html(element);
var myOptions = {to: { 'color': 'red' }};
var spy = spyOn(window, 'copy');
expect(spy).not.toHaveBeenCalled();
var animation = $animate.leave(element, myOptions);
$rootScope.$digest();
$animate.flush();
expect(spy).toHaveBeenCalledOnce();
dealoc(element);
}));
they('should render an $prop animation',
['enter', 'leave', 'move', 'addClass', 'removeClass', 'setClass'], function(event) {
inject(function($animate, $compile, $rootScope, $rootElement) {
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);
$animate.flush();
expect(element).toHaveClass(activeClass);
browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 2 });
$animate.flush();
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) {
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
$animate.flush();
element.remove();
expect(function() {
$animate.flush();
}).not.toThrow();
dealoc(element);
}));
it('should include the added/removed classes in lieu of the enter animation',
inject(function($animate, $compile, $rootScope, $rootElement, $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();
$animate.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');
$animate.flush();
expect(child).toHaveClass('ng-enter-active');
browserTrigger(child, 'transitionend', { timeStamp: Date.now(), elapsedTime: 2 });
$animate.flush();
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, $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');
$animate.flush();
expect(child).toHaveClass('ng-enter-active');
expect(child).toHaveClass('expand-add-active');
browserTrigger(child, 'transitionend', { timeStamp: Date.now(), elapsedTime: 2 });
$animate.flush();
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 issue a RAF for each element animation on all DOM levels', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$rAF) {
ss.addRule('.ng-enter', 'transition:2s linear all;');
element = jqLite(
'' +
'
' +
'
' +
'{{ item }}' +
'
' +
'
' +
'
'
);
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
$compile(element)($rootScope);
$rootScope.$digest();
var outer = element;
var inner = element.find('div');
$rootScope.exp = true;
$rootScope.items = [1,2,3,4,5,6,7,8,9,10];
$rootScope.$digest();
expect(outer).not.toHaveClass('parent');
expect(inner).not.toHaveClass('parent2');
assertTotalRepeats(0);
$$rAF.flush();
expect(outer).toHaveClass('parent');
assertTotalRepeats(0);
$$rAF.flush();
expect(inner).toHaveClass('parent2');
assertTotalRepeats(10);
function assertTotalRepeats(total) {
expect(inner[0].querySelectorAll('div.ng-enter').length).toBe(total);
}
});
});
it('should add the preparation class for an enter animation before a parent class-based animation is applied', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document) {
element = jqLite(
''
);
ss.addRule('.ng-enter', 'transition:2s linear all;');
ss.addRule('.parent-add', 'transition:5s linear all;');
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
$compile(element)($rootScope);
$rootScope.exp = true;
$rootScope.$digest();
var parent = element;
var child = element.find('div');
expect(parent).not.toHaveClass('parent');
expect(parent).toHaveClass('parent-add');
expect(child).not.toHaveClass('ng-enter');
expect(child).toHaveClass('ng-enter-prepare');
$animate.flush();
expect(parent).toHaveClass('parent parent-add parent-add-active');
expect(child).toHaveClass('ng-enter ng-enter-active');
expect(child).not.toHaveClass('ng-enter-prepare');
});
});
it('should avoid adding the ng-enter-prepare method to a parent structural animation that contains child animations', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$rAF) {
element = jqLite(
''
);
ss.addRule('.ng-enter', 'transition:2s linear all;');
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
$compile(element)($rootScope);
$rootScope.parent = true;
$rootScope.child = true;
$rootScope.$digest();
var parent = jqLite(element[0].querySelector('.parent'));
var child = jqLite(element[0].querySelector('.child'));
expect(parent).not.toHaveClass('ng-enter-prepare');
expect(child).toHaveClass('ng-enter-prepare');
$$rAF.flush();
expect(parent).not.toHaveClass('ng-enter-prepare');
expect(child).not.toHaveClass('ng-enter-prepare');
});
});
it('should add the preparation class for an enter animation before a parent class-based animation is applied', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document) {
element = jqLite(
''
);
ss.addRule('.ng-enter', 'transition:2s linear all;');
ss.addRule('.parent-add', 'transition:5s linear all;');
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
$compile(element)($rootScope);
$rootScope.exp = true;
$rootScope.$digest();
var parent = element;
var child = element.find('div');
expect(parent).not.toHaveClass('parent');
expect(parent).toHaveClass('parent-add');
expect(child).not.toHaveClass('ng-enter');
expect(child).toHaveClass('ng-enter-prepare');
$animate.flush();
expect(parent).toHaveClass('parent parent-add parent-add-active');
expect(child).toHaveClass('ng-enter ng-enter-active');
expect(child).not.toHaveClass('ng-enter-prepare');
});
});
it('should remove the prepare classes when different structural animations happen in the same digest', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$animateCache) {
element = jqLite(
// Class animation on parent element is neeeded so the child elements get the prepare class
''
);
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
$compile(element)($rootScope);
$rootScope.cond = false;
$rootScope.$digest();
$rootScope.cond = true;
$rootScope.$digest();
var parent = element;
var truthySwitch = jqLite(parent[0].querySelector('#truthy'));
var defaultSwitch = jqLite(parent[0].querySelector('#default'));
expect(parent).not.toHaveClass('blue');
expect(parent).toHaveClass('blue-add');
expect(truthySwitch).toHaveClass('ng-enter-prepare');
expect(defaultSwitch).toHaveClass('ng-leave-prepare');
$animate.flush();
expect(parent).toHaveClass('blue');
expect(parent).not.toHaveClass('blue-add');
expect(truthySwitch).not.toHaveClass('ng-enter-prepare');
expect(defaultSwitch).not.toHaveClass('ng-leave-prepare');
});
});
it('should respect the element node for caching when animations with the same type happen in the same digest', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document, $$animateCache) {
ss.addRule('.animate.ng-enter', 'transition:2s linear all;');
element = jqLite(
''
);
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
$compile(element)($rootScope);
$rootScope.cond = true;
$rootScope.$digest();
var parent = element;
var noanimate = jqLite(parent[0].querySelector('#noanimate'));
var animate = jqLite(parent[0].querySelector('#animate'));
expect(noanimate).not.toHaveClass('ng-enter');
expect(animate).toHaveClass('ng-enter');
$animate.closeAndFlush();
expect(noanimate).not.toHaveClass('ng-enter');
expect(animate).not.toHaveClass('ng-enter');
});
});
it('should pack level elements into their own RAF flush', function() {
module('ngAnimateMock');
inject(function($animate, $compile, $rootScope, $rootElement, $document) {
ss.addRule('.inner', 'transition:2s linear all;');
element = jqLite(
'' +
'
' +
'
' +
'
' +
'
' +
'
'
);
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
$compile(element)($rootScope);
$rootScope.$digest();
assertGroupHasClass(query('outer'), 'on', true);
expect(query('inner').length).toBe(0);
$rootScope.exp = true;
$rootScope.$digest();
assertGroupHasClass(query('outer'), 'on', true);
assertGroupHasClass(query('inner'), 'ng-enter', true);
$animate.flush();
assertGroupHasClass(query('outer'), 'on');
assertGroupHasClass(query('inner'), 'ng-enter');
function query(className) {
return element[0].querySelectorAll('.' + className);
}
function assertGroupHasClass(elms, className, not) {
for (var i = 0; i < elms.length; i++) {
var assert = expect(jqLite(elms[i]));
(not ? assert.not : assert).toHaveClass(className);
}
}
});
});
it('should trigger callbacks at the start and end of an animation',
inject(function($rootScope, $rootElement, $animate, $compile) {
ss.addRule('.animate-me', 'transition:2s linear all;');
var parent = jqLite('');
element = parent.find('div');
html(parent);
$compile(parent)($rootScope);
$rootScope.$digest();
var spy = jasmine.createSpy();
$animate.on('enter', parent, spy);
$rootScope.exp = true;
$rootScope.$digest();
element = parent.find('div');
$animate.flush();
expect(spy).toHaveBeenCalledTimes(1);
browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 2 });
$animate.flush();
expect(spy).toHaveBeenCalledTimes(2);
dealoc(element);
}));
it('should remove a class when the same class is currently being added by a joined class-based animation',
inject(function($animate, $animateCss, $rootScope, $document, $rootElement, $$rAF) {
ss.addRule('.hide', 'opacity: 0');
ss.addRule('.hide-add, .hide-remove', 'transition: 1s linear all');
jqLite($document[0].body).append($rootElement);
element = jqLite('');
$rootElement.append(element);
// These animations will be joined together
$animate.addClass(element, 'red');
$animate.addClass(element, 'hide');
$rootScope.$digest();
expect(element).toHaveClass('red-add');
expect(element).toHaveClass('hide-add');
// When a digest has passed, but no $rAF has been issued yet, .hide hasn't been added to
// the element yet
$animate.removeClass(element, 'hide');
$rootScope.$digest();
$$rAF.flush();
expect(element).not.toHaveClass('hide-add hide-add-active');
expect(element).toHaveClass('hide-remove hide-remove-active');
//End the animation process
browserTrigger(element, 'transitionend',
{ timeStamp: Date.now() + 1000, elapsedTime: 2 });
$animate.flush();
expect(element).not.toHaveClass('hide-add-active red-add-active');
expect(element).toHaveClass('red');
expect(element).not.toHaveClass('hide');
}));
it('should handle ng-if & ng-class with a class that is removed before its add animation has concluded', function() {
inject(function($animate, $rootScope, $compile, $timeout, $$rAF) {
ss.addRule('.animate-me', 'transition: all 0.5s;');
element = jqLite('');
html(element);
$rootScope.blue = true;
$rootScope.red = true;
$compile(element)($rootScope);
$rootScope.$digest();
var child = element.find('div');
// Trigger class removal before the add animation has been concluded
$rootScope.blue = false;
$animate.closeAndFlush();
expect(child).toHaveClass('red');
expect(child).not.toHaveClass('blue');
});
});
it('should not apply ngAnimate CSS preparation classes when a css animation definition has duration = 0', function() {
function fill(max) {
var arr = [];
for (var i = 0; i < max; i++) {
arr.push(i);
}
return arr;
}
inject(function($animate, $rootScope, $compile, $timeout, $$rAF, $$jqLite) {
ss.addRule('.animate-me', 'transition: all 0.5s;');
var classAddSpy = spyOn($$jqLite, 'addClass').and.callThrough();
var classRemoveSpy = spyOn($$jqLite, 'removeClass').and.callThrough();
element = jqLite(
' '
);
html(element);
$compile(element)($rootScope);
$rootScope.items = fill(100);
$rootScope.$digest();
expect(classAddSpy.calls.count()).toBe(2);
expect(classRemoveSpy.calls.count()).toBe(2);
expect(classAddSpy.calls.argsFor(0)[1]).toBe('ng-animate');
expect(classAddSpy.calls.argsFor(1)[1]).toBe('ng-enter');
expect(classRemoveSpy.calls.argsFor(0)[1]).toBe('ng-enter');
expect(classRemoveSpy.calls.argsFor(1)[1]).toBe('ng-animate');
expect(element.children().length).toBe(100);
});
});
});
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) {
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();
$animate.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();
$animate.flush();
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();
$animate.flush();
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 ');
}
});
});
it('should not alter the provided options values in anyway throughout the animation', function() {
var animationSpy = jasmine.createSpy();
module(function($animateProvider) {
$animateProvider.register('.this-animation', function() {
return {
enter: function(element, done) {
animationSpy();
done();
}
};
});
});
inject(function($animate, $rootScope, $compile) {
element = jqLite('');
var child = jqLite('');
var initialOptions = {
from: { height: '50px' },
to: { width: '100px' },
addClass: 'one',
removeClass: 'two',
domOperation: undefined
};
var copiedOptions = copy(initialOptions);
expect(copiedOptions).toEqual(initialOptions);
html(element);
$compile(element)($rootScope);
$animate.enter(child, element, null, copiedOptions);
$rootScope.$digest();
expect(copiedOptions).toEqual(initialOptions);
$animate.flush();
expect(copiedOptions).toEqual(initialOptions);
expect(child).toHaveClass('one');
expect(child).not.toHaveClass('two');
expect(child.attr('style')).toContain('100px');
expect(child.attr('style')).toContain('50px');
});
});
it('should execute the enter animation on a