'use strict'; describe('$$animation', function() { beforeEach(module('ngAnimate')); beforeEach(module('ngAnimateMock')); 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, $animate, $rootScope) { element = jqLite('
'); var done = false; $$animation(element, 'someEvent').then(function() { done = true; }); $animate.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, $animate, $rootScope) { element = jqLite(''); var parent = jqLite(''); parent.append(element); var done = false; $$animation(element, 'someEvent').then(function() { done = true; }); $rootScope.$digest(); $animate.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, $animate) { var status; var element = jqLite(''); var parent = jqLite(''); parent.append(element); var runner = $$animation(element, 'enter'); runner.then(function() { status = 'resolve'; }, function() { status = 'reject'; }); // the animation is started $rootScope.$digest(); if (event === 'resolve') { runner.end(); } else { runner.cancel(); } // the resolve/rejection digest $animate.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); }; })); it('should space out multiple ancestorial class-based animations with a RAF in between', inject(function($rootScope, $$animation, $$rAF) { var parent = element; element = jqLite(''); parent.append(element); var child = jqLite(''); element.append(child); $$animation(parent, 'addClass', { addClass: 'blue' }); $$animation(element, 'addClass', { addClass: 'red' }); $$animation(child, 'addClass', { addClass: 'green' }); $rootScope.$digest(); expect(captureLog.length).toBe(1); expect(capturedAnimation.options.addClass).toBe('blue'); $$rAF.flush(); expect(captureLog.length).toBe(2); expect(capturedAnimation.options.addClass).toBe('red'); $$rAF.flush(); expect(captureLog.length).toBe(3); expect(capturedAnimation.options.addClass).toBe('green'); })); it('should properly cancel out pending animations that are spaced with a RAF request before the digest completes', inject(function($rootScope, $$animation, $$rAF) { var parent = element; element = jqLite(''); parent.append(element); var child = jqLite(''); element.append(child); var r1 = $$animation(parent, 'addClass', { addClass: 'blue' }); var r2 = $$animation(element, 'addClass', { addClass: 'red' }); var r3 = $$animation(child, 'addClass', { addClass: 'green' }); r2.end(); $rootScope.$digest(); expect(captureLog.length).toBe(1); expect(capturedAnimation.options.addClass).toBe('blue'); $$rAF.flush(); expect(captureLog.length).toBe(2); expect(capturedAnimation.options.addClass).toBe('green'); })); it('should properly cancel out pending animations that are spaced with a RAF request after the digest completes', inject(function($rootScope, $$animation, $$rAF) { var parent = element; element = jqLite(''); parent.append(element); var child = jqLite(''); element.append(child); var r1 = $$animation(parent, 'addClass', { addClass: 'blue' }); var r2 = $$animation(element, 'addClass', { addClass: 'red' }); var r3 = $$animation(child, 'addClass', { addClass: 'green' }); $rootScope.$digest(); r2.end(); expect(captureLog.length).toBe(1); expect(capturedAnimation.options.addClass).toBe('blue'); $$rAF.flush(); expect(captureLog.length).toBe(1); $$rAF.flush(); expect(captureLog.length).toBe(2); expect(capturedAnimation.options.addClass).toBe('green'); })); 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, $animate) { var status; var runner = $$animation(element, 'someEvent'); runner.then(function() { status = 'end'; }, function() { status = 'cancel'; }); runner[method](); $rootScope.$digest(); expect(runnerLog).toEqual([]); $animate.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, $animate, $rootScope) { var runner = $$animation(element, 'someEvent'); $rootScope.$digest(); 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, $animate, $rootScope) { var runner = $$animation(element, 'leave', {}, function() { element.remove(); }); $rootScope.$digest(); 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, $rootScope) { var addListen = spyOn(element, 'on').and.callThrough(); var removeListen = spyOn(element, 'off').and.callThrough(); var runner = $$animation(element, 'someEvent'); var args = addListen.calls.mostRecent().args[0]; expect(args).toBe('$destroy'); runner.end(); args = removeListen.calls.mostRecent().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, $rootScope, $animate) { 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(); $animate.flush(); expect(captureLog[0].element).toBe(element); expect(captureLog[1].element).toBe(child); expect(captureLog[2].element).toBe(grandchild); })); they('should only apply the ng-$prop-prepare class if there are a child animations', ['enter', 'leave', 'move'], function(animationType) { inject(function($$animation, $rootScope, $animate) { var expectedClassName = 'ng-' + animationType + '-prepare'; $$animation(element, animationType); $rootScope.$digest(); expect(element).not.toHaveClass(expectedClassName); var child = jqLite(''); element.append(child); $$animation(element, animationType); $$animation(child, animationType); $rootScope.$digest(); expect(element).not.toHaveClass(expectedClassName); expect(child).toHaveClass(expectedClassName); }); }); they('should remove the preparation class before the $prop-animation starts', ['enter', 'leave', 'move'], function(animationType) { inject(function($$animation, $rootScope, $$rAF) { var expectedClassName = 'ng-' + animationType + '-prepare'; var child = jqLite(''); element.append(child); $$animation(element, animationType); $$animation(child, animationType); $rootScope.$digest(); expect(element).not.toHaveClass(expectedClassName); expect(child).toHaveClass(expectedClassName); $$rAF.flush(); expect(element).not.toHaveClass(expectedClassName); expect(child).not.toHaveClass(expectedClassName); }); }); }); describe('grouped', function() { var fromElement; var toElement; var fromAnchors; var toAnchors; beforeEach(module(function($provide) { fromElement = jqLite(''); toElement = jqLite(''); fromAnchors = [ jqLite('