');
expect(capturedAnimation[1]).toBe('enter');
});
});
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);
options.foo = 'bar';
$animate.enter(element, parent, null, options);
expect(parent.children().length).toBe(1);
$rootScope.$digest();
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);
expect(parent.children().length).toBe(1);
expect(parent2.children().length).toBe(0);
options.foo = 'bar';
$animate.move(element, parent2, null, options);
expect(parent.children().length).toBe(0);
expect(parent2.children().length).toBe(1);
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('move');
expect(capturedAnimation[2].foo).toEqual(options.foo);
}));
they('$prop() should insert the element adjacent to the after element if provided',
['enter', 'move'], function(event) {
inject(function($animate, $rootScope) {
parent.append(element);
assertCompareNodes(parent2.next(), element, true);
$animate[event](element, null, parent2, options);
assertCompareNodes(parent2.next(), element);
$rootScope.$digest();
expect(capturedAnimation[1]).toBe(event);
});
});
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(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();
expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('leave');
expect(capturedAnimation[2].foo).toEqual(options.foo);
expect(element.parent().length).toBe(1);
capturedAnimation[2].domOperation();
expect(element.parent().length).toBe(0);
}));
it('should remove all element and comment nodes during leave animation',
inject(function($compile, $rootScope, $animate, $$AnimateRunner) {
element = $compile(
'
' +
'
start
' +
'
end
' +
'
'
)($rootScope);
parent.append(element);
$rootScope.items = [1,2,3,4,5];
$rootScope.$digest();
// all the start/end repeat anchors + their adjacent comments
expect(element[0].childNodes.length).toBe(22);
var runner = new $$AnimateRunner();
overriddenAnimationRunner = runner;
$rootScope.items.length = 0;
$rootScope.$digest();
runner.end();
$animate.flush();
// we're left with a text node and a comment node
expect(element[0].childNodes.length).toBeLessThan(3);
}));
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(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('addClass');
expect(capturedAnimation[2].foo).toEqual(options.foo);
expect(element).not.toHaveClass('red');
applyAnimationClasses(element, capturedAnimation[2]);
expect(element).toHaveClass('red');
}));
it('removeClass() should issue a removeClass animation with the correct DOM operation', inject(function($animate, $rootScope) {
parent.append(element);
element.addClass('blue');
options.foo = 'bar';
$animate.removeClass(element, 'blue', options);
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('removeClass');
expect(capturedAnimation[2].foo).toEqual(options.foo);
expect(element).toHaveClass('blue');
applyAnimationClasses(element, capturedAnimation[2]);
expect(element).not.toHaveClass('blue');
}));
it('setClass() should issue a setClass animation with the correct DOM operation', inject(function($animate, $rootScope) {
parent.append(element);
element.addClass('green');
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);
expect(element).not.toHaveClass('yellow');
expect(element).toHaveClass('green');
applyAnimationClasses(element, capturedAnimation[2]);
expect(element).toHaveClass('yellow');
expect(element).not.toHaveClass('green');
}));
they('$prop() should operate using a native DOM element',
['enter', 'move', 'leave', 'addClass', 'removeClass', 'setClass', 'animate'], function(event) {
inject(function($animate, $rootScope, $document) {
var element = $document[0].createElement('div');
element.setAttribute('id', 'crazy-man');
if (event !== 'enter' && event !== 'move') {
parent.append(element);
}
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;
}
$rootScope.$digest();
expect(capturedAnimation[0].attr('id')).toEqual(element.getAttribute('id'));
});
});
describe('addClass / removeClass', function() {
it('should not perform an animation if there are no valid CSS classes to add',
inject(function($animate, $rootScope) {
parent.append(element);
$animate.removeClass(element, 'something-to-remove');
$rootScope.$digest();
expect(capturedAnimation).toBeNull();
element.addClass('something-to-add');
$animate.addClass(element, 'something-to-add');
$rootScope.$digest();
expect(capturedAnimation).toBeNull();
}));
});
describe('animate()', function() {
they('should not perform an animation if $prop is provided as a `to` style',
{ '{}': {},
'null': null,
'false': false,
'""': '',
'[]': [] }, function(toStyle) {
inject(function($animate, $rootScope) {
parent.append(element);
$animate.animate(element, null, toStyle);
$rootScope.$digest();
expect(capturedAnimation).toBeNull();
});
});
it('should not perform an animation if only from styles are provided',
inject(function($animate, $rootScope) {
var fromStyle = { color: 'pink' };
parent.append(element);
$animate.animate(element, fromStyle);
$rootScope.$digest();
expect(capturedAnimation).toBeNull();
}));
it('should perform an animation if only from styles are provided as well as any valid classes',
inject(function($animate, $rootScope) {
parent.append(element);
var fromStyle = { color: 'red' };
var options = { removeClass: 'goop' };
$animate.animate(element, fromStyle, null, null, options);
$rootScope.$digest();
expect(capturedAnimation).toBeNull();
fromStyle = { color: 'blue' };
options = { addClass: 'goop' };
$animate.animate(element, fromStyle, null, null, options);
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
}));
});
describe('$animate.cancel()', function() {
it('should cancel enter()', inject(function($animate, $rootScope) {
expect(parent.children().length).toBe(0);
options.foo = 'bar';
var spy = jasmine.createSpy('cancelCatch');
var runner = $animate.enter(element, parent, null, options);
runner.catch(spy);
expect(parent.children().length).toBe(1);
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('enter');
expect(capturedAnimation[2].foo).toEqual(options.foo);
$animate.cancel(runner);
// Since enter() immediately adds the element, we can only check if the
// element is still at the position
expect(parent.children().length).toBe(1);
$rootScope.$digest();
// Catch handler is called after digest
expect(spy).toHaveBeenCalled();
}));
it('should cancel move()', inject(function($animate, $rootScope) {
parent.append(element);
expect(parent.children().length).toBe(1);
expect(parent2.children().length).toBe(0);
options.foo = 'bar';
var spy = jasmine.createSpy('cancelCatch');
var runner = $animate.move(element, parent2, null, options);
runner.catch(spy);
expect(parent.children().length).toBe(0);
expect(parent2.children().length).toBe(1);
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('move');
expect(capturedAnimation[2].foo).toEqual(options.foo);
$animate.cancel(runner);
// Since moves() immediately moves the element, we can only check if the
// element is still at the correct position
expect(parent.children().length).toBe(0);
expect(parent2.children().length).toBe(1);
$rootScope.$digest();
// Catch handler is called after digest
expect(spy).toHaveBeenCalled();
}));
it('cancel leave()', inject(function($animate, $rootScope) {
parent.append(element);
options.foo = 'bar';
var spy = jasmine.createSpy('cancelCatch');
var runner = $animate.leave(element, options);
runner.catch(spy);
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('leave');
expect(capturedAnimation[2].foo).toEqual(options.foo);
expect(element.parent().length).toBe(1);
$animate.cancel(runner);
// Animation concludes immediately
expect(element.parent().length).toBe(0);
expect(spy).not.toHaveBeenCalled();
$rootScope.$digest();
// Catch handler is called after digest
expect(spy).toHaveBeenCalled();
}));
it('should cancel addClass()', inject(function($animate, $rootScope) {
parent.append(element);
options.foo = 'bar';
var runner = $animate.addClass(element, 'red', options);
var spy = jasmine.createSpy('cancelCatch');
runner.catch(spy);
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('addClass');
expect(capturedAnimation[2].foo).toEqual(options.foo);
$animate.cancel(runner);
expect(element).toHaveClass('red');
expect(spy).not.toHaveBeenCalled();
$rootScope.$digest();
expect(spy).toHaveBeenCalled();
}));
it('should cancel setClass()', inject(function($animate, $rootScope) {
parent.append(element);
element.addClass('red');
options.foo = 'bar';
var runner = $animate.setClass(element, 'blue', 'red', options);
var spy = jasmine.createSpy('cancelCatch');
runner.catch(spy);
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('setClass');
expect(capturedAnimation[2].foo).toEqual(options.foo);
$animate.cancel(runner);
expect(element).toHaveClass('blue');
expect(element).not.toHaveClass('red');
expect(spy).not.toHaveBeenCalled();
$rootScope.$digest();
expect(spy).toHaveBeenCalled();
}));
it('should cancel removeClass()', inject(function($animate, $rootScope) {
parent.append(element);
element.addClass('red blue');
options.foo = 'bar';
var runner = $animate.removeClass(element, 'red', options);
var spy = jasmine.createSpy('cancelCatch');
runner.catch(spy);
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('removeClass');
expect(capturedAnimation[2].foo).toEqual(options.foo);
$animate.cancel(runner);
expect(element).not.toHaveClass('red');
expect(element).toHaveClass('blue');
$rootScope.$digest();
expect(spy).toHaveBeenCalled();
}));
it('should cancel animate()',
inject(function($animate, $rootScope) {
parent.append(element);
var fromStyle = { color: 'blue' };
var options = { addClass: 'red' };
var runner = $animate.animate(element, fromStyle, null, null, options);
var spy = jasmine.createSpy('cancelCatch');
runner.catch(spy);
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
$animate.cancel(runner);
expect(element).toHaveClass('red');
$rootScope.$digest();
expect(spy).toHaveBeenCalled();
}));
});
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) {
inject(function($rootScope, $animate) {
parent.append(element);
var child = jqLite('');
if (animationType === 'structural') {
$animate.enter(child, element);
} else {
element.append(child);
$animate.addClass(child, 'test');
}
$animate.addClass(parent, 'abc');
expect(capturedAnimationHistory.length).toBe(0);
$rootScope.$digest();
expect(capturedAnimationHistory.length).toBe(2);
});
});
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) {
inject(function($rootScope, $animate) {
parent.append(element);
var child = jqLite('');
$animate.addClass(parent, 'abc');
$rootScope.$digest();
if (animationType === 'structural') {
$animate.enter(child, element);
} else {
element.append(child);
$animate.addClass(child, 'test');
}
expect(capturedAnimationHistory.length).toBe(1);
$rootScope.$digest();
expect(capturedAnimationHistory.length).toBe(2);
});
});
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) {
inject(function($rootScope, $animate) {
parent.append(element);
var child = jqLite('');
if (animationType === 'structural') {
$animate.enter(child, element);
} else {
element.append(child);
$animate.addClass(child, 'test');
}
$rootScope.$digest();
$animate.addClass(parent, 'abc');
expect(capturedAnimationHistory.length).toBe(1);
$rootScope.$digest();
expect(capturedAnimationHistory.length).toBe(2);
});
});
});
it('should NOT clobber all data on an element when animation is finished',
inject(function($animate, $rootScope) {
element.data('foo', 'bar');
$animate.removeClass(element, 'ng-hide');
$rootScope.$digest();
$animate.addClass(element, 'ng-hide');
$rootScope.$digest();
expect(element.data('foo')).toEqual('bar');
}));
describe('child animations', function() {
it('should skip animations if the element is not attached to the $rootElement',
inject(function($compile, $rootScope, $animate) {
$animate.enabled(true);
var elm1 = $compile('')($rootScope);
expect(capturedAnimation).toBeNull();
$animate.addClass(elm1, 'klass2');
expect(capturedAnimation).toBeNull();
$rootScope.$digest();
expect(capturedAnimation).toBeNull();
}));
it('should skip animations if the element is attached to the $rootElement, but not apart of the body',
inject(function($compile, $rootScope, $animate, $rootElement) {
$animate.enabled(true);
var elm1 = $compile('')($rootScope);
var newParent = $compile('')($rootScope);
newParent.append($rootElement);
$rootElement.append(elm1);
expect(capturedAnimation).toBeNull();
$animate.addClass(elm1, 'klass2');
expect(capturedAnimation).toBeNull();
$rootScope.$digest();
expect(capturedAnimation).toBeNull();
}));
it('should skip the animation if the element is removed from the DOM before the post digest kicks in',
inject(function($animate, $rootScope) {
$animate.enter(element, parent);
expect(capturedAnimation).toBeNull();
element.remove();
$rootScope.$digest();
expect(capturedAnimation).toBeNull();
}));
it('should be blocked when there is an ongoing structural parent animation occurring',
inject(function($rootScope, $rootElement, $animate) {
parent.append(element);
expect(capturedAnimation).toBeNull();
$animate.move(parent, parent2);
$rootScope.$digest();
// yes the animation is going on
expect(capturedAnimation[0]).toBe(parent);
capturedAnimation = null;
$animate.addClass(element, 'blue');
$rootScope.$digest();
expect(capturedAnimation).toBeNull();
}));
it('should disable all child animations for atleast one turn when a structural animation is issued',
inject(function($animate, $rootScope, $compile, $document, $rootElement, $$AnimateRunner) {
element = $compile(
'
' +
'
' +
' {{ item }}' +
'
' +
'
'
)($rootScope);
jqLite($document[0].body).append($rootElement);
$rootElement.append(element);
var runner = new $$AnimateRunner();
overriddenAnimationRunner = runner;
$rootScope.items = [1];
$rootScope.$digest();
expect(capturedAnimation[0]).toHaveClass('if-animation');
expect(capturedAnimationHistory.length).toBe(1);
expect(element[0].querySelectorAll('.repeat-animation').length).toBe(1);
$rootScope.items = [1, 2];
$rootScope.$digest();
expect(capturedAnimation[0]).toHaveClass('if-animation');
expect(capturedAnimationHistory.length).toBe(1);
expect(element[0].querySelectorAll('.repeat-animation').length).toBe(2);
runner.end();
$animate.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 not be blocked when there is an ongoing class-based parent animation occurring',
inject(function($rootScope, $rootElement, $animate) {
parent.append(element);
expect(capturedAnimation).toBeNull();
$animate.addClass(parent, 'rogers');
$rootScope.$digest();
// yes the animation is going on
expect(capturedAnimation[0]).toBe(parent);
capturedAnimation = null;
$animate.addClass(element, 'blue');
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(element);
}));
describe('when a parent structural animation is triggered:', function() {
it('should skip all pre-digest queued child animations',
inject(function($rootScope, $rootElement, $animate) {
parent.append(element);
$animate.addClass(element, 'rumlow');
$animate.move(parent, null, parent2);
expect(capturedAnimation).toBeNull();
expect(capturedAnimationHistory.length).toBe(0);
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(parent);
expect(capturedAnimationHistory.length).toBe(1);
}));
it('should end all ongoing post-digest child animations',
inject(function($rootScope, $rootElement, $animate) {
parent.append(element);
$animate.addClass(element, 'rumlow');
var isCancelled = false;
overriddenAnimationRunner = extend(defaultFakeAnimationRunner, {
end: function() {
isCancelled = true;
}
});
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(element);
expect(isCancelled).toBe(false);
// restore the default
overriddenAnimationRunner = defaultFakeAnimationRunner;
$animate.move(parent, null, parent2);
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(parent);
expect(isCancelled).toBe(true);
}));
it('should ignore children that have animation data-attributes but no animation data',
inject(function($rootScope, $rootElement, $animate) {
parent.append(element);
$animate.addClass(element, 'rumlow');
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(element);
// If an element is cloned during an animation, the clone has the data-attributes indicating
// an animation
var clone = element.clone();
parent.append(clone);
$animate.move(parent, null, parent2);
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(parent);
}));
});
it('should not end any child animations if a parent class-based animation is issued',
inject(function($rootScope, $rootElement, $animate) {
parent.append(element);
var element2 = jqLite('
element2
');
$animate.enter(element2, parent);
var isCancelled = false;
overriddenAnimationRunner = extend(defaultFakeAnimationRunner, {
end: function() {
isCancelled = true;
}
});
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(element2);
expect(isCancelled).toBe(false);
// restore the default
overriddenAnimationRunner = defaultFakeAnimationRunner;
$animate.addClass(parent, 'peter');
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(parent);
expect(isCancelled).toBe(false);
}));
it('should allow follow-up class-based animations to run in parallel on the same element',
inject(function($rootScope, $animate) {
parent.append(element);
var runner1done = false;
var runner1 = $animate.addClass(element, 'red');
runner1.done(function() {
runner1done = true;
});
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
expect(runner1done).toBeFalsy();
capturedAnimation = null;
// 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
}
});
var runner2done = false;
var runner2 = $animate.addClass(element, 'blue');
runner2.done(function() {
runner2done = true;
});
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
expect(runner2done).toBeFalsy();
expect(runner1done).toBeFalsy();
runner2.end();
expect(runner2done).toBeTruthy();
expect(runner1done).toBeFalsy();
}));
it('should remove the animation block on child animations once the parent animation is complete',
inject(function($rootScope, $rootElement, $animate, $$AnimateRunner) {
var runner = new $$AnimateRunner();
overriddenAnimationRunner = runner;
parent.append(element);
$animate.enter(parent, null, parent2);
$rootScope.$digest();
expect(capturedAnimationHistory.length).toBe(1);
$animate.addClass(element, 'tony');
$rootScope.$digest();
expect(capturedAnimationHistory.length).toBe(1);
runner.end();
$animate.flush();
$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) {
var enterComplete = false;
overriddenAnimationRunner = extend(defaultFakeAnimationRunner, {
end: function() {
enterComplete = true;
}
});
parent.append(element);
$animate.move(element, parent2);
$rootScope.$digest();
expect(enterComplete).toBe(false);
$animate.leave(element);
$rootScope.$digest();
expect(enterComplete).toBe(true);
}));
it('should cancel the previous structural animation if a follow-up structural animation takes over before the postDigest',
inject(function($animate) {
var enterDone = jasmine.createSpy('enter animation done');
$animate.enter(element, parent).done(enterDone);
expect(enterDone).not.toHaveBeenCalled();
$animate.leave(element);
$animate.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) {
parent.append(element);
var runner = $animate.addClass(element, 'active-class');
$rootScope.$digest();
var doneHandler = jasmine.createSpy('addClass done');
runner.done(doneHandler);
expect(doneHandler).not.toHaveBeenCalled();
$animate.removeClass(element, 'active-class');
$rootScope.$digest();
// true = rejected
expect(doneHandler).toHaveBeenCalledWith(true);
}));
it('should cancel the previously running removeClass animation if a follow-up addClass animation is using the same class value',
inject(function($animate, $rootScope) {
element.addClass('active-class');
parent.append(element);
var runner = $animate.removeClass(element, 'active-class');
$rootScope.$digest();
var doneHandler = jasmine.createSpy('addClass done');
runner.done(doneHandler);
expect(doneHandler).not.toHaveBeenCalled();
$animate.addClass(element, 'active-class');
$rootScope.$digest();
// true = rejected
expect(doneHandler).toHaveBeenCalledWith(true);
}));
it('should merge a follow-up animation that does not add classes into the previous animation (pre-digest)',
inject(function($animate, $rootScope) {
$animate.enter(element, parent);
$animate.animate(element, {height: 0}, {height: '100px'});
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
expect(capturedAnimation[1]).toBe('enter'); // make sure the enter animation is present
// fake the style setting (because $$animation is mocked)
applyAnimationStyles(element, capturedAnimation[2]);
expect(element.css('height')).toContain('100px');
}));
it('should immediately skip the class-based animation if there is an active structural animation',
inject(function($animate, $rootScope) {
$animate.enter(element, parent);
$rootScope.$digest();
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) {
$animate.enter(element, parent);
expect(capturedAnimation).toBeNull();
$animate.addClass(element, 'red');
expect(element).not.toHaveClass('red');
expect(capturedAnimation).toBeNull();
$rootScope.$digest();
expect(capturedAnimation[1]).toBe('enter');
expect(capturedAnimation[2].addClass).toBe('red');
}));
it('should issue a new runner instance if a previous structural animation was cancelled',
inject(function($animate, $rootScope) {
parent.append(element);
var runner1 = $animate.move(element, parent2);
$rootScope.$digest();
var runner2 = $animate.leave(element);
$rootScope.$digest();
expect(runner1).not.toBe(runner2);
}));
it('should properly cancel out animations when the same class is added/removed within the same digest',
inject(function($animate, $rootScope) {
parent.append(element);
$animate.addClass(element, 'red');
$animate.removeClass(element, 'red');
$rootScope.$digest();
expect(capturedAnimation).toBeNull();
$animate.addClass(element, 'blue');
$rootScope.$digest();
expect(capturedAnimation[2].addClass).toBe('blue');
}));
it('should NOT cancel a previously joined addClass+structural animation if a follow-up ' +
'removeClass animation is using the same class value (pre-digest)',
inject(function($animate, $rootScope) {
var runner = $animate.enter(element, parent);
$animate.addClass(element, 'active-class');
var doneHandler = jasmine.createSpy('enter done');
runner.done(doneHandler);
expect(doneHandler).not.toHaveBeenCalled();
$animate.removeClass(element, 'active-class');
$rootScope.$digest();
expect(capturedAnimation[1]).toBe('enter');
expect(capturedAnimation[2].addClass).toBe(null);
expect(capturedAnimation[2].removeClass).toBe(null);
expect(doneHandler).not.toHaveBeenCalled();
}));
});
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');
$animate.addClass(element, 'red');
$animate.addClass(element, 'blue');
$animate.removeClass(element, 'green');
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('setClass');
options = capturedAnimation[2];
expect(options.addClass).toEqual('red blue');
expect(options.removeClass).toEqual('green');
expect(element).not.toHaveClass('red');
expect(element).not.toHaveClass('blue');
expect(element).toHaveClass('green');
applyAnimationClasses(element, capturedAnimation[2]);
expect(element).toHaveClass('red');
expect(element).toHaveClass('blue');
expect(element).not.toHaveClass('green');
}));
it('multiple class-based animations together into a single structural event before the digest passes', inject(function($animate, $rootScope) {
element.addClass('green');
expect(element.parent().length).toBe(0);
$animate.enter(element, parent);
expect(element.parent().length).toBe(1);
$animate.addClass(element, 'red');
$animate.removeClass(element, 'green');
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('enter');
options = capturedAnimation[2];
expect(options.addClass).toEqual('red');
expect(options.removeClass).toEqual('green');
expect(element).not.toHaveClass('red');
expect(element).toHaveClass('green');
applyAnimationClasses(element, capturedAnimation[2]);
expect(element).toHaveClass('red');
expect(element).not.toHaveClass('green');
}));
it('should automatically cancel out class-based animations if the element already contains or doesn\'t contain the applied classes',
inject(function($animate, $rootScope) {
parent.append(element);
element.addClass('one three');
$animate.addClass(element, 'one');
$animate.addClass(element, 'two');
$animate.removeClass(element, 'three');
$animate.removeClass(element, 'four');
$rootScope.$digest();
options = capturedAnimation[2];
expect(options.addClass).toEqual('two');
expect(options.removeClass).toEqual('three');
}));
it('and skip the animation entirely if no class-based animations remain and if there is no structural animation applied',
inject(function($animate, $rootScope) {
parent.append(element);
element.addClass('one three');
$animate.addClass(element, 'one');
$animate.removeClass(element, 'four');
$rootScope.$digest();
expect(capturedAnimation).toBeNull();
}));
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) {
element.addClass('one three');
$animate.addClass(element, 'one');
$animate.removeClass(element, 'four');
$animate.enter(element, parent);
$rootScope.$digest();
expect(capturedAnimation[1]).toBe('enter');
}));
it('class-based animations, however it should also cancel former structural animations in the process',
inject(function($animate, $rootScope) {
element.addClass('green lime');
$animate.enter(element, parent);
$animate.addClass(element, 'red');
$animate.removeClass(element, 'green');
$animate.leave(element);
$animate.addClass(element, 'pink');
$animate.removeClass(element, 'lime');
expect(element).toHaveClass('red');
expect(element).not.toHaveClass('green');
expect(element).not.toHaveClass('pink');
expect(element).toHaveClass('lime');
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(element);
expect(capturedAnimation[1]).toBe('leave');
// $$hashKey causes comparison issues
expect(element.parent()[0]).toBe(parent[0]);
options = capturedAnimation[2];
expect(options.addClass).toEqual('pink');
expect(options.removeClass).toEqual('lime');
}));
it('should retain the instance to the very first runner object when multiple element-level animations are issued',
inject(function($animate, $rootScope) {
element.addClass('green');
var r1 = $animate.enter(element, parent);
var r2 = $animate.addClass(element, 'red');
var r3 = $animate.removeClass(element, 'green');
expect(r1).toBe(r2);
expect(r2).toBe(r3);
}));
it('should not skip or miss the animations when animations are executed sequential',
inject(function($animate, $rootScope, $rootElement) {
element = jqLite('');
$rootElement.append(element);
$animate.addClass(element, 'rclass');
$animate.removeClass(element, 'rclass');
$animate.addClass(element, 'rclass');
$animate.removeClass(element, 'rclass');
$rootScope.$digest();
expect(element).not.toHaveClass('rclass');
}));
});
});
they('should allow an animation to run on the $prop element', ['$rootElement', 'body'], function(name) {
var capturedAnimation;
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);
var body = jqLite($document[0].body);
var targetElement = name === 'body' ? body : $rootElement;
$animate.addClass(targetElement, 'red');
$rootScope.$digest();
expect(capturedAnimation[0]).toBe(targetElement);
expect(capturedAnimation[1]).toBe('addClass');
});
});
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, $document, $animate) {
jqLite($document[0].body).append($rootElement);
parent = jqLite('');
element = jqLite('');
child = jqLite('');
$animate.enabled(true);
};
}));
it('should allow child animations to run when the attribute is used',
inject(function($animate, $rootScope, $rootElement, $compile) {
$animate.enter(parent, $rootElement);
$animate.enter(element, parent);
$animate.enter(child, element);
$rootScope.$digest();
expect(captureLog.length).toBe(1);
captureLog = [];
parent.attr('ng-animate-children', '');
$compile(parent)($rootScope);
$rootScope.$digest();
$animate.enter(parent, $rootElement);
$rootScope.$digest();
expect(captureLog.length).toBe(1);
$animate.enter(element, parent);
$animate.enter(child, element);
$rootScope.$digest();
expect(captureLog.length).toBe(3);
}));
it('should fully disallow all parallel child animations from running if `off` is used',
inject(function($animate, $rootScope, $rootElement, $compile) {
$rootElement.append(parent);
parent.append(element);
element.append(child);
parent.attr('ng-animate-children', 'off');
element.attr('ng-animate-children', 'on');
$compile(parent)($rootScope);
$compile(element)($rootScope);
$rootScope.$digest();
$animate.leave(parent);
$animate.leave(element);
$animate.leave(child);
$rootScope.$digest();
expect(captureLog.length).toBe(1);
dealoc(element);
dealoc(child);
}));
it('should watch to see if the ng-animate-children attribute changes',
inject(function($animate, $rootScope, $rootElement, $compile) {
$rootElement.append(parent);
$rootScope.val = 'on';
parent.attr('ng-animate-children', '{{ val }}');
$compile(parent)($rootScope);
$rootScope.$digest();
$animate.enter(parent, $rootElement);
$animate.enter(element, parent);
$animate.enter(child, element);
$rootScope.$digest();
expect(captureLog.length).toBe(3);
captureLog = [];
$rootScope.val = 'off';
$rootScope.$digest();
$animate.leave(parent);
$animate.leave(element);
$animate.leave(child);
$rootScope.$digest();
expect(captureLog.length).toBe(1);
dealoc(element);
dealoc(child);
}));
it('should respect the value if the directive is on an element with ngIf',
inject(function($rootScope, $rootElement, $compile) {
parent.attr('ng-animate-children', 'true');
parent.attr('ng-if', 'true');
element.attr('ng-if', 'true');
$rootElement.append(parent);
parent.append(element);
$compile(parent)($rootScope);
$rootScope.$digest();
expect(captureLog.length).toBe(2);
}));
});
describe('.pin()', function() {
var capturedAnimation;
beforeEach(module(function($provide) {
capturedAnimation = null;
$provide.factory('$$animation', function($$AnimateRunner) {
return function() {
capturedAnimation = arguments;
return new $$AnimateRunner();
};
});
return function($animate) {
$animate.enabled(true);
};
}));
it('should throw if the arguments are not elements',
inject(function($animate, $rootElement) {
var element = jqLite('');
expect(function() {
$animate.pin(element);
}).toThrowMinErr('ng', 'areq', 'Argument \'parentElement\' is not an element');
expect(function() {
$animate.pin(null, $rootElement);
}).toThrowMinErr('ng', 'areq', 'Argument \'element\' is not an element');
dealoc(element);
}));
they('should animate an element inside a pinned element that is the $prop element',
['same', 'parent', 'grandparent'],
function(elementRelation) {
inject(function($animate, $document, $rootElement, $rootScope) {
var pinElement, animateElement;
var innerParent = jqLite('');
jqLite($document[0].body).append(innerParent);
innerParent.append($rootElement);
switch (elementRelation) {
case 'same':
pinElement = jqLite('');
break;
case 'parent':
pinElement = jqLite('
');
break;
case 'grandparent':
pinElement = jqLite('
');
break;
}
jqLite($document[0].body).append(pinElement);
animateElement = jqLite($document[0].getElementById('animate'));
$animate.addClass(animateElement, 'red');
$rootScope.$digest();
expect(capturedAnimation).toBeNull();
// Pin the element to the app root to enable animations
$animate.pin(pinElement, $rootElement);
$animate.addClass(animateElement, 'blue');
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
dealoc(pinElement);
});
});
they('should not animate an element when the pinned ($prop) element, is pinned to an element that is not a child of the $rootElement',
['same', 'parent', 'grandparent'],
function(elementRelation) {
inject(function($animate, $document, $rootElement, $rootScope) {
var pinElement, animateElement, pinTargetElement = jqLite('');
var innerParent = jqLite('');
jqLite($document[0].body).append(innerParent);
innerParent.append($rootElement);
switch (elementRelation) {
case 'same':
pinElement = jqLite('');
break;
case 'parent':
pinElement = jqLite('
');
break;
case 'grandparent':
pinElement = jqLite('
');
break;
}
// Append both the pin element and the pinTargetElement outside the app root
jqLite($document[0].body).append(pinElement);
jqLite($document[0].body).append(pinTargetElement);
animateElement = jqLite($document[0].getElementById('animate'));
$animate.addClass(animateElement, 'red');
$rootScope.$digest();
expect(capturedAnimation).toBeNull();
$animate.pin(pinElement, pinTargetElement);
$animate.addClass(animateElement, 'blue');
$rootScope.$digest();
expect(capturedAnimation).toBeNull();
dealoc(pinElement);
});
});
they('should adhere to the disabled state of the hosted parent when the $prop element is pinned',
['same', 'parent', 'grandparent'],
function(elementRelation) {
inject(function($animate, $document, $rootElement, $rootScope) {
var pinElement, animateElement, pinHostElement = jqLite('');
var innerParent = jqLite('');
jqLite($document[0].body).append(innerParent);
innerParent.append($rootElement);
switch (elementRelation) {
case 'same':
pinElement = jqLite('');
break;
case 'parent':
pinElement = jqLite('
');
break;
case 'grandparent':
pinElement = jqLite('
');
break;
}
$rootElement.append(pinHostElement);
jqLite($document[0].body).append(pinElement);
animateElement = jqLite($document[0].getElementById('animate'));
$animate.pin(pinElement, pinHostElement);
$animate.enabled(pinHostElement, false);
$animate.addClass(animateElement, 'blue');
$rootScope.$digest();
expect(capturedAnimation).toBeNull();
$animate.enabled(pinHostElement, true);
$animate.addClass(animateElement, 'red');
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
dealoc(pinElement);
});
});
});
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);
runner = new $$AnimateRunner();
return runner;
};
});
return function($document, $rootElement, $animate) {
if ($document !== $rootElement) {
jqLite($document[0].body).append($rootElement);
}
$animate.enabled(true);
};
}));
it('should trigger a callback for an enter animation',
inject(function($animate, $rootScope, $rootElement, $document) {
var callbackTriggered = false;
$animate.on('enter', jqLite($document[0].body), function() {
callbackTriggered = true;
});
element = jqLite('');
$animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.flush();
expect(callbackTriggered).toBe(true);
}));
it('should fire the callback with the signature of (element, phase, data)',
inject(function($animate, $rootScope, $rootElement, $document) {
var capturedElement;
var capturedPhase;
var capturedData;
$animate.on('enter', jqLite($document[0].body),
function(element, phase, data) {
capturedElement = element;
capturedPhase = phase;
capturedData = data;
});
element = jqLite('');
$animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.flush();
expect(capturedElement).toBe(element);
expect(isString(capturedPhase)).toBe(true);
expect(isObject(capturedData)).toBe(true);
}));
it('should not fire a callback if the element is outside of the given container',
inject(function($animate, $rootScope, $rootElement) {
var callbackTriggered = false;
var innerContainer = jqLite('');
$rootElement.append(innerContainer);
$animate.on('enter', innerContainer,
function(element, phase, data) {
callbackTriggered = true;
});
element = jqLite('');
$animate.enter(element, $rootElement);
$rootScope.$digest();
expect(callbackTriggered).toBe(false);
}));
it('should fire a callback if the element is the given container',
inject(function($animate, $rootScope, $rootElement) {
element = jqLite('');
var callbackTriggered = false;
$animate.on('enter', element,
function(element, phase, data) {
callbackTriggered = true;
});
$animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.flush();
expect(callbackTriggered).toBe(true);
}));
it('should remove all the event-based event listeners when $animate.off(event) is called',
inject(function($animate, $rootScope, $rootElement, $document) {
element = jqLite('');
var count = 0;
$animate.on('enter', element, counter);
$animate.on('enter', jqLite($document[0].body), counter);
function counter(element, phase) {
count++;
}
$animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.flush();
expect(count).toBe(2);
$animate.off('enter');
$animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.flush();
expect(count).toBe(2);
}));
it('should remove the container-based event listeners when $animate.off(event, container) is called',
inject(function($animate, $rootScope, $rootElement, $document) {
element = jqLite('');
var count = 0;
$animate.on('enter', element, counter);
$animate.on('enter', jqLite($document[0].body), counter);
function counter(element, phase) {
if (phase === 'start') {
count++;
}
}
$animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.flush();
expect(count).toBe(2);
$animate.off('enter', jqLite($document[0].body));
$animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.flush();
expect(count).toBe(3);
}));
it('should remove the callback-based event listener when $animate.off(event, container, callback) is called',
inject(function($animate, $rootScope, $rootElement) {
element = jqLite('');
var count = 0;
$animate.on('enter', element, counter1);
$animate.on('enter', element, counter2);
function counter1(element, phase) {
if (phase === 'start') {
count++;
}
}
function counter2(element, phase) {
if (phase === 'start') {
count++;
}
}
$animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.flush();
expect(count).toBe(2);
$animate.off('enter', element, counter2);
$animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.flush();
expect(count).toBe(3);
}));
it('should remove all event listeners for an element when $animate.off(element) is called',
inject(function($animate, $rootScope, $rootElement, $document, $$rAF) {
element = jqLite('');
var otherElement = jqLite('');
$rootElement.append(otherElement);
var count = 0;
var runner;
$animate.on('enter', element, counter);
$animate.on('leave', element, counter);
$animate.on('addClass', element, counter);
$animate.on('addClass', otherElement, counter);
function counter(element, phase) {
count++;
}
runner = $animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.flush();
runner.end();
runner = $animate.addClass(element, 'blue');
$rootScope.$digest();
$animate.flush();
runner.end();
$$rAF.flush();
expect(count).toBe(4);
$animate.off(element);
runner = $animate.enter(element, $rootElement);
$animate.flush();
expect(capturedAnimation[1]).toBe('enter');
runner.end();
runner = $animate.addClass(element, 'red');
$animate.flush();
expect(capturedAnimation[1]).toBe('addClass');
runner.end();
runner = $animate.leave(element);
$animate.flush();
expect(capturedAnimation[1]).toBe('leave');
runner.end();
// Try to flush all remaining callbacks
expect(function() {
$$rAF.flush();
}).toThrowError('No rAF callbacks present');
expect(count).toBe(4);
// Check that other elements' event listeners are not affected
$animate.addClass(otherElement, 'green');
$animate.flush();
expect(count).toBe(5);
}));
it('should not get affected by custom, enumerable properties on `Object.prototype`',
inject(function($animate) {
// eslint-disable-next-line no-extend-native
Object.prototype.foo = 'ENUMARABLE_AND_NOT_AN_ARRAY';
element = jqLite('');
expect(function() { $animate.off(element); }).not.toThrow();
delete Object.prototype.foo;
})
);
it('should fire a `start` callback when the animation starts with the matching element',
inject(function($animate, $rootScope, $rootElement, $document) {
element = jqLite('');
var capturedState;
var capturedElement;
$animate.on('enter', jqLite($document[0].body), function(element, phase) {
capturedState = phase;
capturedElement = element;
});
$animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.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, $rootElement, $document) {
element = jqLite('');
var capturedState;
var capturedElement;
$animate.on('enter', jqLite($document[0].body), function(element, phase) {
capturedState = phase;
capturedElement = element;
});
var runner = $animate.enter(element, $rootElement);
$rootScope.$digest();
runner.end();
$animate.flush();
expect(capturedState).toBe('close');
expect(capturedElement).toBe(element);
}));
they('should remove all event listeners when the element is removed via $prop',
['leave()', 'remove()'], function(method) {
inject(function($animate, $rootScope, $rootElement, $$rAF) {
element = jqLite('');
var count = 0;
var enterSpy = jasmine.createSpy();
var addClassSpy = jasmine.createSpy();
var runner;
$animate.on('enter', element, enterSpy);
$animate.on('addClass', element[0], addClassSpy);
function counter(element, phase) {
if (phase === 'start') {
count++;
}
}
runner = $animate.enter(element, $rootElement);
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
$animate.flush();
expect(enterSpy.calls.count()).toBe(1);
expect(enterSpy.calls.mostRecent().args[1]).toBe('start');
runner.end(); // Otherwise the class animation won't run because enter is still in progress
$$rAF.flush();
expect(enterSpy.calls.count()).toBe(2);
expect(enterSpy.calls.mostRecent().args[1]).toBe('close');
enterSpy.calls.reset();
capturedAnimation = null;
runner = $animate.addClass(element, 'blue');
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
$animate.flush();
expect(addClassSpy.calls.count()).toBe(1);
expect(addClassSpy.calls.mostRecent().args[1]).toBe('start');
runner.end();
$$rAF.flush();
expect(addClassSpy.calls.count()).toBe(2);
expect(addClassSpy.calls.mostRecent().args[1]).toBe('close');
addClassSpy.calls.reset();
capturedAnimation = null;
if (method === 'leave()') {
runner = $animate.leave(element);
$animate.flush();
runner.end();
} else if (method === 'remove()') {
element.remove();
}
runner = $animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.flush();
expect(enterSpy.calls.count()).toBe(0);
runner.end(); // Otherwise the class animation won't run because enter is still in progress
expect(function() {
$$rAF.flush();
}).toThrowError('No rAF callbacks present'); // Try to flush any callbacks
expect(enterSpy.calls.count()).toBe(0);
capturedAnimation = null;
$animate.addClass(element, 'red');
$rootScope.$digest();
expect(capturedAnimation).toBeTruthy();
$animate.flush();
expect(addClassSpy.calls.count()).toBe(0);
runner.end();
expect(function() {
$$rAF.flush();
}).toThrowError('No rAF callbacks present'); // Try to flush any callbacks
expect(addClassSpy.calls.count()).toBe(0);
expect(enterSpy.calls.count()).toBe(0);
});
});
it('should always detect registered callbacks after one postDigest has fired',
inject(function($animate, $rootScope, $rootElement) {
element = jqLite('');
var spy = jasmine.createSpy();
registerCallback();
var runner = $animate.enter(element, $rootElement);
registerCallback();
$rootScope.$digest();
registerCallback();
expect(spy).not.toHaveBeenCalled();
$animate.flush();
// this is not 3 since the 3rd callback
// was added after the first callback
// was fired
expect(spy).toHaveBeenCalledTimes(2);
spy.calls.reset();
runner.end();
$animate.flush();
// now we expect all three callbacks
// to fire when the animation ends since
// the callback detection happens again
expect(spy).toHaveBeenCalledTimes(3);
function registerCallback() {
$animate.on('enter', element, spy);
}
}));
it('should use RAF if there are detected callbacks within the hierarchy of the element being animated',
inject(function($animate, $rootScope, $rootElement, $$rAF) {
var runner;
element = jqLite('');
runner = $animate.enter(element, $rootElement);
$rootScope.$digest();
runner.end();
assertRAFsUsed(false);
var spy = jasmine.createSpy();
$animate.on('leave', element, spy);
runner = $animate.leave(element, $rootElement);
$rootScope.$digest();
runner.end();
assertRAFsUsed(true);
function assertRAFsUsed(bool) {
expect($$rAF.queue.length)[bool ? 'toBeGreaterThan' : 'toBe'](0);
}
}));
describe('for leave', function() {
it('should remove the element even if another animation is called afterwards',
inject(function($animate, $rootScope, $rootElement) {
var outerContainer = jqLite('');
element = jqLite('');
outerContainer.append(element);
$rootElement.append(outerContainer);
var runner = $animate.leave(element, $rootElement);
$animate.removeClass(element,'rclass');
$rootScope.$digest();
runner.end();
$animate.flush();
var isElementRemoved = !outerContainer[0].contains(element[0]);
expect(isElementRemoved).toBe(true);
}));
they('should trigger callbacks when the listener is on the $prop element', ['same', 'parent'],
function(elementRelation) {
inject(function($animate, $rootScope, $$rAF, $rootElement, $document) {
var listenerElement, callbackSpy = jasmine.createSpy();
element = jqLite('');
listenerElement = elementRelation === 'same' ? element : jqLite($document[0].body);
$animate.on('leave', listenerElement, callbackSpy);
$rootElement.append(element);
var runner = $animate.leave(element, $rootElement);
$rootScope.$digest();
$$rAF.flush();
expect(callbackSpy.calls.count()).toBe(1);
expect(callbackSpy.calls.mostRecent().args[1]).toBe('start');
callbackSpy.calls.reset();
runner.end();
$$rAF.flush();
expect(callbackSpy.calls.count()).toBe(1);
expect(callbackSpy.calls.mostRecent().args[1]).toBe('close');
});
}
);
it('should trigger callbacks for a leave animation',
inject(function($animate, $rootScope, $$rAF, $rootElement, $document) {
var callbackSpy = jasmine.createSpy();
$animate.on('leave', jqLite($document[0].body), callbackSpy);
element = jqLite('');
$rootElement.append(element);
$animate.leave(element, $rootElement);
$rootScope.$digest();
$$rAF.flush();
expect(callbackSpy).toHaveBeenCalled();
expect(callbackSpy.calls.count()).toBe(1);
}));
it('should trigger a callback for an leave animation (same element)',
inject(function($animate, $rootScope, $$rAF, $rootElement, $document) {
var callbackSpy = jasmine.createSpy();
element = jqLite('');
$animate.on('leave', element, callbackSpy);
$rootElement.append(element);
var runner = $animate.leave(element, $rootElement);
$rootScope.$digest();
$$rAF.flush();
expect(callbackSpy.calls.count()).toBe(1);
expect(callbackSpy.calls.mostRecent().args[1]).toBe('start');
callbackSpy.calls.reset();
runner.end();
$$rAF.flush();
expect(callbackSpy.calls.count()).toBe(1);
expect(callbackSpy.calls.mostRecent().args[1]).toBe('close');
}));
it('should not fire a callback if the element is outside of the given container',
inject(function($animate, $rootScope, $$rAF, $rootElement) {
var callbackTriggered = false;
var innerContainer = jqLite('');
$rootElement.append(innerContainer);
$animate.on('leave', innerContainer,
function(element, phase, data) {
callbackTriggered = true;
});
element = jqLite('');
$rootElement.append(element);
$animate.leave(element, $rootElement);
$rootScope.$digest();
expect(callbackTriggered).toBe(false);
}));
it('should fire a `start` callback when the animation starts',
inject(function($animate, $rootScope, $$rAF, $rootElement, $document) {
element = jqLite('');
var capturedState;
var capturedElement;
$animate.on('leave', jqLite($document[0].body), function(element, phase) {
capturedState = phase;
capturedElement = element;
});
$rootElement.append(element);
$animate.leave(element, $rootElement);
$rootScope.$digest();
$$rAF.flush();
expect(capturedState).toBe('start');
expect(capturedElement).toBe(element);
}));
it('should fire a `close` callback when the animation ends',
inject(function($animate, $rootScope, $$rAF, $rootElement, $document) {
element = jqLite('');
var capturedState;
var capturedElement;
$animate.on('leave', jqLite($document[0].body), function(element, phase) {
capturedState = phase;
capturedElement = element;
});
$rootElement.append(element);
var runner = $animate.leave(element, $rootElement);
$rootScope.$digest();
runner.end();
$$rAF.flush();
expect(capturedState).toBe('close');
expect(capturedElement).toBe(element);
}));
it('should remove all event listeners after all callbacks for the "leave:close" phase have been called',
inject(function($animate, $rootScope, $rootElement, $$rAF) {
var leaveSpy = jasmine.createSpy();
var addClassSpy = jasmine.createSpy();
element = jqLite('');
$animate.on('leave', element, leaveSpy);
$animate.on('addClass', element, addClassSpy);
$rootElement.append(element);
var runner = $animate.leave(element, $rootElement);
$animate.flush();
runner.end();
$$rAF.flush();
expect(leaveSpy.calls.mostRecent().args[1]).toBe('close');
$animate.addClass(element, 'blue');
$animate.flush();
runner.end();
expect(function() {
$$rAF.flush();
}).toThrowError('No rAF callbacks present');
expect(addClassSpy.calls.count()).toBe(0);
}));
});
describe('event data', function() {
it('should be included for enter',
inject(function($animate, $rootScope, $rootElement, $document) {
var eventData;
$animate.on('enter', jqLite($document[0].body), function(element, phase, data) {
eventData = data;
});
element = jqLite('');
$animate.enter(element, $rootElement, null, {
addClass: 'red blue',
removeClass: 'yellow green',
from: {opacity: 0},
to: {opacity: 1}
});
$rootScope.$digest();
$animate.flush();
expect(eventData).toEqual({
addClass: 'red blue',
removeClass: null,
from: {opacity: 0},
to: {opacity: 1}
});
}));
it('should be included for leave',
inject(function($animate, $rootScope, $rootElement, $document) {
var eventData;
$animate.on('leave', jqLite($document[0].body), function(element, phase, data) {
eventData = data;
});
var outerContainer = jqLite('');
element = jqLite('');
outerContainer.append(element);
$rootElement.append(outerContainer);
$animate.leave(element, {
addClass: 'red blue',
removeClass: 'yellow green',
from: {opacity: 0},
to: {opacity: 1}
});
$animate.flush();
expect(eventData).toEqual({
addClass: 'red blue',
removeClass: null,
from: {opacity: 0},
to: {opacity: 1}
});
})
);
it('should be included for move',
inject(function($animate, $rootScope, $rootElement, $document) {
var eventData;
$animate.on('move', jqLite($document[0].body), function(element, phase, data) {
eventData = data;
});
var parent = jqLite('');
var parent2 = jqLite('');
element = jqLite('');
parent.append(element);
$rootElement.append(parent);
$rootElement.append(parent2);
$animate.move(element, parent2, null, {
addClass: 'red blue',
removeClass: 'yellow green',
from: {opacity: 0},
to: {opacity: 1}
});
$animate.flush();
expect(eventData).toEqual({
addClass: 'red blue',
removeClass: null,
from: {opacity: 0},
to: {opacity: 1}
});
})
);
it('should be included for addClass', inject(function($animate, $rootElement) {
var eventData;
element = jqLite('');
$animate.on('addClass', element, function(element, phase, data) {
eventData = data;
});
$rootElement.append(element);
$animate.addClass(element, 'red blue', {
from: {opacity: 0},
to: {opacity: 1}
});
$animate.flush();
expect(eventData).toEqual({
addClass: 'red blue',
removeClass: null,
from: {opacity: 0},
to: {opacity: 1}
});
}));
it('should be included for removeClass', inject(function($animate, $rootElement) {
var eventData;
element = jqLite('');
$animate.on('removeClass', element, function(element, phase, data) {
eventData = data;
});
$rootElement.append(element);
$animate.removeClass(element, 'red blue', {
from: {opacity: 0},
to: {opacity: 1}
});
$animate.flush();
expect(eventData).toEqual({
removeClass: 'red blue',
addClass: null,
from: {opacity: 0},
to: {opacity: 1}
});
}));
it('should be included for setClass', inject(function($animate, $rootElement) {
var eventData;
element = jqLite('');
$animate.on('setClass', element, function(element, phase, data) {
eventData = data;
});
$rootElement.append(element);
$animate.setClass(element, 'red blue', 'yellow green', {
from: {opacity: 0},
to: {opacity: 1}
});
$animate.flush();
expect(eventData).toEqual({
addClass: 'red blue',
removeClass: 'yellow green',
from: {opacity: 0},
to: {opacity: 1}
});
}));
it('should be included for animate', inject(function($animate, $rootElement) {
// The event for animate changes to 'setClass' if both addClass and removeClass
// are definded, because the operations are merged. However, it is still 'animate'
// and not 'addClass' if only 'addClass' is defined.
// Ideally, we would make this consistent, but it's a BC
var eventData, eventName;
element = jqLite('');
$animate.on('setClass', element, function(element, phase, data) {
eventData = data;
eventName = 'setClass';
});
$animate.on('animate', element, function(element, phase, data) {
eventData = data;
eventName = 'animate';
});
$rootElement.append(element);
var runner = $animate.animate(element, {opacity: 0}, {opacity: 1}, null, {
addClass: 'red blue',
removeClass: 'yellow green'
});
$animate.flush();
runner.end();
expect(eventName).toBe('setClass');
expect(eventData).toEqual({
addClass: 'red blue',
removeClass: 'yellow green',
from: {opacity: 0},
to: {opacity: 1}
});
eventData = eventName = null;
runner = $animate.animate(element, {opacity: 0}, {opacity: 1}, null, {
addClass: 'yellow green'
});
$animate.flush();
runner.end();
expect(eventName).toBe('animate');
expect(eventData).toEqual({
addClass: 'yellow green',
removeClass: null,
from: {opacity: 0},
to: {opacity: 1}
});
eventData = eventName = null;
runner = $animate.animate(element, {opacity: 0}, {opacity: 1}, null, {
removeClass: 'yellow green'
});
$animate.flush();
runner.end();
expect(eventName).toBe('animate');
expect(eventData).toEqual({
addClass: null,
removeClass: 'yellow green',
from: {opacity: 0},
to: {opacity: 1}
});
}));
});
they('should trigger a callback for a $prop animation if the listener is on the document',
['enter', 'leave'], function($event) {
module(function($provide) {
$provide.factory('$rootElement', function($document) {
// Since we listen on document, $document must be the $rootElement for animations to work
return $document;
});
});
inject(function($animate, $rootScope, $document) {
var callbackTriggered = false;
$animate.on($event, $document[0], function() {
callbackTriggered = true;
});
var container = jqLite('');
jqLite($document[0].body).append(container);
element = jqLite('');
if ($event === 'leave') {
container.append(element);
}
$animate[$event](element, container);
$rootScope.$digest();
$animate.flush();
expect(callbackTriggered).toBe(true);
});
});
describe('when animations are skipped, disabled, or invalid', function() {
var overriddenAnimationRunner;
var capturedAnimation;
var capturedAnimationHistory;
var defaultFakeAnimationRunner;
var parent;
var parent2;
beforeEach(module(function($provide) {
overriddenAnimationRunner = null;
capturedAnimation = null;
capturedAnimationHistory = [];
$provide.value('$$animation', function() {
capturedAnimationHistory.push(capturedAnimation = arguments);
return overriddenAnimationRunner || defaultFakeAnimationRunner;
});
return function($rootElement, $q, $animate, $$AnimateRunner, $document) {
defaultFakeAnimationRunner = new $$AnimateRunner();
element = jqLite('
element
');
parent = jqLite('
parent
');
parent2 = jqLite('
parent
');
$rootElement.append(parent);
$rootElement.append(parent2);
};
}));
it('should trigger all callbacks if a follow-up structural animation takes over a running animation',
inject(function($animate, $rootScope) {
parent.append(element);
var moveSpy = jasmine.createSpy();
var leaveSpy = jasmine.createSpy();
$animate.on('move', parent2, moveSpy);
$animate.on('leave', parent2, leaveSpy);
$animate.move(element, parent2);
$rootScope.$digest();
$animate.flush();
expect(moveSpy.calls.count()).toBe(1);
expect(moveSpy.calls.mostRecent().args[1]).toBe('start');
$animate.leave(element);
$rootScope.$digest();
$animate.flush();
expect(moveSpy.calls.count()).toBe(2);
expect(moveSpy.calls.mostRecent().args[1]).toBe('close');
expect(leaveSpy.calls.count()).toBe(2);
expect(leaveSpy.calls.argsFor(0)[1]).toBe('start');
expect(leaveSpy.calls.argsFor(1)[1]).toBe('close');
}));
it('should not trigger callbacks for the previous structural animation if a follow-up structural animation takes over before the postDigest',
inject(function($animate, $rootScope) {
var enterDone = jasmine.createSpy('enter animation done');
var enterSpy = jasmine.createSpy();
var leaveSpy = jasmine.createSpy();
$animate.on('enter', parent, enterSpy);
$animate.on('leave', parent, leaveSpy);
$animate.enter(element, parent).done(enterDone);
expect(enterDone).not.toHaveBeenCalled();
var runner = $animate.leave(element);
$animate.flush();
expect(enterDone).toHaveBeenCalled();
expect(enterSpy).not.toHaveBeenCalled();
expect(leaveSpy.calls.count()).toBe(1);
expect(leaveSpy.calls.mostRecent().args[1]).toBe('start');
leaveSpy.calls.reset();
runner.end();
$animate.flush();
expect(enterSpy).not.toHaveBeenCalled();
expect(leaveSpy.calls.count()).toBe(1);
expect(leaveSpy.calls.mostRecent().args[1]).toBe('close');
}));
it('should not trigger the callback if animations are disabled on the element',
inject(function($animate, $rootScope, $rootElement, $document) {
var callbackTriggered = false;
var spy = jasmine.createSpy('enter');
$animate.on('enter', jqLite($document[0].body), spy);
element = jqLite('');
$animate.enabled(element, false);
var runner = $animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.flush(); // Flushes the animation frames for the callbacks
expect(spy).not.toHaveBeenCalled();
}));
it('should not trigger the callbacks if the animation is skipped because there are no class-based animations and no structural animation',
inject(function($animate, $rootScope) {
parent.append(element);
var classSpy = jasmine.createSpy('classChange');
$animate.on('addClass', element, classSpy);
$animate.on('removeClass', element, classSpy);
element.addClass('one three');
$animate.addClass(element, 'one');
$animate.removeClass(element, 'four');
$rootScope.$digest();
$animate.flush();
expect(classSpy).not.toHaveBeenCalled();
}));
describe('because the document is hidden', function() {
var hidden = true;
beforeEach(function() {
module(function($provide) {
$provide.value('$$isDocumentHidden', function() {
return hidden;
});
});
});
it('should trigger callbacks for an enter animation',
inject(function($animate, $rootScope, $rootElement, $document) {
var spy = jasmine.createSpy();
$animate.on('enter', jqLite($document[0].body), spy);
element = jqLite('');
var runner = $animate.enter(element, $rootElement);
$rootScope.$digest();
$animate.flush(); // Flushes the animation frames for the callbacks
expect(spy.calls.count()).toBe(2);
expect(spy.calls.argsFor(0)[1]).toBe('start');
expect(spy.calls.argsFor(1)[1]).toBe('close');
}));
});
});
});
});