'use strict'; describe('ngAnimate $animateCss', function() { beforeEach(module('ngAnimate')); beforeEach(module('ngAnimateMock')); function assertAnimationRunning(element, not) { var className = element.attr('class'); var regex = /\b\w+-active\b/; if (not) { expect(className).toMatch(regex); } else { expect(className).not.toMatch(regex); } } function getPossiblyPrefixedStyleValue(element, styleProp) { var value = element.css(styleProp); if (isUndefined(value)) value = element.css('-webkit-' + styleProp); return value; } function keyframeProgress(element, duration, delay) { browserTrigger(element, 'animationend', { timeStamp: Date.now() + ((delay || 1) * 1000), elapsedTime: duration }); } function transitionProgress(element, duration, delay) { browserTrigger(element, 'transitionend', { timeStamp: Date.now() + ((delay || 1) * 1000), elapsedTime: duration }); } function isPromiseLike(p) { return !!(p && p.then); } var fakeStyle = { color: 'blue' }; var ss, triggerAnimationStartFrame; beforeEach(module(function() { return function($document, $sniffer, $$rAF, $animate) { ss = createMockStyleSheet($document); $animate.enabled(true); triggerAnimationStartFrame = function() { $$rAF.flush(); }; }; })); afterEach(function() { if (ss) { ss.destroy(); } }); it('should return false if neither transitions or keyframes are supported by the browser', inject(function($animateCss, $sniffer, $rootElement, $document) { var animator; var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); $sniffer.transitions = $sniffer.animations = false; animator = $animateCss(element, { duration: 10, to: { 'background': 'red' } }); expect(animator.$$willAnimate).toBeFalsy(); })); describe('when active', function() { if (!browserSupportsCssAnimations()) return; it('should not attempt an animation if animations are globally disabled', inject(function($animateCss, $animate, $rootElement, $document) { $animate.enabled(false); var animator, element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); animator = $animateCss(element, { duration: 10, to: { 'height': '100px' } }); expect(animator.$$willAnimate).toBeFalsy(); })); it('should silently quit the animation and not throw when an element has no parent during preparation', inject(function($animateCss, $rootScope, $document, $rootElement) { var element = angular.element('
'); expect(function() { $animateCss(element, { duration: 1000, event: 'fake', to: fakeStyle }).start(); }).not.toThrow(); expect(element).not.toHaveClass('fake'); triggerAnimationStartFrame(); expect(element).not.toHaveClass('fake-active'); })); it('should silently quit the animation and not throw when an element has no parent before starting', inject(function($animateCss, $$rAF, $rootScope, $document, $rootElement) { var element = angular.element('
'); angular.element($document[0].body).append($rootElement); $rootElement.append(element); $animateCss(element, { duration: 1000, addClass: 'wait-for-it', to: fakeStyle }).start(); element.remove(); expect(function() { triggerAnimationStartFrame(); }).not.toThrow(); })); describe('rAF usage', function() { it('should buffer all requests into a single requestAnimationFrame call', inject(function($animateCss, $$rAF, $rootScope, $document, $rootElement) { angular.element($document[0].body).append($rootElement); var count = 0; var runners = []; function makeRequest() { var element = angular.element('
'); $rootElement.append(element); var runner = $animateCss(element, { duration: 5, to: fakeStyle }).start(); runner.then(function() { count++; }); runners.push(runner); } makeRequest(); makeRequest(); makeRequest(); expect(count).toBe(0); triggerAnimationStartFrame(); forEach(runners, function(runner) { runner.end(); }); $rootScope.$digest(); expect(count).toBe(3); })); it('should cancel previous requests to rAF to avoid premature flushing', function() { var count = 0; module(function($provide) { $provide.value('$$rAF', function() { return function cancellationFn() { count++; }; }); }); inject(function($animateCss, $$rAF, $document, $rootElement) { angular.element($document[0].body).append($rootElement); function makeRequest() { var element = angular.element('
'); $rootElement.append(element); $animateCss(element, { duration: 5, to: fakeStyle }).start(); } makeRequest(); makeRequest(); makeRequest(); expect(count).toBe(2); }); }); }); describe('animator and runner', function() { var animationDuration = 5; var element, animator; beforeEach(inject(function($animateCss, $rootElement, $document) { element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); animator = $animateCss(element, { event: 'enter', structural: true, duration: animationDuration, to: fakeStyle }); })); it('should expose start and end functions for the animator object', inject(function() { expect(typeof animator.start).toBe('function'); expect(typeof animator.end).toBe('function'); })); it('should expose end, cancel, resume and pause methods on the runner object', inject(function() { var runner = animator.start(); triggerAnimationStartFrame(); expect(typeof runner.end).toBe('function'); expect(typeof runner.cancel).toBe('function'); expect(typeof runner.resume).toBe('function'); expect(typeof runner.pause).toBe('function'); })); it('should start the animation', inject(function() { expect(element).not.toHaveClass('ng-enter-active'); animator.start(); triggerAnimationStartFrame(); expect(element).toHaveClass('ng-enter-active'); })); it('should end the animation when called from the animator object', inject(function() { animator.start(); triggerAnimationStartFrame(); animator.end(); expect(element).not.toHaveClass('ng-enter-active'); })); it('should end the animation when called from the runner object', inject(function() { var runner = animator.start(); triggerAnimationStartFrame(); runner.end(); expect(element).not.toHaveClass('ng-enter-active'); })); it('should permanently close the animation if closed before the next rAF runs', inject(function() { var runner = animator.start(); runner.end(); triggerAnimationStartFrame(); expect(element).not.toHaveClass('ng-enter-active'); })); it('should return a runner object at the start of the animation that contains a `then` method', inject(function($rootScope) { var runner = animator.start(); triggerAnimationStartFrame(); expect(isPromiseLike(runner)).toBeTruthy(); var resolved; runner.then(function() { resolved = true; }); runner.end(); $rootScope.$digest(); expect(resolved).toBeTruthy(); })); it('should cancel the animation and reject', inject(function($rootScope) { var rejected; var runner = animator.start(); triggerAnimationStartFrame(); runner.then(noop, function() { rejected = true; }); runner.cancel(); $rootScope.$digest(); expect(rejected).toBeTruthy(); })); it('should run pause, but not effect the transition animation', inject(function() { var blockingDelay = '-' + animationDuration + 's'; expect(element.css('transition-delay')).toEqual(blockingDelay); var runner = animator.start(); triggerAnimationStartFrame(); expect(element.css('transition-delay')).not.toEqual(blockingDelay); runner.pause(); expect(element.css('transition-delay')).not.toEqual(blockingDelay); })); it('should pause the transition, have no effect, but not end it', inject(function() { var runner = animator.start(); triggerAnimationStartFrame(); runner.pause(); browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 5 }); expect(element).toHaveClass('ng-enter-active'); })); it('should resume the animation', inject(function() { var runner = animator.start(); triggerAnimationStartFrame(); runner.pause(); browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 5 }); expect(element).toHaveClass('ng-enter-active'); runner.resume(); expect(element).not.toHaveClass('ng-enter-active'); })); it('should pause and resume a keyframe animation using animation-play-state', inject(function($animateCss) { element.attr('style', ''); ss.addPossiblyPrefixedRule('.ng-enter', 'animation:1.5s keyframe_animation;'); animator = $animateCss(element, { event: 'enter', structural: true }); var runner = animator.start(); triggerAnimationStartFrame(); runner.pause(); expect(getPossiblyPrefixedStyleValue(element, 'animation-play-state')).toEqual('paused'); runner.resume(); expect(element.attr('style')).toBeFalsy(); })); it('should remove the animation-play-state style if the animation is closed', inject(function($animateCss) { element.attr('style', ''); ss.addPossiblyPrefixedRule('.ng-enter', 'animation:1.5s keyframe_animation;'); animator = $animateCss(element, { event: 'enter', structural: true }); var runner = animator.start(); triggerAnimationStartFrame(); runner.pause(); expect(getPossiblyPrefixedStyleValue(element, 'animation-play-state')).toEqual('paused'); runner.end(); expect(element.attr('style')).toBeFalsy(); })); }); describe('CSS', function() { describe('detected styles', function() { var element, options; function assertAnimationComplete(bool) { var assert = expect(element); if (bool) { assert = assert.not; } assert.toHaveClass('ng-enter'); assert.toHaveClass('ng-enter-active'); } beforeEach(inject(function($rootElement, $document) { element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); options = { event: 'enter', structural: true }; })); it('should always return an object even if no animation is detected', inject(function($animateCss) { ss.addRule('.some-animation', 'background:red;'); element.addClass('some-animation'); var animator = $animateCss(element, options); expect(animator).toBeTruthy(); expect(isFunction(animator.start)).toBeTruthy(); expect(animator.end).toBeTruthy(); expect(animator.$$willAnimate).toBe(false); })); it('should close the animation immediately, but still return an animator object if no animation is detected', inject(function($animateCss) { ss.addRule('.another-fake-animation', 'background:blue;'); element.addClass('another-fake-animation'); var animator = $animateCss(element, { event: 'enter', structural: true }); expect(element).not.toHaveClass('ng-enter'); expect(isFunction(animator.start)).toBeTruthy(); })); they('should close the animation, but still accept $prop callbacks if no animation is detected', ['done', 'then'], function(method) { inject(function($animateCss, $animate, $rootScope) { ss.addRule('.the-third-fake-animation', 'background:green;'); element.addClass('another-fake-animation'); var animator = $animateCss(element, { event: 'enter', structural: true }); var done = false; animator.start()[method](function() { done = true; }); expect(done).toBe(false); $animate.flush(); if (method === 'then') { $rootScope.$digest(); } expect(done).toBe(true); }); }); they('should close the animation, but still accept recognize runner.$prop if no animation is detected', ['done(cancel)', 'catch'], function(method) { inject(function($animateCss, $rootScope) { ss.addRule('.the-third-fake-animation', 'background:green;'); element.addClass('another-fake-animation'); var animator = $animateCss(element, { event: 'enter', structural: true }); var cancelled = false; var runner = animator.start(); if (method === 'catch') { runner.catch(function() { cancelled = true; }); } else { runner.done(function(status) { cancelled = status === false; }); } expect(cancelled).toBe(false); runner.cancel(); if (method === 'catch') { $rootScope.$digest(); } expect(cancelled).toBe(true); }); }); it('should use the highest transition duration value detected in the CSS class', inject(function($animateCss) { ss.addPossiblyPrefixedRule('.ng-enter', 'transition:1s linear all;' + 'transition-duration:10s, 15s, 20s;'); var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); transitionProgress(element, 10); assertAnimationComplete(false); transitionProgress(element, 15); assertAnimationComplete(false); transitionProgress(element, 20); assertAnimationComplete(true); })); it('should use the highest transition delay value detected in the CSS class', inject(function($animateCss) { ss.addPossiblyPrefixedRule('.ng-enter', 'transition:1s linear all;' + 'transition-delay:10s, 15s, 20s;'); var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); transitionProgress(element, 1, 10); assertAnimationComplete(false); transitionProgress(element, 1, 15); assertAnimationComplete(false); transitionProgress(element, 1, 20); assertAnimationComplete(true); })); it('should only close when both the animation delay and duration have passed', inject(function($animateCss) { ss.addPossiblyPrefixedRule('.ng-enter', 'transition:10s 5s linear all;'); var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); transitionProgress(element, 10, 2); assertAnimationComplete(false); transitionProgress(element, 9, 6); assertAnimationComplete(false); transitionProgress(element, 10, 5); assertAnimationComplete(true); })); it('should not close a transition when a child element fires the transitionend event', inject(function($animateCss) { ss.addPossiblyPrefixedRule('.ng-enter', 'transition:4s linear all;'); ss.addPossiblyPrefixedRule('.non-angular-animation', 'transition:5s linear all;'); var child = angular.element('
'); element.append(child); var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); browserTrigger(child, 'transitionend', { timeStamp: Date.now(), elapsedTime: 5, bubbles: true }); transitionProgress(element, 1); assertAnimationComplete(false); transitionProgress(element, 4); assertAnimationComplete(true); })); it('should not close a keyframe animation when a child element fires the animationend event', inject(function($animateCss) { ss.addPossiblyPrefixedRule('.ng-enter', 'animation:animation 4s;'); ss.addPossiblyPrefixedRule('.non-angular-animation', 'animation:animation 5s;'); var child = angular.element('
'); element.append(child); var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); browserTrigger(child, 'animationend', { timeStamp: Date.now(), elapsedTime: 5, bubbles: true }); keyframeProgress(element, 1); assertAnimationComplete(false); keyframeProgress(element, 4); assertAnimationComplete(true); })); it('should use the highest keyframe duration value detected in the CSS class', inject(function($animateCss) { ss.addPossiblyPrefixedRule('.ng-enter', 'animation:animation 1s, animation 2s, animation 3s;'); var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); keyframeProgress(element, 1); assertAnimationComplete(false); keyframeProgress(element, 2); assertAnimationComplete(false); keyframeProgress(element, 3); assertAnimationComplete(true); })); it('should use the highest keyframe delay value detected in the CSS class', inject(function($animateCss) { ss.addPossiblyPrefixedRule('.ng-enter', 'animation:animation 1s 2s, animation 1s 10s, animation 1s 1000ms;'); var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); keyframeProgress(element, 1, 1); assertAnimationComplete(false); keyframeProgress(element, 1, 2); assertAnimationComplete(false); keyframeProgress(element, 1, 10); assertAnimationComplete(true); })); it('should use the highest keyframe duration value detected in the CSS class with respect to the animation-iteration-count property', inject(function($animateCss) { ss.addPossiblyPrefixedRule('.ng-enter', 'animation:animation 1s 2s 3, animation 1s 10s 2, animation 1s 1000ms infinite;'); var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); keyframeProgress(element, 1, 1); assertAnimationComplete(false); keyframeProgress(element, 1, 2); assertAnimationComplete(false); keyframeProgress(element, 3, 10); assertAnimationComplete(true); })); it('should use the highest duration value when both transitions and keyframes are used', inject(function($animateCss) { ss.addPossiblyPrefixedRule('.ng-enter', 'transition:1s linear all;' + 'transition-duration:10s, 15s, 20s;' + 'animation:animation 1s, animation 2s, animation 3s 0s 7;'); var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); transitionProgress(element, 10); keyframeProgress(element, 10); assertAnimationComplete(false); transitionProgress(element, 15); keyframeProgress(element, 15); assertAnimationComplete(false); transitionProgress(element, 20); keyframeProgress(element, 20); assertAnimationComplete(false); // 7 * 3 = 21 transitionProgress(element, 21); keyframeProgress(element, 21); assertAnimationComplete(true); })); it('should use the highest delay value when both transitions and keyframes are used', inject(function($animateCss) { ss.addPossiblyPrefixedRule('.ng-enter', 'transition:1s linear all;' + 'transition-delay:10s, 15s, 20s;' + 'animation:animation 1s 2s, animation 1s 16s, animation 1s 19s;'); var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); transitionProgress(element, 1, 10); keyframeProgress(element, 1, 10); assertAnimationComplete(false); transitionProgress(element, 1, 16); keyframeProgress(element, 1, 16); assertAnimationComplete(false); transitionProgress(element, 1, 19); keyframeProgress(element, 1, 19); assertAnimationComplete(false); transitionProgress(element, 1, 20); keyframeProgress(element, 1, 20); assertAnimationComplete(true); })); }); describe('staggering', function() { it('should apply a stagger based when an active ng-EVENT-stagger class with a transition-delay is detected', inject(function($animateCss, $document, $rootElement, $timeout) { angular.element($document[0].body).append($rootElement); ss.addPossiblyPrefixedRule('.ng-enter-stagger', 'transition-delay:0.2s'); ss.addPossiblyPrefixedRule('.ng-enter', 'transition:2s linear all'); var elements = []; var i; var elm; for (i = 0; i < 5; i++) { elm = angular.element('
'); elements.push(elm); $rootElement.append(elm); $animateCss(elm, { event: 'enter', structural: true }).start(); expect(elm).not.toHaveClass('ng-enter-stagger'); expect(elm).toHaveClass('ng-enter'); } triggerAnimationStartFrame(); expect(elements[0]).toHaveClass('ng-enter-active'); for (i = 1; i < 5; i++) { elm = elements[i]; expect(elm).not.toHaveClass('ng-enter-active'); $timeout.flush(200); expect(elm).toHaveClass('ng-enter-active'); browserTrigger(elm, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 2 }); expect(elm).not.toHaveClass('ng-enter'); expect(elm).not.toHaveClass('ng-enter-active'); expect(elm).not.toHaveClass('ng-enter-stagger'); } })); it('should apply a stagger based when for all provided addClass/removeClass CSS classes', inject(function($animateCss, $document, $rootElement, $timeout) { angular.element($document[0].body).append($rootElement); ss.addPossiblyPrefixedRule('.red-add-stagger,' + '.blue-remove-stagger,' + '.green-add-stagger', 'transition-delay:0.2s'); ss.addPossiblyPrefixedRule('.red-add,' + '.blue-remove,' + '.green-add', 'transition:2s linear all'); var elements = []; var i; var elm; for (i = 0; i < 5; i++) { elm = angular.element('
'); elements.push(elm); $rootElement.append(elm); $animateCss(elm, { addClass: 'red green', removeClass: 'blue' }).start(); } triggerAnimationStartFrame(); for (i = 0; i < 5; i++) { elm = elements[i]; expect(elm).not.toHaveClass('red-add-stagger'); expect(elm).not.toHaveClass('green-add-stagger'); expect(elm).not.toHaveClass('blue-remove-stagger'); expect(elm).toHaveClass('red-add'); expect(elm).toHaveClass('green-add'); expect(elm).toHaveClass('blue-remove'); } expect(elements[0]).toHaveClass('red-add-active'); expect(elements[0]).toHaveClass('green-add-active'); expect(elements[0]).toHaveClass('blue-remove-active'); for (i = 1; i < 5; i++) { elm = elements[i]; expect(elm).not.toHaveClass('red-add-active'); expect(elm).not.toHaveClass('green-add-active'); expect(elm).not.toHaveClass('blue-remove-active'); $timeout.flush(200); expect(elm).toHaveClass('red-add-active'); expect(elm).toHaveClass('green-add-active'); expect(elm).toHaveClass('blue-remove-active'); browserTrigger(elm, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 2 }); expect(elm).not.toHaveClass('red-add-active'); expect(elm).not.toHaveClass('green-add-active'); expect(elm).not.toHaveClass('blue-remove-active'); expect(elm).not.toHaveClass('red-add-stagger'); expect(elm).not.toHaveClass('green-add-stagger'); expect(elm).not.toHaveClass('blue-remove-stagger'); } })); it('should block the transition animation between start and animate when staggered', inject(function($animateCss, $document, $rootElement) { angular.element($document[0].body).append($rootElement); ss.addPossiblyPrefixedRule('.ng-enter-stagger', 'transition-delay:0.2s'); ss.addPossiblyPrefixedRule('.ng-enter', 'transition:2s linear all;'); var element; var i; var elms = []; for (i = 0; i < 5; i++) { element = angular.element('
'); $rootElement.append(element); $animateCss(element, { event: 'enter', structural: true }).start(); elms.push(element); } triggerAnimationStartFrame(); for (i = 0; i < 5; i++) { element = elms[i]; if (i === 0) { expect(element.attr('style')).toBeFalsy(); } else { expect(element.css('transition-delay')).toContain('-2s'); } } })); it('should block (pause) the keyframe animation between start and animate when staggered', inject(function($animateCss, $document, $rootElement) { angular.element($document[0].body).append($rootElement); ss.addPossiblyPrefixedRule('.ng-enter-stagger', 'animation-delay:0.2s'); ss.addPossiblyPrefixedRule('.ng-enter', 'animation:my_animation 2s;'); var i, element, elements = []; for (i = 0; i < 5; i++) { element = angular.element('
'); $rootElement.append(element); $animateCss(element, { event: 'enter', structural: true }).start(); elements.push(element); } triggerAnimationStartFrame(); for (i = 0; i < 5; i++) { element = elements[i]; if (i === 0) { // the first element is always run right away expect(element.attr('style')).toBeFalsy(); } else { expect(getPossiblyPrefixedStyleValue(element, 'animation-play-state')).toBe('paused'); } } })); it('should not apply a stagger if the transition delay value is inherited from a earlier CSS class', inject(function($animateCss, $document, $rootElement) { angular.element($document[0].body).append($rootElement); ss.addPossiblyPrefixedRule('.transition-animation', 'transition:2s 5s linear all;'); for (var i = 0; i < 5; i++) { var element = angular.element('
'); $rootElement.append(element); $animateCss(element, { event: 'enter', structural: true }).start(); triggerAnimationStartFrame(); expect(element).toHaveClass('ng-enter-active'); } })); it('should apply a stagger only if the transition duration value is zero when inherited from a earlier CSS class', inject(function($animateCss, $document, $rootElement) { angular.element($document[0].body).append($rootElement); ss.addPossiblyPrefixedRule('.transition-animation', 'transition:2s 5s linear all;'); ss.addPossiblyPrefixedRule('.transition-animation.ng-enter-stagger', 'transition-duration:0s; transition-delay:0.2s;'); var element, i, elms = []; for (i = 0; i < 5; i++) { element = angular.element('
'); $rootElement.append(element); elms.push(element); $animateCss(element, { event: 'enter', structural: true }).start(); } triggerAnimationStartFrame(); for (i = 1; i < 5; i++) { element = elms[i]; expect(element).not.toHaveClass('ng-enter-active'); } })); it('should ignore animation staggers if only transition animations were detected', inject(function($animateCss, $document, $rootElement) { angular.element($document[0].body).append($rootElement); ss.addPossiblyPrefixedRule('.ng-enter-stagger', 'animation-delay:0.2s'); ss.addPossiblyPrefixedRule('.transition-animation', 'transition:2s 5s linear all;'); for (var i = 0; i < 5; i++) { var element = angular.element('
'); $rootElement.append(element); $animateCss(element, { event: 'enter', structural: true }).start(); triggerAnimationStartFrame(); expect(element).toHaveClass('ng-enter-active'); } })); it('should ignore transition staggers if only keyframe animations were detected', inject(function($animateCss, $document, $rootElement) { angular.element($document[0].body).append($rootElement); ss.addPossiblyPrefixedRule('.ng-enter-stagger', 'transition-delay:0.2s'); ss.addPossiblyPrefixedRule('.transition-animation', 'animation: 2s 5s my_animation;'); for (var i = 0; i < 5; i++) { var elm = angular.element('
'); $rootElement.append(elm); var animator = $animateCss(elm, { event: 'enter', structural: true }).start(); triggerAnimationStartFrame(); expect(elm).toHaveClass('ng-enter-active'); } })); it('should start on the highest stagger value if both transition and keyframe staggers are used together', inject(function($animateCss, $document, $rootElement, $timeout, $browser) { angular.element($document[0].body).append($rootElement); ss.addPossiblyPrefixedRule('.ng-enter-stagger', 'transition-delay: 0.5s; ' + 'animation-delay: 1s'); ss.addPossiblyPrefixedRule('.ng-enter', 'transition: 10s linear all; ' + 'animation: 20s my_animation'); var i, elm, elements = []; for (i = 0; i < 5; i++) { elm = angular.element('
'); elements.push(elm); $rootElement.append(elm); $animateCss(elm, { event: 'enter', structural: true }).start(); expect(elm).toHaveClass('ng-enter'); } triggerAnimationStartFrame(); expect(elements[0]).toHaveClass('ng-enter-active'); for (i = 1; i < 5; i++) { elm = elements[i]; expect(elm).not.toHaveClass('ng-enter-active'); $timeout.flush(500); expect(elm).not.toHaveClass('ng-enter-active'); $timeout.flush(500); expect(elm).toHaveClass('ng-enter-active'); } })); it('should apply the closing timeout ontop of the stagger timeout', inject(function($animateCss, $document, $rootElement, $timeout, $browser) { angular.element($document[0].body).append($rootElement); ss.addPossiblyPrefixedRule('.ng-enter-stagger', 'transition-delay:1s;'); ss.addPossiblyPrefixedRule('.ng-enter', 'transition:10s linear all;'); var elm, i, elms = []; for (i = 0; i < 5; i++) { elm = angular.element('
'); elms.push(elm); $rootElement.append(elm); $animateCss(elm, { event: 'enter', structural: true }).start(); triggerAnimationStartFrame(); } for (i = 1; i < 2; i++) { elm = elms[i]; expect(elm).toHaveClass('ng-enter'); $timeout.flush(1000); $timeout.flush(15000); expect(elm).not.toHaveClass('ng-enter'); } })); it('should apply the closing timeout ontop of the stagger timeout with an added delay', inject(function($animateCss, $document, $rootElement, $timeout, $browser) { angular.element($document[0].body).append($rootElement); ss.addPossiblyPrefixedRule('.ng-enter-stagger', 'transition-delay:1s;'); ss.addPossiblyPrefixedRule('.ng-enter', 'transition:10s linear all; transition-delay:50s;'); var elm, i, elms = []; for (i = 0; i < 5; i++) { elm = angular.element('
'); elms.push(elm); $rootElement.append(elm); $animateCss(elm, { event: 'enter', structural: true }).start(); triggerAnimationStartFrame(); } for (i = 1; i < 2; i++) { elm = elms[i]; expect(elm).toHaveClass('ng-enter'); $timeout.flush(1000); $timeout.flush(65000); expect(elm).not.toHaveClass('ng-enter'); } })); it('should issue a stagger if a stagger value is provided in the options', inject(function($animateCss, $document, $rootElement, $timeout) { angular.element($document[0].body).append($rootElement); ss.addPossiblyPrefixedRule('.ng-enter', 'transition:2s linear all'); var elm, i, elements = []; for (i = 0; i < 5; i++) { elm = angular.element('
'); elements.push(elm); $rootElement.append(elm); $animateCss(elm, { event: 'enter', structural: true, stagger: 0.5 }).start(); expect(elm).toHaveClass('ng-enter'); } triggerAnimationStartFrame(); expect(elements[0]).toHaveClass('ng-enter-active'); for (i = 1; i < 5; i++) { elm = elements[i]; expect(elm).not.toHaveClass('ng-enter-active'); $timeout.flush(500); expect(elm).toHaveClass('ng-enter-active'); browserTrigger(elm, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 2 }); expect(elm).not.toHaveClass('ng-enter'); expect(elm).not.toHaveClass('ng-enter-active'); expect(elm).not.toHaveClass('ng-enter-stagger'); } })); it('should only add/remove classes once the stagger timeout has passed', inject(function($animateCss, $document, $rootElement, $timeout) { angular.element($document[0].body).append($rootElement); var element = angular.element('
'); $rootElement.append(element); $animateCss(element, { addClass: 'red', removeClass: 'green', duration: 5, stagger: 0.5, staggerIndex: 3 }).start(); triggerAnimationStartFrame(); expect(element).toHaveClass('green'); expect(element).not.toHaveClass('red'); $timeout.flush(1500); expect(element).not.toHaveClass('green'); expect(element).toHaveClass('red'); })); }); describe('closing timeout', function() { it('should close off the animation after 150% of the animation time has passed', inject(function($animateCss, $document, $rootElement, $timeout) { ss.addPossiblyPrefixedRule('.ng-enter', 'transition:10s linear all;'); var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); var animator = $animateCss(element, { event: 'enter', structural: true }); animator.start(); triggerAnimationStartFrame(); expect(element).toHaveClass('ng-enter'); expect(element).toHaveClass('ng-enter-active'); $timeout.flush(15000); expect(element).not.toHaveClass('ng-enter'); expect(element).not.toHaveClass('ng-enter-active'); })); it('should close off the animation after 150% of the animation time has passed and consider the detected delay value', inject(function($animateCss, $document, $rootElement, $timeout) { ss.addPossiblyPrefixedRule('.ng-enter', 'transition:10s linear all; transition-delay:30s;'); var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); var animator = $animateCss(element, { event: 'enter', structural: true }); animator.start(); triggerAnimationStartFrame(); expect(element).toHaveClass('ng-enter'); expect(element).toHaveClass('ng-enter-active'); $timeout.flush(45000); expect(element).not.toHaveClass('ng-enter'); expect(element).not.toHaveClass('ng-enter-active'); })); it('should still resolve the animation once expired', inject(function($animateCss, $document, $rootElement, $timeout, $animate, $rootScope) { ss.addPossiblyPrefixedRule('.ng-enter', 'transition:10s linear all;'); var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); var animator = $animateCss(element, { event: 'enter', structural: true }); var failed, passed; animator.start().then(function() { passed = true; }, function() { failed = true; }); triggerAnimationStartFrame(); $timeout.flush(15000); $animate.flush(); $rootScope.$digest(); expect(passed).toBe(true); })); it('should not resolve/reject after passing if the animation completed successfully', inject(function($animateCss, $document, $rootElement, $timeout, $rootScope, $animate) { ss.addPossiblyPrefixedRule('.ng-enter', 'transition:10s linear all;'); var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); var animator = $animateCss(element, { event: 'enter', structural: true }); var failed, passed; animator.start().then( function() { passed = true; }, function() { failed = true; } ); triggerAnimationStartFrame(); browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 10 }); $animate.flush(); $rootScope.$digest(); expect(passed).toBe(true); expect(failed).not.toBe(true); $timeout.flush(15000); expect(passed).toBe(true); expect(failed).not.toBe(true); })); it('should close all stacked animations after the last timeout runs on the same element', inject(function($animateCss, $document, $rootElement, $timeout, $animate) { var now = 0; spyOn(Date, 'now').and.callFake(function() { return now; }); var cancelSpy = spyOn($timeout, 'cancel').and.callThrough(); var doneSpy = jasmine.createSpy(); ss.addPossiblyPrefixedRule('.elm', 'transition:1s linear all;'); ss.addRule('.elm.red', 'background:red;'); ss.addPossiblyPrefixedRule('.elm.blue', 'transition:2s linear all; background:blue;'); ss.addRule('.elm.green', 'background:green;'); var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); // timeout will be at 1500s animate(element, 'red', doneSpy); expect(doneSpy).not.toHaveBeenCalled(); fastForwardClock(500); //1000s left to go // timeout will not be at 500 + 3000s = 3500s animate(element, 'blue', doneSpy); expect(doneSpy).not.toHaveBeenCalled(); expect(cancelSpy).toHaveBeenCalled(); cancelSpy.calls.reset(); // timeout will not be set again since the former animation is longer animate(element, 'green', doneSpy); expect(doneSpy).not.toHaveBeenCalled(); expect(cancelSpy).not.toHaveBeenCalled(); // this will close the animations fully fastForwardClock(3500); $animate.flush(); expect(doneSpy).toHaveBeenCalled(); expect(doneSpy).toHaveBeenCalledTimes(3); function fastForwardClock(time) { now += time; $timeout.flush(time); } function animate(element, klass, onDone) { var animator = $animateCss(element, { addClass: klass }).start(); animator.done(onDone); triggerAnimationStartFrame(); return animator; } })); it('should not throw an error any pending timeout requests resolve after the element has already been removed', inject(function($animateCss, $document, $rootElement, $timeout, $animate) { var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); ss.addPossiblyPrefixedRule('.red', 'transition:1s linear all;'); $animateCss(element, { addClass: 'red' }).start(); triggerAnimationStartFrame(); element.remove(); expect(function() { $timeout.flush(); }).not.toThrow(); })); it('should consider a positive options.delay value for the closing timeout', inject(function($animateCss, $rootElement, $timeout, $document) { var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); var options = { delay: 3, duration: 3, to: { height: '100px' } }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); // At this point, the animation should still be running (closing timeout is 7500ms ... duration * 1.5 + delay => 7.5) $timeout.flush(7000); expect(getPossiblyPrefixedStyleValue(element, 'transition-delay')).toBe('3s'); expect(getPossiblyPrefixedStyleValue(element, 'transition-duration')).toBe('3s'); // Let's flush the remaining amount of time for the timeout timer to kick in $timeout.flush(500); expect(getPossiblyPrefixedStyleValue(element, 'transition-duration')).toBeOneOf('', '0s'); expect(getPossiblyPrefixedStyleValue(element, 'transition-delay')).toBeOneOf('', '0s'); })); it('should ignore a boolean options.delay value for the closing timeout', inject(function($animateCss, $rootElement, $timeout, $document) { var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); var options = { delay: true, duration: 3, to: { height: '100px' } }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); // At this point, the animation should still be running (closing timeout is 4500ms ... duration * 1.5 => 4.5) $timeout.flush(4000); expect(getPossiblyPrefixedStyleValue(element, 'transition-delay')).toBeOneOf('initial', '0s'); expect(getPossiblyPrefixedStyleValue(element, 'transition-duration')).toBe('3s'); // Let's flush the remaining amount of time for the timeout timer to kick in $timeout.flush(500); expect(getPossiblyPrefixedStyleValue(element, 'transition-duration')).toBeOneOf('', '0s'); expect(getPossiblyPrefixedStyleValue(element, 'transition-delay')).toBeOneOf('', '0s'); })); it('should cancel the timeout when the animation is ended normally', inject(function($animateCss, $document, $rootElement, $timeout) { ss.addPossiblyPrefixedRule('.ng-enter', 'transition:10s linear all;'); var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); var animator = $animateCss(element, { event: 'enter', structural: true }); animator.start(); triggerAnimationStartFrame(); expect(element).toHaveClass('ng-enter'); expect(element).toHaveClass('ng-enter-active'); animator.end(); expect(element.data(ANIMATE_TIMER_KEY)).toBeUndefined(); $timeout.verifyNoPendingTasks(); })); }); describe('getComputedStyle', function() { var count; var acceptableTimingsData = { transitionDuration: '10s' }; beforeEach(module(function($provide) { count = {}; $provide.value('$window', extend({}, window, { document: angular.element(window.document), getComputedStyle: function(node) { var key = node.className.indexOf('stagger') >= 0 ? 'stagger' : 'normal'; count[key] = count[key] || 0; count[key]++; return acceptableTimingsData; } })); return function($document, $rootElement) { angular.element($document[0].body).append($rootElement); }; })); it('should cache frequent calls to getComputedStyle before the next animation frame kicks in', inject(function($animateCss, $document, $rootElement) { var i, elm, animator; for (i = 0; i < 5; i++) { elm = angular.element('
'); $rootElement.append(elm); animator = $animateCss(elm, { event: 'enter', structural: true }); var runner = animator.start(); } expect(count.normal).toBe(1); for (i = 0; i < 5; i++) { elm = angular.element('
'); $rootElement.append(elm); animator = $animateCss(elm, { event: 'enter', structural: true }); animator.start(); } expect(count.normal).toBe(1); triggerAnimationStartFrame(); expect(count.normal).toBe(2); for (i = 0; i < 5; i++) { elm = angular.element('
'); $rootElement.append(elm); animator = $animateCss(elm, { event: 'enter', structural: true }); animator.start(); } expect(count.normal).toBe(3); })); it('should cache frequent calls to getComputedStyle for stagger animations before the next animation frame kicks in', inject(function($animateCss, $document, $rootElement, $$rAF) { var element = angular.element('
'); $rootElement.append(element); var animator = $animateCss(element, { event: 'enter', structural: true }); animator.start(); triggerAnimationStartFrame(); expect(count.stagger).toBeUndefined(); var i, elm; for (i = 0; i < 5; i++) { elm = angular.element('
'); $rootElement.append(elm); animator = $animateCss(elm, { event: 'enter', structural: true }); animator.start(); } expect(count.stagger).toBe(1); for (i = 0; i < 5; i++) { elm = angular.element('
'); $rootElement.append(elm); animator = $animateCss(elm, { event: 'enter', structural: true }); animator.start(); } expect(count.stagger).toBe(1); $$rAF.flush(); for (i = 0; i < 5; i++) { elm = angular.element('
'); $rootElement.append(elm); animator = $animateCss(elm, { event: 'enter', structural: true }); animator.start(); } triggerAnimationStartFrame(); expect(count.stagger).toBe(2); })); }); describe('transitionend/animationend event listeners', function() { var element, elementOnSpy, elementOffSpy, progress; function setStyles(event) { switch (event) { case TRANSITIONEND_EVENT: ss.addPossiblyPrefixedRule('.ng-enter', 'transition: 10s linear all;'); progress = transitionProgress; break; case ANIMATIONEND_EVENT: ss.addPossiblyPrefixedRule('.ng-enter', 'animation: animation 10s;'); progress = keyframeProgress; break; } } beforeEach(inject(function($rootElement, $document) { element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); elementOnSpy = spyOn(element, 'on').and.callThrough(); elementOffSpy = spyOn(element, 'off').and.callThrough(); })); they('should remove the $prop event listeners on cancel', [TRANSITIONEND_EVENT, ANIMATIONEND_EVENT], function(event) { inject(function($animateCss) { setStyles(event); var animator = $animateCss(element, { event: 'enter', structural: true }); var runner = animator.start(); triggerAnimationStartFrame(); expect(elementOnSpy).toHaveBeenCalledOnce(); expect(elementOnSpy.calls.mostRecent().args[0]).toBe(event); runner.cancel(); expect(elementOffSpy).toHaveBeenCalledOnce(); expect(elementOffSpy.calls.mostRecent().args[0]).toBe(event); }); }); they('should remove the $prop event listener when the animation is closed', [TRANSITIONEND_EVENT, ANIMATIONEND_EVENT], function(event) { inject(function($animateCss) { setStyles(event); var animator = $animateCss(element, { event: 'enter', structural: true }); var runner = animator.start(); triggerAnimationStartFrame(); expect(elementOnSpy).toHaveBeenCalledOnce(); expect(elementOnSpy.calls.mostRecent().args[0]).toBe(event); progress(element, 10); expect(elementOffSpy).toHaveBeenCalledOnce(); expect(elementOffSpy.calls.mostRecent().args[0]).toBe(event); }); }); they('should remove the $prop event listener when the closing timeout occurs', [TRANSITIONEND_EVENT, ANIMATIONEND_EVENT], function(event) { inject(function($animateCss, $timeout) { setStyles(event); var animator = $animateCss(element, { event: 'enter', structural: true }); animator.start(); triggerAnimationStartFrame(); expect(elementOnSpy).toHaveBeenCalledOnce(); expect(elementOnSpy.calls.mostRecent().args[0]).toBe(event); $timeout.flush(15000); expect(elementOffSpy).toHaveBeenCalledOnce(); expect(elementOffSpy.calls.mostRecent().args[0]).toBe(event); }); }); they('should not add or remove $prop event listeners when no animation styles are detected', [TRANSITIONEND_EVENT, ANIMATIONEND_EVENT], function(event) { inject(function($animateCss, $timeout) { progress = event === TRANSITIONEND_EVENT ? transitionProgress : keyframeProgress; // Make sure other event listeners are not affected var otherEndSpy = jasmine.createSpy('otherEndSpy'); element.on(event, otherEndSpy); expect(elementOnSpy).toHaveBeenCalledOnce(); elementOnSpy.calls.reset(); var animator = $animateCss(element, { event: 'enter', structural: true }); expect(animator.$$willAnimate).toBeFalsy(); // This will close the animation because no styles have been detected var runner = animator.start(); triggerAnimationStartFrame(); expect(elementOnSpy).not.toHaveBeenCalled(); expect(elementOffSpy).not.toHaveBeenCalled(); progress(element, 10); expect(otherEndSpy).toHaveBeenCalledOnce(); }); }); }); }); it('should avoid applying the same cache to an element a follow-up animation is run on the same element', inject(function($animateCss, $rootElement, $document) { function endTransition(element, elapsedTime) { browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: elapsedTime }); } function startAnimation(element, duration, color) { $animateCss(element, { duration: duration, to: { background: color } }).start(); triggerAnimationStartFrame(); } var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); startAnimation(element, 0.5, 'red'); expect(element.attr('style')).toContain('transition'); endTransition(element, 0.5); expect(element.attr('style')).not.toContain('transition'); startAnimation(element, 0.8, 'blue'); expect(element.attr('style')).toContain('transition'); // Trigger an extra transitionend event that matches the original transition endTransition(element, 0.5); expect(element.attr('style')).toContain('transition'); endTransition(element, 0.8); expect(element.attr('style')).not.toContain('transition'); })); it('should clear cache if no animation so follow-up animation on the same element will not be from cache', inject(function($animateCss, $rootElement, $document, $$rAF) { var element = angular.element('
'); var options = { event: 'enter', structural: true }; $rootElement.append(element); angular.element($document[0].body).append($rootElement); var animator = $animateCss(element, options); expect(animator.$$willAnimate).toBeFalsy(); $$rAF.flush(); ss.addPossiblyPrefixedRule('.ng-enter', 'animation:3.5s keyframe_animation;'); animator = $animateCss(element, options); expect(animator.$$willAnimate).toBeTruthy(); })); it('should apply a custom temporary class when a non-structural animation is used', inject(function($animateCss, $rootElement, $document) { var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); $animateCss(element, { event: 'super', duration: 1000, to: fakeStyle }).start(); expect(element).toHaveClass('super'); triggerAnimationStartFrame(); expect(element).toHaveClass('super-active'); })); describe('structural animations', function() { they('should decorate the element with the ng-$prop CSS class', ['enter', 'leave', 'move'], function(event) { inject(function($animateCss, $rootElement, $document) { var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); $animateCss(element, { event: event, structural: true, duration: 1000, to: fakeStyle }); expect(element).toHaveClass('ng-' + event); }); }); they('should decorate the element with the ng-$prop-active CSS class', ['enter', 'leave', 'move'], function(event) { inject(function($animateCss, $rootElement, $document) { var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); var animator = $animateCss(element, { event: event, structural: true, duration: 1000, to: fakeStyle }); animator.start(); triggerAnimationStartFrame(); expect(element).toHaveClass('ng-' + event + '-active'); }); }); they('should remove the ng-$prop and ng-$prop-active CSS classes from the element once the animation is done', ['enter', 'leave', 'move'], function(event) { inject(function($animateCss, $rootElement, $document) { var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); var animator = $animateCss(element, { event: event, structural: true, duration: 1, to: fakeStyle }); animator.start(); triggerAnimationStartFrame(); browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 }); expect(element).not.toHaveClass('ng-' + event); expect(element).not.toHaveClass('ng-' + event + '-active'); }); }); they('should allow additional CSS classes to be added and removed alongside the $prop animation', ['enter', 'leave', 'move'], function(event) { inject(function($animateCss, $rootElement) { var element = angular.element('
'); $rootElement.append(element); var animator = $animateCss(element, { event: event, structural: true, duration: 1, to: fakeStyle, addClass: 'red', removeClass: 'green' }); animator.start(); triggerAnimationStartFrame(); expect(element).toHaveClass('ng-' + event); expect(element).toHaveClass('ng-' + event + '-active'); expect(element).toHaveClass('red'); expect(element).toHaveClass('red-add'); expect(element).toHaveClass('red-add-active'); expect(element).not.toHaveClass('green'); expect(element).toHaveClass('green-remove'); expect(element).toHaveClass('green-remove-active'); }); }); they('should place a CSS transition block after the preparation function to block accidental style changes', ['enter', 'leave', 'move', 'addClass', 'removeClass'], function(event) { inject(function($animateCss, $rootElement, $document) { var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); ss.addPossiblyPrefixedRule('.cool-animation', 'transition:1.5s linear all;'); element.addClass('cool-animation'); var data = {}; if (event === 'addClass') { data.addClass = 'green'; } else if (event === 'removeClass') { element.addClass('red'); data.removeClass = 'red'; } else { data.event = event; } var animator = $animateCss(element, data); expect(element.css('transition-delay')).toMatch('-1.5s'); animator.start(); triggerAnimationStartFrame(); expect(element.attr('style')).toBeFalsy(); }); }); they('should not place a CSS transition block if options.skipBlocking is provided', ['enter', 'leave', 'move', 'addClass', 'removeClass'], function(event) { inject(function($animateCss, $rootElement, $document) { var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); ss.addPossiblyPrefixedRule('.cool-animation', 'transition:1.5s linear all;'); element.addClass('cool-animation'); var data = {}; if (event === 'addClass') { data.addClass = 'green'; } else if (event === 'removeClass') { element.addClass('red'); data.removeClass = 'red'; } else { data.event = event; } var blockSpy = spyOn(helpers, 'blockTransitions').and.callThrough(); data.skipBlocking = true; var animator = $animateCss(element, data); expect(blockSpy).not.toHaveBeenCalled(); expect(element.attr('style')).toBeFalsy(); animator.start(); triggerAnimationStartFrame(); expect(element.attr('style')).toBeFalsy(); // just to prove it works data.skipBlocking = false; $animateCss(element, { addClass: 'test' }); expect(blockSpy).toHaveBeenCalled(); }); }); they('should place a CSS transition block after the preparation function even if a duration is provided', ['enter', 'leave', 'move', 'addClass', 'removeClass'], function(event) { inject(function($animateCss, $rootElement, $document) { var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); ss.addPossiblyPrefixedRule('.cool-animation', 'transition:1.5s linear all;'); element.addClass('cool-animation'); var data = {}; if (event === 'addClass') { data.addClass = 'green'; } else if (event === 'removeClass') { element.addClass('red'); data.removeClass = 'red'; } else { data.event = event; } data.duration = 10; var animator = $animateCss(element, data); expect(element.css('transition-delay')).toMatch('-10s'); expect(element.css('transition-duration')).toMatch(''); animator.start(); triggerAnimationStartFrame(); expect(element.attr('style')).not.toContain('transition-delay'); expect(element.css('transition-property')).toContain('all'); expect(element.css('transition-duration')).toContain('10s'); }); }); it('should allow multiple events to be animated at the same time', inject(function($animateCss, $rootElement, $document) { var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); $animateCss(element, { event: ['enter', 'leave', 'move'], structural: true, duration: 1, to: fakeStyle }).start(); triggerAnimationStartFrame(); expect(element).toHaveClass('ng-enter'); expect(element).toHaveClass('ng-leave'); expect(element).toHaveClass('ng-move'); expect(element).toHaveClass('ng-enter-active'); expect(element).toHaveClass('ng-leave-active'); expect(element).toHaveClass('ng-move-active'); browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 1 }); expect(element).not.toHaveClass('ng-enter'); expect(element).not.toHaveClass('ng-leave'); expect(element).not.toHaveClass('ng-move'); expect(element).not.toHaveClass('ng-enter-active'); expect(element).not.toHaveClass('ng-leave-active'); expect(element).not.toHaveClass('ng-move-active'); })); it('should not break when running anchored animations without duration', inject(function($animate, $document, $rootElement) { var element1 = angular.element('
Item 1
'); var element2 = angular.element('
Item 2
'); angular.element($document[0].body).append($rootElement); $rootElement.append(element1); expect($rootElement.text()).toBe('Item 1'); $animate.leave(element1); $animate.enter(element2, $rootElement); $animate.flush(); expect($rootElement.text()).toBe('Item 2'); }) ); }); describe('class-based animations', function() { they('should decorate the element with the class-$prop CSS class', ['add', 'remove'], function(event) { inject(function($animateCss, $rootElement) { var element = angular.element('
'); $rootElement.append(element); var options = {}; options[event + 'Class'] = 'class'; options.duration = 1000; options.to = fakeStyle; $animateCss(element, options); expect(element).toHaveClass('class-' + event); }); }); they('should decorate the element with the class-$prop-active CSS class', ['add', 'remove'], function(event) { inject(function($animateCss, $rootElement) { var element = angular.element('
'); $rootElement.append(element); var options = {}; options[event + 'Class'] = 'class'; options.duration = 1000; options.to = fakeStyle; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); expect(element).toHaveClass('class-' + event + '-active'); }); }); they('should remove the class-$prop-add and class-$prop-active CSS classes from the element once the animation is done', ['enter', 'leave', 'move'], function(event) { inject(function($animateCss, $rootElement, $document) { var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); var options = {}; options.event = event; options.duration = 10; options.to = fakeStyle; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 10 }); expect(element).not.toHaveClass('ng-' + event); expect(element).not.toHaveClass('ng-' + event + '-active'); }); }); they('should allow the class duration styles to be recalculated once started if the CSS classes being applied result new transition styles', ['add', 'remove'], function(event) { inject(function($animateCss, $rootElement, $document) { var element = angular.element('
'); if (event === 'add') { ss.addPossiblyPrefixedRule('.natural-class', 'transition:1s linear all;'); } else { ss.addPossiblyPrefixedRule('.natural-class', 'transition:0s linear none;'); ss.addPossiblyPrefixedRule('.base-class', 'transition:1s linear none;'); element.addClass('base-class'); element.addClass('natural-class'); } $rootElement.append(element); angular.element($document[0].body).append($rootElement); var options = {}; options[event + 'Class'] = 'natural-class'; var runner = $animateCss(element, options); runner.start(); triggerAnimationStartFrame(); expect(element).toHaveClass('natural-class-' + event); expect(element).toHaveClass('natural-class-' + event + '-active'); browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 1 }); expect(element).not.toHaveClass('natural-class-' + event); expect(element).not.toHaveClass('natural-class-' + event + '-active'); }); }); they('should force the class-based values to be applied early if no options.applyClassEarly is used as an option', ['enter', 'leave', 'move'], function(event) { inject(function($animateCss, $rootElement, $document) { ss.addPossiblyPrefixedRule('.blue.ng-' + event, 'transition:2s linear all;'); var element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); var runner = $animateCss(element, { addClass: 'blue', applyClassesEarly: true, removeClass: 'red', event: event, structural: true }); runner.start(); expect(element).toHaveClass('ng-' + event); expect(element).toHaveClass('blue'); expect(element).not.toHaveClass('red'); triggerAnimationStartFrame(); expect(element).toHaveClass('ng-' + event); expect(element).toHaveClass('ng-' + event + '-active'); expect(element).toHaveClass('blue'); expect(element).not.toHaveClass('red'); browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 2 }); expect(element).not.toHaveClass('ng-' + event); expect(element).not.toHaveClass('ng-' + event + '-active'); expect(element).toHaveClass('blue'); expect(element).not.toHaveClass('red'); }); }); }); describe('options', function() { var element; beforeEach(module(function() { return function($rootElement, $document) { angular.element($document[0].body).append($rootElement); element = angular.element('
'); $rootElement.append(element); }; })); it('should not alter the provided options input in any way throughout the animation', inject(function($animateCss) { var initialOptions = { from: { height: '50px' }, to: { width: '50px' }, addClass: 'one', removeClass: 'two', duration: 10, delay: 10, structural: true, keyframeStyle: '1s rotate', transitionStyle: '1s linear', stagger: 0.5, staggerIndex: 3 }; var copiedOptions = copy(initialOptions); expect(copiedOptions).toEqual(initialOptions); var animator = $animateCss(element, copiedOptions); expect(copiedOptions).toEqual(initialOptions); var runner = animator.start(); expect(copiedOptions).toEqual(initialOptions); triggerAnimationStartFrame(); expect(copiedOptions).toEqual(initialOptions); runner.end(); expect(copiedOptions).toEqual(initialOptions); })); it('should not create a copy of the provided options if they have already been prepared earlier', inject(function($animate, $animateCss) { var options = { from: { height: '50px' }, to: { width: '50px' }, addClass: 'one', removeClass: 'two' }; options.$$prepared = true; var runner = $animateCss(element, options).start(); runner.end(); $animate.flush(); expect(options.addClass).toBeFalsy(); expect(options.removeClass).toBeFalsy(); expect(options.to).toBeFalsy(); expect(options.from).toBeFalsy(); })); describe('[$$skipPreparationClasses]', function() { it('should not apply and remove the preparation classes to the element when true', inject(function($animateCss) { var options = { duration: 3000, to: fakeStyle, event: 'event', structural: true, addClass: 'klass', $$skipPreparationClasses: true }; var animator = $animateCss(element, options); expect(element).not.toHaveClass('klass-add'); expect(element).not.toHaveClass('ng-event'); var runner = animator.start(); triggerAnimationStartFrame(); expect(element).not.toHaveClass('klass-add'); expect(element).not.toHaveClass('ng-event'); expect(element).toHaveClass('klass-add-active'); expect(element).toHaveClass('ng-event-active'); element.addClass('klass-add ng-event'); runner.end(); expect(element).toHaveClass('klass-add'); expect(element).toHaveClass('ng-event'); expect(element).not.toHaveClass('klass-add-active'); expect(element).not.toHaveClass('ng-event-active'); })); }); describe('[duration]', function() { it('should be applied for a transition directly', inject(function($animateCss, $rootElement) { var element = angular.element('
'); $rootElement.append(element); var options = { duration: 3000, to: fakeStyle, event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); var style = element.attr('style'); expect(style).toContain('3000s'); expect(style).toContain('linear'); })); it('should be applied to a CSS keyframe animation directly if keyframes are detected within the CSS class', inject(function($animateCss, $rootElement) { ss.addPossiblyPrefixedRule('.ng-enter', 'animation:1.5s keyframe_animation;'); var options = { duration: 5, event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); expect(getPossiblyPrefixedStyleValue(element, 'animation-duration')).toEqual('5s'); })); it('should remove all inline keyframe styling when an animation completes if a custom duration was applied', inject(function($animateCss, $rootElement) { ss.addPossiblyPrefixedRule('.ng-enter', 'animation:1.5s keyframe_animation;'); var options = { duration: 5, event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); browserTrigger(element, 'animationend', { timeStamp: Date.now() + 5000, elapsedTime: 5 }); expect(element.attr('style')).toBeFalsy(); })); it('should remove all inline keyframe delay styling when an animation completes if a custom duration was applied', inject(function($animateCss, $rootElement) { ss.addPossiblyPrefixedRule('.ng-enter', 'animation:1.5s keyframe_animation;'); var options = { delay: 5, event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); expect(getPossiblyPrefixedStyleValue(element, 'animation-delay')).toEqual('5s'); browserTrigger(element, 'animationend', { timeStamp: Date.now() + 5000, elapsedTime: 1.5 }); expect(element.attr('style')).toBeFalsy(); })); it('should not prepare the animation at all if a duration of zero is provided', inject(function($animateCss, $rootElement) { ss.addPossiblyPrefixedRule('.ng-enter', 'transition:1s linear all;'); var options = { duration: 0, event: 'enter', structural: true }; var animator = $animateCss(element, options); expect(animator.$$willAnimate).toBeFalsy(); })); it('should apply a transition and keyframe duration directly if both transitions and keyframe classes are detected', inject(function($animateCss, $rootElement) { ss.addPossiblyPrefixedRule('.ng-enter', 'animation:3s keyframe_animation;' + 'transition:5s linear all;'); var options = { duration: 4, event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); var style = element.attr('style'); expect(style).toMatch(/animation(?:-duration)?:\s*4s/); expect(element.css('transition-duration')).toMatch('4s'); expect(element.css('transition-property')).toMatch('all'); expect(style).toContain('linear'); })); }); describe('[delay]', function() { it('should be applied for a transition directly', inject(function($animateCss, $rootElement) { var element = angular.element('
'); $rootElement.append(element); var options = { duration: 3000, delay: 500, to: fakeStyle, event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); var prop = element.css('transition-delay'); expect(prop).toEqual('500s'); })); it('should return false for the animator if a delay is provided but not a duration', inject(function($animateCss, $rootElement) { var element = angular.element('
'); $rootElement.append(element); var options = { delay: 500, to: fakeStyle, event: 'enter', structural: true }; var animator = $animateCss(element, options); expect(animator.$$willAnimate).toBeFalsy(); })); it('should override the delay value present in the CSS class', inject(function($animateCss, $rootElement) { ss.addPossiblyPrefixedRule('.ng-enter', 'transition:1s linear all;' + 'transition-delay:10s;'); var element = angular.element('
'); $rootElement.append(element); var options = { delay: 500, event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); var prop = element.css('transition-delay'); expect(prop).toEqual('500s'); })); it('should allow the delay value to zero if provided', inject(function($animateCss, $rootElement) { ss.addPossiblyPrefixedRule('.ng-enter', 'transition:1s linear all;' + 'transition-delay:10s;'); var element = angular.element('
'); $rootElement.append(element); var options = { delay: 0, event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); var prop = element.css('transition-delay'); expect(prop).toEqual('0s'); })); it('should be applied to a CSS keyframe animation if detected within the CSS class', inject(function($animateCss, $rootElement) { ss.addPossiblyPrefixedRule('.ng-enter', 'animation:1.5s keyframe_animation;'); var options = { delay: 400, event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); expect(getPossiblyPrefixedStyleValue(element, 'animation-delay')).toEqual('400s'); expect(element.attr('style')).not.toContain('transition-delay'); })); it('should apply a transition and keyframe delay if both transitions and keyframe classes are detected', inject(function($animateCss, $rootElement) { ss.addPossiblyPrefixedRule('.ng-enter', 'animation:3s keyframe_animation;' + 'transition:5s linear all;'); var options = { delay: 10, event: 'enter', structural: true }; var animator = $animateCss(element, options); expect(element.css('transition-delay')).toContain('-5s'); expect(element.attr('style')).not.toContain('animation-delay'); animator.start(); triggerAnimationStartFrame(); expect(getPossiblyPrefixedStyleValue(element, 'animation-delay')).toEqual('10s'); expect(element.css('transition-delay')).toEqual('10s'); })); it('should apply the keyframe and transition duration value before the CSS classes are applied', function() { var classSpy = jasmine.createSpy(); module(function($provide) { $provide.value('$$jqLite', { addClass: function() { classSpy(); }, removeClass: function() { classSpy(); } }); }); inject(function($animateCss, $rootElement) { element.addClass('element'); ss.addPossiblyPrefixedRule('.element', 'animation:3s keyframe_animation;' + 'transition:5s linear all;'); var options = { delay: 2, duration: 2, addClass: 'superman', $$skipPreparationClasses: true, structural: true }; var animator = $animateCss(element, options); expect(element.attr('style') || '').not.toContain('animation-delay'); expect(element.attr('style') || '').not.toContain('transition-delay'); expect(classSpy).not.toHaveBeenCalled(); //redefine the classSpy to assert that the delay values have been //applied before the classes are added var assertionsRun = false; classSpy = function() { assertionsRun = true; expect(getPossiblyPrefixedStyleValue(element, 'animation-delay')).toEqual('2s'); expect(element.css('transition-delay')).toEqual('2s'); expect(element).not.toHaveClass('superman'); }; animator.start(); triggerAnimationStartFrame(); expect(assertionsRun).toBe(true); }); }); it('should apply blocking before the animation starts, but then apply the detected delay when options.delay is true', inject(function($animateCss, $rootElement) { ss.addPossiblyPrefixedRule('.ng-enter', 'transition:2s linear all; transition-delay: 1s;'); var options = { delay: true, event: 'enter', structural: true }; var animator = $animateCss(element, options); expect(element.css('transition-delay')).toEqual('-2s'); animator.start(); triggerAnimationStartFrame(); expect(element.attr('style') || '').not.toContain('transition-delay'); })); it('should consider a negative value when delay:true is used with a keyframe animation', inject(function($animateCss, $rootElement) { ss.addPossiblyPrefixedRule('.ng-enter', 'animation: 2s keyframe_animation; ' + 'animation-delay: -1s;'); var options = { delay: true, event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); expect(getPossiblyPrefixedStyleValue(element, 'animation-delay')).toContain('-1s'); })); they('should consider a negative value when a negative option delay is provided for a $prop animation', { 'transition': function() { return { prop: 'transition-delay', css: 'transition:2s linear all' }; }, 'keyframe': function() { return { prop: 'animation-delay', css: 'animation: 2s keyframe_animation' }; } }, function(testDetailsFactory) { inject(function($animateCss, $rootElement) { var testDetails = testDetailsFactory(); ss.addPossiblyPrefixedRule('.ng-enter', testDetails.css); var options = { delay: -2, event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); expect(getPossiblyPrefixedStyleValue(element, testDetails.prop)).toContain('-2s'); }); }); they('should expect the $propend event to always return the full duration even when negative values are used', { 'transition': function() { return { event: 'transitionend', css: 'transition:5s linear all; transition-delay: -2s' }; }, 'animation': function() { return { event: 'animationend', css: 'animation: 5s keyframe_animation; animation-delay: -2s;' }; } }, function(testDetailsFactory) { inject(function($animateCss, $rootElement) { var testDetails = testDetailsFactory(); var event = testDetails.event; ss.addPossiblyPrefixedRule('.ng-enter', testDetails.css); var options = { event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); // 5 + (-2s) = 3 browserTrigger(element, event, { timeStamp: Date.now(), elapsedTime: 3 }); assertAnimationRunning(element, true); // 5 seconds is the full animation browserTrigger(element, event, { timeStamp: Date.now(), elapsedTime: 5 }); assertAnimationRunning(element); }); }); }); describe('[transitionStyle]', function() { it('should apply the transition directly onto the element and animate accordingly', inject(function($animateCss, $rootElement) { var options = { transitionStyle: '5.5s linear all', event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); var style = element.attr('style'); expect(element.css('transition-duration')).toMatch('5.5s'); expect(element.css('transition-property')).toMatch('all'); expect(style).toContain('linear'); expect(element).toHaveClass('ng-enter'); expect(element).toHaveClass('ng-enter-active'); browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 10000, elapsedTime: 5.5 }); expect(element).not.toHaveClass('ng-enter'); expect(element).not.toHaveClass('ng-enter-active'); expect(element.attr('style')).toBeFalsy(); })); it('should give priority to the provided duration value, but only update the duration style itself', inject(function($animateCss, $rootElement) { var options = { transitionStyle: '5.5s ease-in color', duration: 4, event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); var style = element.attr('style'); expect(element.css('transition-duration')).toMatch('4s'); expect(element.css('transition-property')).toMatch('color'); expect(style).toContain('ease-in'); })); it('should give priority to the provided delay value, but only update the delay style itself', inject(function($animateCss, $rootElement) { var options = { transitionStyle: '5.5s 4s ease-in color', delay: 20, event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); var style = element.attr('style'); expect(element.css('transition-delay')).toMatch('20s'); expect(element.css('transition-duration')).toMatch('5.5s'); expect(element.css('transition-property')).toMatch('color'); expect(style).toContain('ease-in'); })); it('should execute the animation only if there is any provided CSS styling to go with the transition', inject(function($animateCss, $rootElement) { var options = { transitionStyle: '6s 4s ease-out all' }; $animateCss(element, options).start(); triggerAnimationStartFrame(); expect(getPossiblyPrefixedStyleValue(element, 'transition-delay')).not.toEqual('4s'); expect(getPossiblyPrefixedStyleValue(element, 'transition-duration')).not.toEqual('6s'); options.to = { color: 'brown' }; $animateCss(element, options).start(); triggerAnimationStartFrame(); expect(getPossiblyPrefixedStyleValue(element, 'transition-delay')).toEqual('4s'); expect(getPossiblyPrefixedStyleValue(element, 'transition-duration')).toEqual('6s'); })); }); describe('[keyframeStyle]', function() { it('should apply the keyframe animation directly onto the element and animate accordingly', inject(function($animateCss, $rootElement) { var options = { keyframeStyle: 'my_animation 5.5s', event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); var detectedStyle = element.attr('style'); expect(detectedStyle).toContain('5.5s'); expect(detectedStyle).toContain('my_animation'); expect(element).toHaveClass('ng-enter'); expect(element).toHaveClass('ng-enter-active'); browserTrigger(element, 'animationend', { timeStamp: Date.now() + 10000, elapsedTime: 5.5 }); expect(element).not.toHaveClass('ng-enter'); expect(element).not.toHaveClass('ng-enter-active'); expect(element.attr('style')).toBeFalsy(); })); it('should give priority to the provided duration value, but only update the duration style itself', inject(function($animateCss, $rootElement) { var options = { keyframeStyle: 'my_animation 5.5s', duration: 50, event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); var detectedStyle = element.attr('style'); expect(detectedStyle).toContain('50s'); expect(detectedStyle).toContain('my_animation'); })); it('should give priority to the provided delay value, but only update the duration style itself', inject(function($animateCss, $rootElement) { var options = { keyframeStyle: 'my_animation 5.5s 10s', delay: 50, event: 'enter', structural: true }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); expect(getPossiblyPrefixedStyleValue(element, 'animation-delay')).toEqual('50s'); expect(getPossiblyPrefixedStyleValue(element, 'animation-duration')).toEqual('5.5s'); expect(getPossiblyPrefixedStyleValue(element, 'animation-name')).toEqual('my_animation'); })); it('should be able to execute the animation if it is the only provided value', inject(function($animateCss, $rootElement) { var options = { keyframeStyle: 'my_animation 5.5s 10s' }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); expect(getPossiblyPrefixedStyleValue(element, 'animation-delay')).toEqual('10s'); expect(getPossiblyPrefixedStyleValue(element, 'animation-duration')).toEqual('5.5s'); expect(getPossiblyPrefixedStyleValue(element, 'animation-name')).toEqual('my_animation'); })); }); describe('[from] and [to]', function() { it('should apply from styles to an element during the preparation phase', inject(function($animateCss, $rootElement) { var options = { duration: 2.5, event: 'enter', structural: true, from: { width: '50px' }, to: { width: '100px' } }; var animator = $animateCss(element, options); expect(element.attr('style')).toMatch(/width:\s*50px/); })); it('should apply to styles to an element during the animation phase', inject(function($animateCss, $rootElement) { var options = { duration: 2.5, event: 'enter', structural: true, from: { width: '15px' }, to: { width: '25px' } }; var animator = $animateCss(element, options); var runner = animator.start(); triggerAnimationStartFrame(); runner.end(); expect(element.css('width')).toBe('25px'); })); it('should apply the union of from and to styles to the element if no animation will be run', inject(function($animateCss, $rootElement) { var options = { event: 'enter', structural: true, from: { 'width': '10px', height: '50px' }, to: { 'width': '15px' } }; var animator = $animateCss(element, options); expect(animator.$$willAnimate).toBeFalsy(); animator.start(); expect(element.css('width')).toBe('15px'); expect(element.css('height')).toBe('50px'); })); it('should retain to and from styles on an element after an animation completes', inject(function($animateCss, $rootElement) { var options = { event: 'enter', structural: true, duration: 10, from: { 'width': '10px', height: '66px' }, to: { 'width': '5px' } }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 10000, elapsedTime: 10 }); expect(element).not.toHaveClass('ng-enter'); expect(element.css('width')).toBe('5px'); expect(element.css('height')).toBe('66px'); })); it('should always apply the from styles before the start function is called even if no transition is detected when started', inject(function($animateCss, $rootElement) { ss.addPossiblyPrefixedRule('.my-class', 'transition: 0s linear color'); var options = { addClass: 'my-class', from: { height: '26px' }, to: { height: '500px' } }; var animator = $animateCss(element, options); expect(element.css('height')).toBe('26px'); animator.start(); triggerAnimationStartFrame(); expect(element.css('height')).toBe('500px'); })); it('should apply an inline transition if [to] styles and a duration are provided', inject(function($animateCss, $rootElement) { var options = { event: 'enter', structural: true, duration: 2.5, to: { background: 'red' } }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); var style = element.attr('style'); expect(element.css('transition-duration')).toMatch('2.5s'); expect(element.css('transition-property')).toMatch('all'); expect(style).toContain('linear'); })); it('should remove all inline transition styling when an animation completes', inject(function($animateCss, $rootElement) { var options = { event: 'enter', structural: true, duration: 2.5, to: { background: 'red' } }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); var style = element.attr('style'); expect(style).toContain('transition'); browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 2500, elapsedTime: 2.5 }); style = element.attr('style'); expect(style).not.toContain('transition'); })); it('should retain existing styles when an inline styled animation completes', inject(function($animateCss, $rootElement) { var options = { event: 'enter', structural: true, duration: 2.5 }; element.css('font-size', '20px'); element.css('opacity', '0.5'); var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); var style = element.attr('style'); expect(style).toContain('transition'); animator.end(); style = element.attr('style'); expect(element.attr('style')).not.toContain('transition'); expect(element.css('opacity')).toEqual('0.5'); })); it('should remove all inline transition delay styling when an animation completes', inject(function($animateCss, $rootElement) { ss.addPossiblyPrefixedRule('.ng-enter', 'transition: 1s linear color'); var options = { event: 'enter', structural: true, delay: 5 }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); expect(element.css('transition-delay')).toEqual('5s'); browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 5000, elapsedTime: 1 }); expect(element.attr('style') || '').not.toContain('transition'); })); it('should not apply an inline transition if only [from] styles and a duration are provided', inject(function($animateCss, $rootElement) { var options = { duration: 3, from: { background: 'blue' } }; var animator = $animateCss(element, options); expect(animator.$$willAnimate).toBeFalsy(); })); it('should apply a transition if [from] styles are provided with a class that is added', inject(function($animateCss, $rootElement) { var options = { addClass: 'superb', from: { background: 'blue' } }; var animator = $animateCss(element, options); expect(isFunction(animator.start)).toBe(true); })); it('should apply an inline transition if only [from] styles, but classes are added or removed and a duration is provided', inject(function($animateCss, $rootElement) { var options = { duration: 3, addClass: 'sugar', from: { background: 'yellow' } }; var animator = $animateCss(element, options); expect(animator.$$willAnimate).toBeTruthy(); })); it('should not apply an inline transition if no styles are provided', inject(function($animateCss, $rootElement) { var emptyObject = {}; var options = { duration: 3, to: emptyObject, from: emptyObject }; var animator = $animateCss(element, options); expect(animator.$$willAnimate).toBeFalsy(); })); it('should apply a transition duration if the existing transition duration\'s property value is not \'all\'', inject(function($animateCss, $rootElement) { ss.addPossiblyPrefixedRule('.ng-enter', 'transition: 1s linear color'); var emptyObject = {}; var options = { event: 'enter', structural: true, to: { background: 'blue' } }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); var style = element.attr('style'); expect(element.css('transition-duration')).toMatch('1s'); expect(element.css('transition-property')).toMatch('all'); expect(style).toContain('linear'); })); it('should apply a transition duration and an animation duration if duration + styles options are provided for a matching keyframe animation', inject(function($animateCss, $rootElement) { ss.addPossiblyPrefixedRule('.ng-enter', 'animation:3.5s keyframe_animation;'); var emptyObject = {}; var options = { event: 'enter', structural: true, duration: 10, to: { background: 'blue' } }; var animator = $animateCss(element, options); animator.start(); triggerAnimationStartFrame(); expect(element.css('transition-duration')).toMatch('10s'); expect(getPossiblyPrefixedStyleValue(element, 'animation-duration')).toEqual('10s'); })); }); describe('[easing]', function() { var element; beforeEach(inject(function($document, $rootElement) { element = angular.element('
'); $rootElement.append(element); angular.element($document[0].body).append($rootElement); })); it('should apply easing to a transition animation if it exists', inject(function($animateCss) { ss.addPossiblyPrefixedRule('.red', 'transition:1s linear all;'); var easing = 'ease-out'; var animator = $animateCss(element, { addClass: 'red', easing: easing }); animator.start(); triggerAnimationStartFrame(); var style = element.attr('style'); expect(style).toContain('ease-out'); })); it('should not apply easing to transitions nor keyframes on an element animation if nothing is detected', inject(function($animateCss) { ss.addRule('.red', ';'); var easing = 'ease-out'; var animator = $animateCss(element, { addClass: 'red', easing: easing }); animator.start(); triggerAnimationStartFrame(); expect(element.attr('style')).toBeFalsy(); })); it('should apply easing to both keyframes and transition animations if detected', inject(function($animateCss) { ss.addPossiblyPrefixedRule('.red', 'transition: 1s linear all;'); ss.addPossiblyPrefixedRule('.blue', 'animation: 1s my_keyframe;'); var easing = 'ease-out'; var animator = $animateCss(element, { addClass: 'red blue', easing: easing }); animator.start(); triggerAnimationStartFrame(); var style = element.attr('style'); expect(style).toMatch(/animation(?:-timing-function)?:\s*ease-out/); expect(style).toMatch(/transition(?:-timing-function)?:\s*ease-out/); })); }); describe('[cleanupStyles]', function() { it('should cleanup [from] and [to] styles that have been applied for the animation when true', inject(function($animateCss) { var runner = $animateCss(element, { duration: 1, from: { background: 'gold' }, to: { color: 'brown' }, cleanupStyles: true }).start(); assertStyleIsPresent(element, 'background', true); assertStyleIsPresent(element, 'color', false); triggerAnimationStartFrame(); assertStyleIsPresent(element, 'background', true); assertStyleIsPresent(element, 'color', true); runner.end(); assertStyleIsPresent(element, 'background', false); assertStyleIsPresent(element, 'color', false); function assertStyleIsPresent(element, style, bool) { expect(element[0].style[style])[bool ? 'toBeTruthy' : 'toBeFalsy'](); } })); it('should restore existing overidden styles already present on the element when true', inject(function($animateCss) { element.css('height', '100px'); element.css('width', '111px'); var runner = $animateCss(element, { duration: 1, from: { height: '200px', 'font-size':'66px' }, to: { height: '300px', 'font-size': '99px', width: '222px' }, cleanupStyles: true }).start(); assertStyle(element, 'height', '200px'); assertStyle(element, 'font-size', '66px'); assertStyle(element, 'width', '111px'); triggerAnimationStartFrame(); assertStyle(element, 'height', '300px'); assertStyle(element, 'width', '222px'); assertStyle(element, 'font-size', '99px'); runner.end(); assertStyle(element, 'width', '111px'); assertStyle(element, 'height', '100px'); expect(element[0].style.getPropertyValue('font-size')).not.toBe('66px'); function assertStyle(element, prop, value) { expect(element[0].style.getPropertyValue(prop)).toBe(value); } })); }); it('should round up long elapsedTime values to close off a CSS3 animation', inject(function($animateCss) { ss.addPossiblyPrefixedRule('.millisecond-transition.ng-leave', 'transition:510ms linear all;'); element.addClass('millisecond-transition'); var animator = $animateCss(element, { event: 'leave', structural: true }); animator.start(); triggerAnimationStartFrame(); expect(element).toHaveClass('ng-leave-active'); browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 0.50999999991 }); expect(element).not.toHaveClass('ng-leave-active'); })); }); describe('SVG', function() { it('should properly apply transitions on an SVG element', inject(function($animateCss, $rootScope, $compile, $document, $rootElement) { var element = $compile('' + '' + '')($rootScope); angular.element($document[0].body).append($rootElement); $rootElement.append(element); $animateCss(element, { event: 'enter', structural: true, duration: 10 }).start(); triggerAnimationStartFrame(); expect(element).toHaveClass('ng-enter'); expect(element).toHaveClass('ng-enter-active'); browserTrigger(element, 'transitionend', { timeStamp: Date.now() + 1000, elapsedTime: 10 }); expect(element).not.toHaveClass('ng-enter'); expect(element).not.toHaveClass('ng-enter-active'); })); it('should properly remove classes from SVG elements', inject(function($animateCss) { var element = angular.element('' + '' + ''); var child = element.find('rect'); var animator = $animateCss(child, { removeClass: 'class-of-doom', duration: 0 }); animator.start(); var className = child[0].getAttribute('class'); expect(className).toBe(''); })); }); }); });