From 79bf4a22defb9779a778f195c642d832234cc67f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Wed, 21 Aug 2013 21:29:40 -0400 Subject: [PATCH 1/3] feat(ngMock): add support for creating dynamic style sheets within test code --- angularFiles.js | 3 ++- src/privateMocks.js | 28 +++++++++++++++++++++++++++ test/ngAnimate/animateSpec.js | 12 ++++++++---- test/privateMocksSpec.js | 36 +++++++++++++++++++++++++++++++++++ 4 files changed, 74 insertions(+), 5 deletions(-) create mode 100644 src/privateMocks.js create mode 100644 test/privateMocksSpec.js diff --git a/angularFiles.js b/angularFiles.js index a8509b3f28ff..a20dc21a72e4 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -79,7 +79,8 @@ angularFiles = { 'src/ngTouch/swipe.js', 'src/ngTouch/directive/ngClick.js', 'src/ngTouch/directive/ngSwipe.js', - 'docs/components/angular-bootstrap/bootstrap.js' + 'docs/components/angular-bootstrap/bootstrap.js', + 'src/privateMocks.js' ], 'angularScenario': [ diff --git a/src/privateMocks.js b/src/privateMocks.js new file mode 100644 index 000000000000..6d9fb34fc474 --- /dev/null +++ b/src/privateMocks.js @@ -0,0 +1,28 @@ +function createMockStyleSheet(doc, wind) { + doc = doc ? doc[0] : document; + wind = wind || window; + + var node = doc.createElement('style'); + var head = doc.getElementsByTagName('head')[0]; + head.appendChild(node); + + var ss = doc.styleSheets[doc.styleSheets.length - 1]; + + return { + addRule : function(selector, styles) { + try { + ss.insertRule(selector + '{ ' + styles + '}', 0); + } + catch(e) { + try { + ss.addRule(selector, styles); + } + catch(e) {} + } + }, + + destroy : function() { + head.removeChild(node); + } + }; +}; diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 30bf6ba7502f..3702664037c8 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -1118,11 +1118,15 @@ describe("ngAnimate", function() { })); it("should properly execute CSS animations/transitions and use callbacks when using addClass / removeClass", - inject(function($animate, $rootScope, $sniffer, $rootElement, $timeout) { + inject(function($animate, $rootScope, $sniffer, $rootElement, $timeout, $window, $document) { - var transition = 'transition:11s linear all;'; - var style = transition + ' ' + vendorPrefix + transition; - var parent = jqLite('
'); + var ss = createMockStyleSheet($document, $window); + ss.addRule('.klass-add', 'transition:11s linear all'); + ss.addRule('.klass-add', vendorPrefix + 'transition:11s linear all'); + ss.addRule('.klass-remove', 'transition:11s linear all'); + ss.addRule('.klass-remove', vendorPrefix + 'transition:11s linear all'); + + var parent = jqLite('
'); $rootElement.append(parent); body.append($rootElement); var element = jqLite(parent.find('span')); diff --git a/test/privateMocksSpec.js b/test/privateMocksSpec.js new file mode 100644 index 000000000000..e58a2b750866 --- /dev/null +++ b/test/privateMocksSpec.js @@ -0,0 +1,36 @@ +describe('private mocks', function() { + describe('createMockStyleSheet', function() { + + it('should allow custom styles to be created and removed when the stylesheet is destroyed', + inject(function($compile, $document, $window, $rootElement, $rootScope) { + + var doc = $document[0]; + var count = doc.styleSheets.length; + var stylesheet = createMockStyleSheet($document, $window); + expect(doc.styleSheets.length).toBe(count + 1); + + jqLite(doc.body).append($rootElement); + + var elm = $compile('
...
')($rootScope); + $rootElement.append(elm); + + expect(getStyle(elm, 'paddingTop')).toBe('0px'); + + stylesheet.addRule('.padded', 'padding-top:2px'); + + expect(getStyle(elm, 'paddingTop')).toBe('2px'); + + stylesheet.destroy(); + + expect(getStyle(elm, 'paddingTop')).toBe('0px'); + + function getStyle(element, key) { + var node = element[0]; + return node.currentStyle ? + node.currentStyle[key] : + $window.getComputedStyle(node)[key]; + }; + })); + + }); +}); From 2c9a828d1b5076aab087d192f0cea3d2e25b0aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Wed, 21 Aug 2013 15:03:41 -0400 Subject: [PATCH 2/3] fix($animate): only execute a timeout when transitions or keyframe animations are used ngAnimate causes a 1ms flicker on the screen when no CSS animations are present on the element. The solution is to change $animate to only use $timeouts when a duration is found on the element before the transition/keyframe animation takes over. Closes #3613 --- docs/component-spec/annotationsSpec.js | 11 +- src/ngAnimate/animate.js | 149 +++---- test/ngAnimate/animateSpec.js | 535 +++++++++++++------------ 3 files changed, 364 insertions(+), 331 deletions(-) diff --git a/docs/component-spec/annotationsSpec.js b/docs/component-spec/annotationsSpec.js index 6dcde906e690..08eb7a2718ae 100644 --- a/docs/component-spec/annotationsSpec.js +++ b/docs/component-spec/annotationsSpec.js @@ -111,14 +111,12 @@ describe('Docs Annotations', function() { expect(foldout.html()).toContain('loading'); })); - it('should download a foldout HTML page and animate the contents', inject(function($httpBackend, $timeout) { + it('should download a foldout HTML page and animate the contents', inject(function($httpBackend, $timeout, $sniffer) { $httpBackend.expect('GET', url).respond('hello'); element.triggerHandler('click'); $httpBackend.flush(); - $timeout.flushNext(0); - $timeout.flushNext(1); $timeout.flushNext(0); $timeout.flushNext(1000); @@ -127,27 +125,22 @@ describe('Docs Annotations', function() { expect(foldout.text()).toContain('hello'); })); - it('should hide then show when clicked again', inject(function($httpBackend, $timeout) { + it('should hide then show when clicked again', inject(function($httpBackend, $timeout, $sniffer) { $httpBackend.expect('GET', url).respond('hello'); //enter element.triggerHandler('click'); $httpBackend.flush(); $timeout.flushNext(0); - $timeout.flushNext(1); - $timeout.flushNext(0); $timeout.flushNext(1000); //hide element.triggerHandler('click'); - $timeout.flushNext(1); $timeout.flushNext(0); $timeout.flushNext(200); - $timeout.flushNext(0); //show element.triggerHandler('click'); - $timeout.flushNext(1); $timeout.flushNext(0); $timeout.flushNext(500); $timeout.flushNext(0); diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 4207470b3792..1c1a0cba05cc 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -282,7 +282,9 @@ angular.module('ngAnimate', ['ng']) */ enter : function(element, parent, after, done) { $delegate.enter(element, parent, after); - performAnimation('enter', 'ng-enter', element, parent, after, done); + performAnimation('enter', 'ng-enter', element, parent, after, function() { + $timeout(done || noop, 0, false); + }); }, /** @@ -350,7 +352,9 @@ angular.module('ngAnimate', ['ng']) */ move : function(element, parent, after, done) { $delegate.move(element, parent, after); - performAnimation('move', 'ng-move', element, null, null, done); + performAnimation('move', 'ng-move', element, null, null, function() { + $timeout(done || noop, 0, false); + }); }, /** @@ -361,7 +365,8 @@ angular.module('ngAnimate', ['ng']) * @description * Triggers a custom animation event based off the className variable and then attaches the className value to the element as a CSS class. * Unlike the other animation methods, the animate service will suffix the className value with {@type -add} in order to provide - * the animate service the setup and active CSS classes in order to trigger the animation. + * the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if no CSS transitions + * or keyframes are defined on the -add CSS class). * * Below is a breakdown of each step that occurs during addClass animation: * @@ -395,7 +400,8 @@ angular.module('ngAnimate', ['ng']) * @description * Triggers a custom animation event based off the className variable and then removes the CSS class provided by the className value * from the element. Unlike the other animation methods, the animate service will suffix the className value with {@type -remove} in - * order to provide the animate service the setup and active CSS classes in order to trigger the animation. + * order to provide the animate service the setup and active CSS classes in order to trigger the animation (this will be skipped if + * no CSS transitions or keyframes are defined on the -remove CSS class). * * Below is a breakdown of each step that occurs during removeClass animation: * @@ -546,90 +552,89 @@ angular.module('ngAnimate', ['ng']) function animate(element, className, done) { if (!($sniffer.transitions || $sniffer.animations)) { done(); - } else { - var activeClassName = ''; - $timeout(startAnimation, 1, false); - - //this acts as the cancellation function in case - //a new animation is triggered while another animation - //is still going on (otherwise the active className - //would still hang around until the timer is complete). - return onEnd; - } - - function parseMaxTime(str) { - var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : []; - forEach(values, function(value) { - total = Math.max(parseFloat(value) || 0, total); - }); - return total; + return; } - function startAnimation() { - var duration = 0; - forEach(className.split(' '), function(klass, i) { - activeClassName += (i > 0 ? ' ' : '') + klass + '-active'; - }); + //one day all browsers will have these properties + var w3cAnimationProp = 'animation'; + var w3cTransitionProp = 'transition'; - element.addClass(activeClassName); + //but some still use vendor-prefixed styles + var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation'; + var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition'; - //one day all browsers will have these properties - var w3cAnimationProp = 'animation'; - var w3cTransitionProp = 'transition'; + var durationKey = 'Duration', + delayKey = 'Delay', + animationIterationCountKey = 'IterationCount'; - //but some still use vendor-prefixed styles - var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation'; - var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition'; + //we want all the styles defined before and after + var duration = 0, ELEMENT_NODE = 1; + forEach(element, function(element) { + if (element.nodeType == ELEMENT_NODE) { + var elementStyles = $window.getComputedStyle(element) || {}; - var durationKey = 'Duration', - delayKey = 'Delay', - animationIterationCountKey = 'IterationCount'; + var transitionDelay = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + delayKey]), + parseMaxTime(elementStyles[vendorTransitionProp + delayKey])); - //we want all the styles defined before and after - var ELEMENT_NODE = 1; - forEach(element, function(element) { - if (element.nodeType == ELEMENT_NODE) { - var elementStyles = $window.getComputedStyle(element) || {}; + var animationDelay = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + delayKey]), + parseMaxTime(elementStyles[vendorAnimationProp + delayKey])); - var transitionDelay = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + delayKey]), - parseMaxTime(elementStyles[vendorTransitionProp + delayKey])); + var transitionDuration = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + durationKey]), + parseMaxTime(elementStyles[vendorTransitionProp + durationKey])); - var animationDelay = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + delayKey]), - parseMaxTime(elementStyles[vendorAnimationProp + delayKey])); + var animationDuration = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + durationKey]), + parseMaxTime(elementStyles[vendorAnimationProp + durationKey])); - var transitionDuration = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + durationKey]), - parseMaxTime(elementStyles[vendorTransitionProp + durationKey])); - - var animationDuration = Math.max(parseMaxTime(elementStyles[w3cAnimationProp + durationKey]), - parseMaxTime(elementStyles[vendorAnimationProp + durationKey])); + if(animationDuration > 0) { + animationDuration *= Math.max(parseInt(elementStyles[w3cAnimationProp + animationIterationCountKey]) || 0, + parseInt(elementStyles[vendorAnimationProp + animationIterationCountKey]) || 0, + 1); + } - if(animationDuration > 0) { - animationDuration *= Math.max(parseInt(elementStyles[w3cAnimationProp + animationIterationCountKey]) || 0, - parseInt(elementStyles[vendorAnimationProp + animationIterationCountKey]) || 0, - 1); - } + duration = Math.max(animationDelay + animationDuration, + transitionDelay + transitionDuration, + duration); + } + }); - duration = Math.max(animationDelay + animationDuration, - transitionDelay + transitionDuration, - duration); - } + /* there is no point in performing a reflow if the animation + timeout is empty (this would cause a flicker bug normally + in the page */ + if(duration > 0) { + var activeClassName = ''; + forEach(className.split(' '), function(klass, i) { + activeClassName += (i > 0 ? ' ' : '') + klass + '-active'; }); - $timeout(done, duration * 1000, false); + $timeout(function() { + element.addClass(activeClassName); + $timeout(done, duration * 1000, false); + },0,false); + + //this will automatically be called by $animate so + //there is no need to attach this internally to the + //timeout done method + return function onEnd(cancelled) { + element.removeClass(activeClassName); + + //only when the animation is cancelled is the done() + //function not called for this animation therefore + //this must be also called + if(cancelled) { + done(); + } + } + } + else { + done(); } - //this will automatically be called by $animate so - //there is no need to attach this internally to the - //timeout done method - function onEnd(cancelled) { - element.removeClass(activeClassName); - - //only when the animation is cancelled is the done() - //function not called for this animation therefore - //this must be also called - if(cancelled) { - done(); - } + function parseMaxTime(str) { + var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : []; + forEach(values, function(value) { + total = Math.max(parseFloat(value) || 0, total); + }); + return total; } } diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 3702664037c8..2d9d25afc688 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -4,9 +4,28 @@ describe("ngAnimate", function() { beforeEach(module('ngAnimate')); + var ss, body; + beforeEach(module(function() { + body = jqLite(document.body); + return function($window, $document, $animate, $timeout) { + ss = createMockStyleSheet($document, $window); + try { + $timeout.flush(); + } catch(e) {}; + $animate.enabled(true); + }; + })); + + afterEach(function(){ + if(ss) { + ss.destroy(); + } + dealoc(body); + }); + describe("$animate", function() { - var body, element, $rootElement; + var element, $rootElement; function html(html) { body.append($rootElement); @@ -15,21 +34,6 @@ describe("ngAnimate", function() { return element; } - beforeEach(module(function() { - // we need to run animation on attached elements; - body = jqLite(document.body); - return function($animate, $timeout) { - try { - $timeout.flush(); - } catch(e) {}; - $animate.enabled(true); - }; - })); - - afterEach(function(){ - dealoc(body); - }); - describe("enable / disable", function() { it("should disable and enable the animations", function() { @@ -104,9 +108,19 @@ describe("ngAnimate", function() { } } }); - return function($animate, $compile, $rootScope, $rootElement) { + return function($animate, $compile, $rootScope, $rootElement, $sniffer) { element = $compile('
')($rootScope); child = $compile('
')($rootScope); + + angular.forEach(['.ng-hide-add', '.ng-hide-remove', '.ng-enter', '.ng-leave', '.ng-move'], function(selector) { + ss.addRule(selector, 'transition:1s linear all;' + + vendorPrefix + 'transition:1s linear all'); + }); + + child = $compile('
...
')($rootScope); + jqLite($document[0].body).append($rootElement); + element.append(child); + after = $compile('
')($rootScope); $rootElement.append(element); }; @@ -115,10 +129,19 @@ describe("ngAnimate", function() { it("should animate the enter animation event", inject(function($animate, $rootScope, $sniffer, $timeout) { + element[0].removeChild(child[0]); expect(element.contents().length).toBe(0); $animate.enter(child, element); - $timeout.flush($sniffer.transitions ? 1 : 0); + $timeout.flushNext(0); + + if($sniffer.transitions) { + expect(child.hasClass('ng-enter')).toBe(true); + $timeout.flushNext(0); + expect(child.hasClass('ng-enter-active')).toBe(true); + $timeout.flushNext(1000); + } + $timeout.flush(); expect(element.contents().length).toBe(1); })); @@ -126,10 +149,15 @@ describe("ngAnimate", function() { it("should animate the leave animation event", inject(function($animate, $rootScope, $sniffer, $timeout) { - element.append(child); expect(element.contents().length).toBe(1); $animate.leave(child); - $timeout.flush($sniffer.transitions ? 1 : 0); + if($sniffer.transitions) { + expect(child.hasClass('ng-leave')).toBe(true); + $timeout.flushNext(0); + expect(child.hasClass('ng-leave-active')).toBe(true); + $timeout.flushNext(1000); + } + $timeout.flush(); expect(element.contents().length).toBe(0); })); @@ -138,6 +166,8 @@ describe("ngAnimate", function() { inject(function($animate, $compile, $rootScope, $sniffer, $timeout) { $rootScope.$digest(); + element.html(''); + var child1 = $compile('
1
')($rootScope); var child2 = $compile('
2
')($rootScope); element.append(child1); @@ -145,59 +175,57 @@ describe("ngAnimate", function() { expect(element.text()).toBe('12'); $animate.move(child1, element, child2); expect(element.text()).toBe('21'); - if($sniffer.transitions) { - $timeout.flushNext(0); - $timeout.flushNext(1); - } })); it("should animate the show animation event", inject(function($animate, $rootScope, $sniffer, $timeout) { $rootScope.$digest(); - element.addClass('ng-hide'); - expect(element).toBeHidden(); - $animate.removeClass(element, 'ng-hide'); + child.addClass('ng-hide'); + expect(child).toBeHidden(); + $animate.removeClass(child, 'ng-hide'); if($sniffer.transitions) { - expect(element.hasClass('ng-hide-remove')).toBe(true); - $timeout.flushNext(1); - expect(element.hasClass('ng-hide-remove-active')).toBe(true); + expect(child.hasClass('ng-hide-remove')).toBe(true); $timeout.flushNext(0); + expect(child.hasClass('ng-hide-remove-active')).toBe(true); + $timeout.flushNext(1000); } - expect(element.hasClass('ng-hide-remove')).toBe(false); - expect(element.hasClass('ng-hide-remove-active')).toBe(false); - expect(element).toBeShown(); + expect(child.hasClass('ng-hide-remove')).toBe(false); + expect(child.hasClass('ng-hide-remove-active')).toBe(false); + expect(child).toBeShown(); })); it("should animate the hide animation event", inject(function($animate, $rootScope, $sniffer, $timeout) { $rootScope.$digest(); - expect(element).toBeShown(); - $animate.addClass(element, 'ng-hide'); + expect(child).toBeShown(); + $animate.addClass(child, 'ng-hide'); if($sniffer.transitions) { - expect(element.hasClass('ng-hide-add')).toBe(true); - $timeout.flushNext(1); - expect(element.hasClass('ng-hide-add-active')).toBe(true); + expect(child.hasClass('ng-hide-add')).toBe(true); $timeout.flushNext(0); + expect(child.hasClass('ng-hide-add-active')).toBe(true); + $timeout.flushNext(1000); } $timeout.flush(); - expect(element).toBeHidden(); + expect(child).toBeHidden(); })); - it("should assign the ngAnimate string to all events if a string is given", - inject(function($animate, $sniffer, $rootScope, $timeout, $browser) { + it("should assign the ng-event className to all animation events when transitions/keyframes are used", + inject(function($animate, $sniffer, $rootScope, $timeout, $browser, $compile, $rootElement, $document) { if (!$sniffer.transitions) return; $rootScope.$digest(); + element[0].removeChild(child[0]); //enter $animate.enter(child, element); $timeout.flushNext(0); expect(child.attr('class')).toContain('ng-enter'); - $timeout.flushNext(1); + $timeout.flushNext(0); expect(child.attr('class')).toContain('ng-enter-active'); + $timeout.flushNext(1000); $timeout.flushNext(0); //move @@ -205,32 +233,33 @@ describe("ngAnimate", function() { $animate.move(child, element, after); $timeout.flushNext(0); expect(child.attr('class')).toContain('ng-move'); - $timeout.flushNext(1); + $timeout.flushNext(0); expect(child.attr('class')).toContain('ng-move-active'); + $timeout.flushNext(1000); $timeout.flushNext(0); //hide $animate.addClass(child, 'ng-hide'); expect(child.attr('class')).toContain('ng-hide-add'); - $timeout.flushNext(1); - expect(child.attr('class')).toContain('ng-hide-add-active'); $timeout.flushNext(0); + expect(child.attr('class')).toContain('ng-hide-add-active'); + $timeout.flushNext(1000); $timeout.flushNext(0); //show $animate.removeClass(child, 'ng-hide'); expect(child.attr('class')).toContain('ng-hide-remove'); - $timeout.flushNext(1); - expect(child.attr('class')).toContain('ng-hide-remove-active'); $timeout.flushNext(0); + expect(child.attr('class')).toContain('ng-hide-remove-active'); + $timeout.flushNext(1000); $timeout.flushNext(0); //leave $animate.leave(child); expect(child.attr('class')).toContain('ng-leave'); - $timeout.flushNext(1); - expect(child.attr('class')).toContain('ng-leave-active'); $timeout.flushNext(0); + expect(child.attr('class')).toContain('ng-leave-active'); + $timeout.flushNext(1000); $timeout.flushNext(0); })); @@ -252,10 +281,6 @@ describe("ngAnimate", function() { $animate.enabled(true); $animate.removeClass(element, 'ng-hide'); - if($sniffer.transitions) { - $timeout.flushNext(0); - $timeout.flushNext(1); - } $timeout.flushNext(0); expect(element.text()).toBe('memento'); })); @@ -268,9 +293,6 @@ describe("ngAnimate", function() { expect(element).toBeShown(); $animate.addClass(child, 'ng-hide'); - if($sniffer.transitions) { - $timeout.flushNext(1); - } expect(child).toBeShown(); $animate.leave(child); @@ -280,15 +302,14 @@ describe("ngAnimate", function() { child.css('display','block'); child.removeClass('ng-hide'); - if($sniffer.transitions) { - $timeout.flushNext(0); - } $timeout.flushNext(0); if($sniffer.transitions) { - $timeout.flushNext(1); $timeout.flushNext(0); + $timeout.flushNext(0); + expect(element.children().length).toBe(1); //still animating + $timeout.flushNext(1000); + $timeout.flushNext(1000); } - expect(element.children().length).toBe(1); //still animating $timeout.flushNext(2000); $timeout.flushNext(2000); $timeout.flushNext(0); @@ -305,8 +326,8 @@ describe("ngAnimate", function() { child.addClass('custom-delay ng-hide'); $animate.removeClass(child, 'ng-hide'); if($sniffer.transitions) { - $timeout.flushNext(1); $timeout.flushNext(0); + $timeout.flushNext(1000); } $timeout.flushNext(2000); @@ -322,11 +343,6 @@ describe("ngAnimate", function() { element.append(child); $animate.addClass(child, 'custom-delay'); - - if($sniffer.transitions) { - $timeout.flushNext(1); - $timeout.flushNext(0); - } $animate.addClass(child, 'custom-long-delay'); expect(child.hasClass('animation-cancelled')).toBe(true); @@ -344,13 +360,7 @@ describe("ngAnimate", function() { element.data('foo', 'bar'); $animate.removeClass(element, 'ng-hide'); - if($sniffer.transitions) { - $timeout.flushNext(1); - $timeout.flushNext(0); - } - $animate.addClass(element, 'ng-hide'); - expect(element.data('foo')).toEqual('bar'); })); @@ -359,41 +369,30 @@ describe("ngAnimate", function() { inject(function($animate, $rootScope, $compile, $sniffer, $timeout) { $animate.addClass(element, 'custom-delay custom-long-delay'); - if($sniffer.transitions) { - expect(element[0].className).toContain('custom-delay-add custom-long-delay-add'); - $timeout.flushNext(1); - expect(element[0].className).toContain('custom-delay-add-active custom-long-delay-add-active'); - $timeout.flushNext(0); - } $timeout.flushNext(2000); $timeout.flushNext(20000); $timeout.flushNext(0); - expect(element.hasClass('custom-delay')).toBe(true); - expect(element.hasClass('custom-delay-add')).toBe(false); - expect(element.hasClass('custom-delay-add-active')).toBe(false); - expect(element.hasClass('custom-long-delay')).toBe(true); - expect(element.hasClass('custom-long-delay-add')).toBe(false); - expect(element.hasClass('custom-long-delay-add-active')).toBe(false); })); it("should allow both multiple JS and CSS animations which run in parallel", inject(function($animate, $rootScope, $compile, $sniffer, $timeout, _$rootElement_) { $rootElement = _$rootElement_; - var vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; - var style = 'transition: 1s linear all;' + - vendorPrefix + 'transition: 1s linear all;' + ss.addRule('.ng-hide-add', 'transition:1s linear all;' + + vendorPrefix + 'transition:1s linear all'); + ss.addRule('.ng-hide-remove', 'transition:1s linear all;' + + vendorPrefix + 'transition:1s linear all'); - element = $compile(html('
1
'))($rootScope); + element = $compile(html('
1
'))($rootScope); element.addClass('custom-delay custom-long-delay'); $rootScope.$digest(); $animate.removeClass(element, 'ng-hide'); if($sniffer.transitions) { - $timeout.flushNext(1); + $timeout.flushNext(0); $timeout.flushNext(1000); } $timeout.flushNext(2000); @@ -426,16 +425,19 @@ describe("ngAnimate", function() { it("should properly detect and make use of CSS Animations", inject(function($animate, $rootScope, $compile, $sniffer, $timeout) { - var style = 'animation: some_animation 4s linear 0s 1 alternate;' + - vendorPrefix + 'animation: some_animation 4s linear 0s 1 alternate;'; - element = $compile(html('
1
'))($rootScope); + ss.addRule('.ng-hide-add', 'animation: some_animation 4s linear 0s 1 alternate;' + + vendorPrefix + 'animation: some_animation 4s linear 0s 1 alternate'); + ss.addRule('.ng-hide-remove', 'animation: some_animation 4s linear 0s 1 alternate;' + + vendorPrefix + 'animation: some_animation 4s linear 0s 1 alternate'); + + element = $compile(html('
1
'))($rootScope); element.addClass('ng-hide'); expect(element).toBeHidden(); $animate.removeClass(element, 'ng-hide'); if ($sniffer.animations) { - $timeout.flushNext(1); + $timeout.flushNext(0); $timeout.flushNext(4000); } expect(element).toBeShown(); @@ -448,14 +450,18 @@ describe("ngAnimate", function() { 'animation-iteration-count: 3;' + vendorPrefix + 'animation-duration: 2s;' + vendorPrefix + 'animation-iteration-count: 3;'; - element = $compile(html('
1
'))($rootScope); + + ss.addRule('.ng-hide-add', style); + ss.addRule('.ng-hide-remove', style); + + element = $compile(html('
1
'))($rootScope); element.addClass('ng-hide'); expect(element).toBeHidden(); $animate.removeClass(element, 'ng-hide'); if ($sniffer.animations) { - $timeout.flushNext(1); + $timeout.flushNext(0); $timeout.flushNext(6000); } expect(element).toBeShown(); @@ -468,14 +474,18 @@ describe("ngAnimate", function() { 'animation-iteration-count: infinite;' + vendorPrefix + 'animation-duration: 2s;' + vendorPrefix + 'animation-iteration-count: infinite;'; - element = $compile(html('
1
'))($rootScope); + + ss.addRule('.ng-hide-add', style); + ss.addRule('.ng-hide-remove', style); + + element = $compile(html('
1
'))($rootScope); element.addClass('ng-hide'); expect(element).toBeHidden(); $animate.removeClass(element, 'ng-hide'); if ($sniffer.animations) { - $timeout.flushNext(1); + $timeout.flushNext(0); $timeout.flushNext(2000); } expect(element).toBeShown(); @@ -490,14 +500,18 @@ describe("ngAnimate", function() { vendorPrefix + 'animation-duration: 2s;' + vendorPrefix + 'animation-delay: 10s;' + vendorPrefix + 'animation-iteration-count: 5;'; - element = $compile(html('
1
'))($rootScope); + + ss.addRule('.ng-hide-add', style); + ss.addRule('.ng-hide-remove', style); + + element = $compile(html('
1
'))($rootScope); element.addClass('ng-hide'); expect(element).toBeHidden(); $animate.removeClass(element, 'ng-hide'); if ($sniffer.transitions) { - $timeout.flushNext(1); + $timeout.flushNext(0); $timeout.flushNext(20000); } expect(element).toBeShown(); @@ -509,7 +523,10 @@ describe("ngAnimate", function() { var style = 'animation: some_animation 2s linear 0s 1 alternate;' + vendorPrefix + 'animation: some_animation 2s linear 0s 1 alternate;' - element = $compile(html('
1
'))($rootScope); + ss.addRule('.ng-hide-add', style); + ss.addRule('.ng-hide-remove', style); + + element = $compile(html('
1
'))($rootScope); element.addClass('ng-hide'); expect(element).toBeHidden(); $animate.removeClass(element, 'ng-hide'); @@ -522,13 +539,16 @@ describe("ngAnimate", function() { var style = 'animation: some_animation 2s linear 0s 1 alternate;' + vendorPrefix + 'animation: some_animation 2s linear 0s 1 alternate;' - element = $compile(html('
1
'))($rootScope); + ss.addRule('.ng-hide-add', style); + ss.addRule('.ng-hide-remove', style); + + element = $compile(html('
1
'))($rootScope); element.addClass('custom'); $animate.removeClass(element, 'ng-hide'); if($sniffer.animations) { expect(element.hasClass('ng-hide-remove')).toBe(true); - $timeout.flushNext(1); + $timeout.flushNext(0); expect(element.hasClass('ng-hide-remove-active')).toBe(true); } @@ -539,7 +559,7 @@ describe("ngAnimate", function() { if($sniffer.animations) { //cleanup some pending animations expect(element.hasClass('ng-hide-add')).toBe(true); - $timeout.flushNext(1); + $timeout.flushNext(0); expect(element.hasClass('ng-hide-add-active')).toBe(true); $timeout.flushNext(2000); } @@ -552,8 +572,14 @@ describe("ngAnimate", function() { it("should skip transitions if disabled and run when enabled", inject(function($animate, $rootScope, $compile, $sniffer, $timeout) { + var style = 'transition: 1s linear all;' + + vendorPrefix + 'transition: 1s linear all'; + + ss.addRule('.ng-hide-add', style); + ss.addRule('.ng-hide-remove', style); + $animate.enabled(false); - element = $compile(html('
1
'))($rootScope); + element = $compile(html('
1
'))($rootScope); element.addClass('ng-hide'); expect(element).toBeHidden(); @@ -569,7 +595,7 @@ describe("ngAnimate", function() { $animate.removeClass(element, 'ng-hide'); if ($sniffer.transitions) { - $timeout.flushNext(1); + $timeout.flushNext(0); $timeout.flushNext(1000); } $timeout.flushNext(0); @@ -578,12 +604,21 @@ describe("ngAnimate", function() { it("should skip animations if disabled and run when enabled picking the longest specified duration", inject(function($animate, $rootScope, $compile, $sniffer, $timeout) { - element = $compile(html('
foo
'))($rootScope); + + var style = 'transition-duration: 1s, 2000ms, 1s;' + + 'transition-property: height, left, opacity;' + + vendorPrefix + 'transition-duration: 1s, 2000ms, 1s;' + + vendorPrefix + 'transition-property: height, left, opacity'; + + ss.addRule('.ng-hide-add', style); + ss.addRule('.ng-hide-remove', style); + + element = $compile(html('
foo
'))($rootScope); element.addClass('ng-hide'); $animate.removeClass(element, 'ng-hide'); if ($sniffer.transitions) { expect(element).toBeHidden(); - $timeout.flushNext(1); + $timeout.flushNext(0); $timeout.flushNext(2000); } $timeout.flush(); @@ -593,10 +628,17 @@ describe("ngAnimate", function() { it("should skip animations if disabled and run when enabled picking the longest specified duration/delay combination", inject(function($animate, $rootScope, $compile, $sniffer, $timeout) { $animate.enabled(false); - element = $compile(html('
foo
'))($rootScope); + var style = 'transition-duration: 1s, 0s, 1s; ' + + 'transition-delay: 2s, 1000ms, 2s; ' + + 'transition-property: height, left, opacity;' + + vendorPrefix + 'transition-duration: 1s, 0s, 1s; ' + + vendorPrefix + 'transition-delay: 2s, 1000ms, 2s; ' + + vendorPrefix + 'transition-property: height, left, opacity'; + + ss.addRule('.ng-hide-add', style); + ss.addRule('.ng-hide-remove', style); + + element = $compile(html('
foo
'))($rootScope); element.addClass('ng-hide'); $animate.removeClass(element, 'ng-hide'); @@ -611,7 +653,7 @@ describe("ngAnimate", function() { $animate.removeClass(element, 'ng-hide'); if ($sniffer.transitions) { - $timeout.flushNext(1); + $timeout.flushNext(0); $timeout.flushNext(3000); } $timeout.flush(); @@ -620,19 +662,22 @@ describe("ngAnimate", function() { it("should select the highest duration and delay", inject(function($animate, $rootScope, $compile, $sniffer, $timeout) { - var styles = 'transition:1s linear all 2s;' + + var style = 'transition:1s linear all 2s;' + vendorPrefix + 'transition:1s linear all 2s;' + 'animation:my_ani 10s 1s;' + vendorPrefix + 'animation:my_ani 10s 1s;'; - element = $compile(html('
foo
'))($rootScope); + ss.addRule('.ng-hide-add', style); + ss.addRule('.ng-hide-remove', style); + + element = $compile(html('
foo
'))($rootScope); element.addClass('ng-hide'); expect(element).toBeHidden(); $animate.removeClass(element, 'ng-hide'); if ($sniffer.transitions) { - $timeout.flushNext(1); + $timeout.flushNext(0); $timeout.flushNext(11000); } expect(element).toBeShown(); @@ -643,13 +688,16 @@ describe("ngAnimate", function() { var style = 'transition: 1s linear all;' + vendorPrefix + 'transition: 1s linear all;' - element = $compile(html('
1
'))($rootScope); + ss.addRule('.ng-hide-add', style); + ss.addRule('.ng-hide-remove', style); + + element = $compile(html('
1
'))($rootScope); element.addClass('ng-hide'); $animate.removeClass(element, 'ng-hide'); if($sniffer.transitions) { expect(element.hasClass('ng-hide-remove')).toBe(true); - $timeout.flushNext(1); + $timeout.flushNext(0); expect(element.hasClass('ng-hide-remove-active')).toBe(true); $timeout.flushNext(1000); } @@ -661,7 +709,7 @@ describe("ngAnimate", function() { $animate.addClass(element, 'ng-hide'); if($sniffer.transitions) { expect(element.hasClass('ng-hide-add')).toBe(true); - $timeout.flushNext(1); + $timeout.flushNext(0); expect(element.hasClass('ng-hide-add-active')).toBe(true); } })); @@ -672,6 +720,11 @@ describe("ngAnimate", function() { it('should re-evaluate the CSS classes for an animation each time', inject(function($animate, $rootScope, $sniffer, $rootElement, $timeout) { + ss.addRule('.ng-enter', 'transition:22s linear all;' + + vendorPrefix + 'transition:22s linear all'); + ss.addRule('.ng-enter', 'transition:22s linear all;' + + vendorPrefix + 'transition:22s linear all'); + var parent = jqLite('
'); var element = parent.find('span'); $rootElement.append(parent); @@ -683,10 +736,11 @@ describe("ngAnimate", function() { if ($sniffer.transitions) { expect(element.hasClass('abc ng-enter')).toBe(true); - $timeout.flushNext(1); - expect(element.hasClass('abc ng-enter ng-enter-active')).toBe(true); $timeout.flushNext(0); + expect(element.hasClass('abc ng-enter ng-enter-active')).toBe(true); + $timeout.flushNext(22000); } + $timeout.flushNext(0); expect(element.hasClass('abc')).toBe(true); element[0].className = 'xyz'; @@ -695,16 +749,22 @@ describe("ngAnimate", function() { if ($sniffer.transitions) { expect(element.hasClass('xyz')).toBe(true); - $timeout.flushNext(1); - expect(element.hasClass('xyz ng-enter ng-enter-active')).toBe(true); $timeout.flushNext(0); + expect(element.hasClass('xyz ng-enter ng-enter-active')).toBe(true); + $timeout.flushNext(22000); } + $timeout.flushNext(0); expect(element.hasClass('xyz')).toBe(true); })); it('should only append active to the newly append CSS className values', inject(function($animate, $rootScope, $sniffer, $rootElement, $timeout) { + ss.addRule('.ng-enter', 'transition:9s linear all;' + + vendorPrefix + 'transition:9s linear all'); + ss.addRule('.ng-enter', 'transition:9s linear all;' + + vendorPrefix + 'transition:9s linear all'); + var parent = jqLite('
'); var element = parent.find('span'); $rootElement.append(parent); @@ -716,11 +776,11 @@ describe("ngAnimate", function() { $timeout.flushNext(0); if($sniffer.transitions) { expect(element.hasClass('one two ng-enter')).toBe(true); - $timeout.flushNext(1); + $timeout.flushNext(0); expect(element.hasClass('one two ng-enter ng-enter-active')).toBe(true); expect(element.hasClass('one-active')).toBe(false); expect(element.hasClass('two-active')).toBe(false); - $timeout.flushNext(0); + $timeout.flushNext(9000); } expect(element.hasClass('one two')).toBe(true); @@ -765,11 +825,9 @@ describe("ngAnimate", function() { flag = true; }); - $timeout.flushNext(0); - if($sniffer.transitions) { - $timeout.flushNext(1); - $timeout.flushNext(0); - } + $timeout.flushNext(0); //move operation callback + $timeout.flushNext(0); //ngAnimate callback + expect(flag).toBe(true); })); @@ -786,10 +844,6 @@ describe("ngAnimate", function() { flag = true; }); - if($sniffer.transitions) { - $timeout.flushNext(1); - $timeout.flushNext(0); - } $timeout.flushNext(0); expect(flag).toBe(true); })); @@ -808,11 +862,8 @@ describe("ngAnimate", function() { flag = true; }); - $timeout.flushNext(0); - if($sniffer.transitions) { - $timeout.flushNext(1); - $timeout.flushNext(0); - } + $timeout.flushNext(0); //move operation callback + $timeout.flushNext(0); //ngAnimate callback expect(flag).toBe(true); expect(element.parent().id).toBe(parent2.id); @@ -830,23 +881,13 @@ describe("ngAnimate", function() { $animate.addClass(element, 'on', function() { signature += 'A'; }); - - if($sniffer.transitions) { - $timeout.flushNext(1); - $timeout.flushNext(0); - } $timeout.flushNext(0); $animate.removeClass(element, 'on', function() { signature += 'B'; }); - if($sniffer.transitions) { - $timeout.flushNext(1); - $timeout.flushNext(0); - } $timeout.flushNext(0); - expect(signature).toBe('AB'); })); @@ -871,9 +912,12 @@ describe("ngAnimate", function() { it("should fire a done callback when provided with a css animation/transition", inject(function($animate, $rootScope, $compile, $sniffer, $rootElement, $timeout) { - var transition = 'transition:1s linear all;'; - var style = transition + ' ' + vendorPrefix + transition; - var parent = jqLite('
'); + ss.addRule('.ng-hide-add', 'transition:1s linear all;' + + vendorPrefix + 'transition:1s linear all'); + ss.addRule('.ng-hide-remove', 'transition:1s linear all;' + + vendorPrefix + 'transition:1s linear all'); + + var parent = jqLite('
'); $rootElement.append(parent); body.append($rootElement); var element = parent.find('span'); @@ -884,7 +928,7 @@ describe("ngAnimate", function() { }); if($sniffer.transitions) { - $timeout.flushNext(1); + $timeout.flushNext(0); $timeout.flushNext(1000); } $timeout.flushNext(0); @@ -905,10 +949,6 @@ describe("ngAnimate", function() { flag = true; }); - if($sniffer.transitions) { - $timeout.flushNext(1); - $timeout.flushNext(0); - } $timeout.flushNext(2000); $timeout.flushNext(0); expect(flag).toBe(true); @@ -917,6 +957,11 @@ describe("ngAnimate", function() { it("should fire the callback right away if another animation is called right after", inject(function($animate, $rootScope, $compile, $sniffer, $rootElement, $timeout) { + ss.addRule('.ng-hide-add', 'transition:9s linear all;' + + vendorPrefix + 'transition:9s linear all'); + ss.addRule('.ng-hide-remove', 'transition:9s linear all;' + + vendorPrefix + 'transition:9s linear all'); + var parent = jqLite('
'); $rootElement.append(parent); body.append($rootElement); @@ -932,14 +977,16 @@ describe("ngAnimate", function() { if($sniffer.transitions) { $timeout.flushNext(0); //callback for removeClass - $timeout.flushNext(1); $timeout.flushNext(0); - $timeout.flushNext(1); + $timeout.flushNext(0); + $timeout.flushNext(9000); + $timeout.flushNext(9000); + $timeout.flushNext(0); } $animate.addClass(element, 'ng-hide'); //earlier animation cancelled if($sniffer.transitions) { $timeout.flushNext(0); - $timeout.flushNext(0); + $timeout.flushNext(9000); } $timeout.flush(); expect(signature).toBe('AB'); @@ -976,28 +1023,11 @@ describe("ngAnimate", function() { var element = jqLite(parent.find('span')); $animate.addClass(element,'klass'); - - if($sniffer.transitions) { - expect(element.hasClass('klass-add')).toBe(true); - $timeout.flushNext(1); - expect(element.hasClass('klass-add-active')).toBe(true); - $timeout.flushNext(0); - expect(element.hasClass('klass-add')).toBe(false); - expect(element.hasClass('klass-add-active')).toBe(false); - } $timeout.flushNext(0); expect(element.hasClass('klass')).toBe(true); $animate.removeClass(element,'klass'); - - if($sniffer.transitions) { - expect(element.hasClass('klass')).toBe(true); - expect(element.hasClass('klass-remove')).toBe(true); - $timeout.flushNext(1); - expect(element.hasClass('klass-remove-active')).toBe(true); - $timeout.flushNext(0); - } $timeout.flushNext(0); expect(element.hasClass('klass')).toBe(false); @@ -1019,11 +1049,6 @@ describe("ngAnimate", function() { signature += 'A'; }); - if($sniffer.transitions) { - expect(element.hasClass('klass')).toBe(false); - $timeout.flushNext(1); - $timeout.flushNext(0); - } $timeout.flushNext(0); expect(element.hasClass('klass')).toBe(true); @@ -1031,11 +1056,6 @@ describe("ngAnimate", function() { signature += 'B'; }); - if($sniffer.transitions) { - expect(element.hasClass('klass')).toBe(true); - $timeout.flushNext(1); - $timeout.flushNext(0); - } $timeout.flushNext(0); expect(element.hasClass('klass')).toBe(false); @@ -1045,6 +1065,11 @@ describe("ngAnimate", function() { it("should end the current addClass animation, add the CSS class and then run the removeClass animation", inject(function($animate, $rootScope, $sniffer, $rootElement, $timeout) { + ss.addRule('.klass-add', 'transition:3s linear all;' + + vendorPrefix + 'transition:3s linear all'); + ss.addRule('.klass-remove', 'transition:3s linear all;' + + vendorPrefix + 'transition:3s linear all'); + var parent = jqLite('
'); $rootElement.append(parent); body.append($rootElement); @@ -1058,9 +1083,11 @@ describe("ngAnimate", function() { if($sniffer.transitions) { expect(element.hasClass('klass')).toBe(false); expect(element.hasClass('klass-add')).toBe(true); - $timeout.flushNext(1); + $timeout.flushNext(0); expect(element.hasClass('klass-add-active')).toBe(true); + $timeout.flushNext(3000); } + $timeout.flushNext(0); //this cancels out the older animation $animate.removeClass(element,'klass', function() { @@ -1072,6 +1099,7 @@ describe("ngAnimate", function() { expect(element.hasClass('klass-add')).toBe(false); expect(element.hasClass('klass-add-active')).toBe(false); + $timeout.flushNext(0); expect(element.hasClass('klass-remove')).toBe(true); } $timeout.flush(); @@ -1093,10 +1121,7 @@ describe("ngAnimate", function() { $animate.addClass(element,'klassy', function() { signature += 'X'; }); - if($sniffer.transitions) { - $timeout.flushNext(1); - $timeout.flushNext(0); - } + $timeout.flushNext(500); $timeout.flushNext(0); @@ -1105,10 +1130,7 @@ describe("ngAnimate", function() { $animate.removeClass(element,'klassy', function() { signature += 'Y'; }); - if($sniffer.transitions) { - $timeout.flushNext(1); - $timeout.flushNext(0); - } + $timeout.flushNext(3000); $timeout.flushNext(0); @@ -1118,13 +1140,12 @@ describe("ngAnimate", function() { })); it("should properly execute CSS animations/transitions and use callbacks when using addClass / removeClass", - inject(function($animate, $rootScope, $sniffer, $rootElement, $timeout, $window, $document) { + inject(function($animate, $rootScope, $sniffer, $rootElement, $timeout) { - var ss = createMockStyleSheet($document, $window); - ss.addRule('.klass-add', 'transition:11s linear all'); - ss.addRule('.klass-add', vendorPrefix + 'transition:11s linear all'); - ss.addRule('.klass-remove', 'transition:11s linear all'); - ss.addRule('.klass-remove', vendorPrefix + 'transition:11s linear all'); + ss.addRule('.klass-add', 'transition:11s linear all;' + + vendorPrefix + 'transition:11s linear all'); + ss.addRule('.klass-remove', 'transition:11s linear all;' + + vendorPrefix + 'transition:11s linear all'); var parent = jqLite('
'); $rootElement.append(parent); @@ -1138,7 +1159,7 @@ describe("ngAnimate", function() { }); if($sniffer.transitions) { expect(element.hasClass('klass-add')).toBe(true); - $timeout.flushNext(1); + $timeout.flushNext(0); expect(element.hasClass('klass-add-active')).toBe(true); $timeout.flushNext(11000); expect(element.hasClass('klass-add')).toBe(false); @@ -1153,7 +1174,7 @@ describe("ngAnimate", function() { }); if($sniffer.transitions) { expect(element.hasClass('klass-remove')).toBe(true); - $timeout.flushNext(1); + $timeout.flushNext(0); expect(element.hasClass('klass-remove-active')).toBe(true); $timeout.flushNext(11000); expect(element.hasClass('klass-remove')).toBe(false); @@ -1169,9 +1190,12 @@ describe("ngAnimate", function() { it("should allow for multiple css classes to be animated plus a callback when added", inject(function($animate, $rootScope, $sniffer, $rootElement, $timeout) { - var transition = 'transition:7s linear all;'; - var style = transition + ' ' + vendorPrefix + transition; - var parent = jqLite('
'); + ss.addRule('.one-add', 'transition:7s linear all;' + + vendorPrefix + 'transition:7s linear all'); + ss.addRule('.two-add', 'transition:7s linear all;' + + vendorPrefix + 'transition:7s linear all'); + + var parent = jqLite('
'); $rootElement.append(parent); body.append($rootElement); var element = jqLite(parent.find('span')); @@ -1184,7 +1208,7 @@ describe("ngAnimate", function() { if($sniffer.transitions) { expect(element.hasClass('one-add')).toBe(true); expect(element.hasClass('two-add')).toBe(true); - $timeout.flushNext(1); + $timeout.flushNext(0); expect(element.hasClass('one-add-active')).toBe(true); expect(element.hasClass('two-add-active')).toBe(true); @@ -1206,9 +1230,12 @@ describe("ngAnimate", function() { it("should allow for multiple css classes to be animated plus a callback when removed", inject(function($animate, $rootScope, $sniffer, $rootElement, $timeout) { - var transition = 'transition:9s linear all;'; - var style = transition + ' ' + vendorPrefix + transition; - var parent = jqLite('
'); + ss.addRule('.one-remove', 'transition:9s linear all;' + + vendorPrefix + 'transition:9s linear all'); + ss.addRule('.two-remove', 'transition:9s linear all;' + + vendorPrefix + 'transition:9s linear all'); + + var parent = jqLite('
'); $rootElement.append(parent); body.append($rootElement); var element = jqLite(parent.find('span')); @@ -1225,7 +1252,7 @@ describe("ngAnimate", function() { if($sniffer.transitions) { expect(element.hasClass('one-remove')).toBe(true); expect(element.hasClass('two-remove')).toBe(true); - $timeout.flushNext(1); + $timeout.flushNext(0); expect(element.hasClass('one-remove-active')).toBe(true); expect(element.hasClass('two-remove-active')).toBe(true); @@ -1246,9 +1273,10 @@ describe("ngAnimate", function() { }); }); - var $rootElement, $document; + var $rootElement, $document, vendorPrefix; beforeEach(module(function($provide) { - return function(_$rootElement_, _$document_, $animate) { + return function(_$rootElement_, _$document_, $animate, $sniffer) { + vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; $rootElement = _$rootElement_; $document = _$document_; $animate.enabled(true); @@ -1265,18 +1293,18 @@ describe("ngAnimate", function() { it("should properly animate and parse CSS3 transitions", inject(function($compile, $rootScope, $animate, $sniffer, $timeout) { - var vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; - var style = 'transition: 1s linear all;' + - vendorPrefix + 'transition: 1s linear all;'; + ss.addRule('.ng-enter', 'transition:1s linear all;' + + vendorPrefix + 'transition:1s linear all'); + var element = html($compile('
...
')($rootScope)); - var child = $compile('
...
')($rootScope); + var child = $compile('
...
')($rootScope); $animate.enter(child, element); $timeout.flushNext(0); if($sniffer.transitions) { expect(child.hasClass('ng-enter')).toBe(true); - $timeout.flushNext(1); + $timeout.flushNext(0); expect(child.hasClass('ng-enter-active')).toBe(true); $timeout.flushNext(1000); } @@ -1288,18 +1316,18 @@ describe("ngAnimate", function() { it("should properly animate and parse CSS3 animations", inject(function($compile, $rootScope, $animate, $sniffer, $timeout) { - var vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; - var style = 'animation: some_animation 4s linear 1s 2 alternate;' + - vendorPrefix + 'animation: some_animation 4s linear 1s 2 alternate;'; + ss.addRule('.ng-enter', 'some_animation 4s linear 1s 2 alternate;' + + vendorPrefix + 'animation: some_animation 4s linear 1s 2 alternate'); + var element = html($compile('
...
')($rootScope)); - var child = $compile('
...
')($rootScope); + var child = $compile('
...
')($rootScope); $animate.enter(child, element); $timeout.flushNext(0); if($sniffer.transitions) { expect(child.hasClass('ng-enter')).toBe(true); - $timeout.flushNext(1); + $timeout.flushNext(0); expect(child.hasClass('ng-enter-active')).toBe(true); $timeout.flushNext(9000); } @@ -1313,11 +1341,11 @@ describe("ngAnimate", function() { $sniffer.animations = false; $sniffer.transitions = false; - var vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; - var style = 'animation: some_animation 4s linear 1s 2 alternate;' + - vendorPrefix + 'animation: some_animation 4s linear 1s 2 alternate;'; + ss.addRule('.ng-enter', 'some_animation 4s linear 1s 2 alternate;' + + vendorPrefix + 'animation: some_animation 4s linear 1s 2 alternate'); + var element = html($compile('
...
')($rootScope)); - var child = $compile('
...
')($rootScope); + var child = $compile('
...
')($rootScope); expect(child.hasClass('ng-enter')).toBe(false); $animate.enter(child, element); @@ -1336,11 +1364,12 @@ describe("ngAnimate", function() { }); }) inject(function($compile, $rootScope, $animate, $sniffer, $timeout) { - var vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; - var style = 'transition: 1s linear all;' + - vendorPrefix + 'transition: 1s linear all;'; + + ss.addRule('.ng-enter', 'transition: 1s linear all;' + + vendorPrefix + 'transition: 1s linear all'); + var element = html($compile('
...
')($rootScope)); - var child = $compile('
...
')($rootScope); + var child = $compile('
...
')($rootScope); expect(child.hasClass('i-was-animated')).toBe(false); @@ -1350,7 +1379,7 @@ describe("ngAnimate", function() { if($sniffer.transitions) { expect(child.hasClass('ng-enter')).toBe(true); - $timeout.flushNext(1); + $timeout.flushNext(0); expect(child.hasClass('ng-enter-active')).toBe(true); } @@ -1378,27 +1407,28 @@ describe("ngAnimate", function() { } }); }); - inject(function($compile, $rootScope, $animate, $sniffer) { - }) inject(function($compile, $rootScope, $animate, $sniffer, $timeout) { - var vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; - var style = 'transition: 2s linear all;' + - vendorPrefix + 'transition: 2s linear all;'; + ss.addRule('.ng-enter', 'transition: 2s linear all;' + + vendorPrefix + 'transition: 2s linear all'); + ss.addRule('.ng-leave', 'transition: 2s linear all;' + + vendorPrefix + 'transition: 2s linear all'); + var element = html($compile('
...
')($rootScope)); - var child = $compile('
...
')($rootScope); + var child = $compile('
...
')($rootScope); $animate.enter(child, element); - $timeout.flushNext(0); //initial callback + $timeout.flushNext(0); //this is added/removed right away otherwise if($sniffer.transitions) { expect(child.hasClass('ng-enter')).toBe(true); - $timeout.flushNext(1); + $timeout.flushNext(0); } expect(child.hasClass('this-is-mine-now')).toBe(false); child.addClass('usurper'); $animate.leave(child); + $timeout.flushNext(0); expect(child.hasClass('ng-enter')).toBe(false); expect(child.hasClass('ng-enter-active')).toBe(false); @@ -1406,7 +1436,7 @@ describe("ngAnimate", function() { expect(child.hasClass('usurper')).toBe(true); expect(child.hasClass('this-is-mine-now')).toBe(true); if($sniffer.transitions) { - $timeout.flushNext(1); + $timeout.flushNext(0); } $timeout.flushNext(55); @@ -1425,10 +1455,12 @@ describe("ngAnimate", function() { it("should add and remove CSS classes and perform CSS animations during the process", inject(function($compile, $rootScope, $animate, $sniffer, $timeout) { - var vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; - var style = 'transition: 10s linear all;' + - vendorPrefix + 'transition: 10s linear all;'; - var element = html($compile('
')($rootScope)); + ss.addRule('.on-add', 'transition: 10s linear all; ' + + vendorPrefix + 'transition: 10s linear all'); + ss.addRule('.on-remove', 'transition: 10s linear all; ' + + vendorPrefix + 'transition: 10s linear all'); + + var element = html($compile('
')($rootScope)); expect(element.hasClass('on')).toBe(false); @@ -1477,10 +1509,13 @@ describe("ngAnimate", function() { }); }) inject(function($compile, $rootScope, $animate, $sniffer, $timeout) { - var vendorPrefix = '-' + $sniffer.vendorPrefix.toLowerCase() + '-'; - var style = 'transition: 5s linear all;' + - vendorPrefix + 'transition: 5s linear all;'; - var element = html($compile('
')($rootScope)); + + ss.addRule('.ng-hide-add', 'transition: 5s linear all;' + + vendorPrefix + 'transition: 5s linear all'); + ss.addRule('.ng-hide-remove', 'transition: 5s linear all;' + + vendorPrefix + 'transition: 5s linear all'); + + var element = html($compile('
')($rootScope)); element.addClass('displayer'); From c4430e79ade8b334d69ed1d4f170baa2e7f441fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Thu, 22 Aug 2013 09:30:11 -0400 Subject: [PATCH 3/3] fix($animate): skip ngAnimate animations if the provided element already has transitions/durations attached to it Closes #3587 --- docs/src/templates/css/animations.css | 7 -- src/ng/directive/ngClass.js | 10 ++- src/ngAnimate/animate.js | 98 +++++++++++++++------------ test/ngAnimate/animateSpec.js | 29 ++++++++ 4 files changed, 92 insertions(+), 52 deletions(-) diff --git a/docs/src/templates/css/animations.css b/docs/src/templates/css/animations.css index 81db50bac80a..3f62b7432bf8 100644 --- a/docs/src/templates/css/animations.css +++ b/docs/src/templates/css/animations.css @@ -64,13 +64,6 @@ height:0; } -.animate-container.animations-off * { - -webkit-transition: none; - -moz-transition: none; - -o-transition: color 0 ease-in; /* opera is special :) */ - transition: none; -} - .foldout.ng-enter, .foldout.ng-hide-add, .foldout.ng-hide-remove { diff --git a/src/ng/directive/ngClass.js b/src/ng/directive/ngClass.js index db6e7ac8934b..9edb0a3e845f 100644 --- a/src/ng/directive/ngClass.js +++ b/src/ng/directive/ngClass.js @@ -151,7 +151,7 @@ function classDirective(name, selector) { ## Animations - Example that demostrates how addition and removal of classes can be animated. + The example below demonstrates how to perform animations using ngClass. @@ -196,6 +196,14 @@ function classDirective(name, selector) { }); + + + ## ngClass and pre-existing CSS3 Transitions/Animations + The ngClass directive still supports CSS3 Transitions/Animations even if they do not follow the ngAnimate CSS naming structure. + Therefore, if any CSS3 Transition/Animation styles (outside of ngAnimate) are set on the element, then, if a ngClass animation + is triggered, the ngClass animation will be skipped so that ngAnimate can allow for the pre-existing transition or animation to + take over. This restriction allows for ngClass to still work with standard CSS3 Transitions/Animations that are defined + outside of ngAnimate. */ var ngClassDirective = classDirective('', true); diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 1c1a0cba05cc..32bbd6d50f85 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -267,8 +267,8 @@ angular.module('ngAnimate', ['ng']) * |----------------------------------------------------------------------------------------------|-----------------------------------------------| * | 1. $animate.enter(...) is called | class="my-animation" | * | 2. element is inserted into the parent element or beside the after element | class="my-animation" | - * | 3. the .ng-enter class is added to the element | class="my-animation ng-enter" | - * | 4. $animate runs any JavaScript-defined animations on the element | class="my-animation ng-enter" | + * | 3. $animate runs any JavaScript-defined animations on the element | class="my-animation" | + * | 4. the .ng-enter class is added to the element | class="my-animation ng-enter" | * | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-enter" | * | 6. the .ng-enter-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-enter ng-enter-active" | * | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-enter ng-enter-active" | @@ -302,8 +302,8 @@ angular.module('ngAnimate', ['ng']) * | Animation Step | What the element class attribute looks like | * |----------------------------------------------------------------------------------------------|----------------------------------------------| * | 1. $animate.leave(...) is called | class="my-animation" | - * | 2. the .ng-leave class is added to the element | class="my-animation ng-leave" | - * | 3. $animate runs any JavaScript-defined animations on the element | class="my-animation ng-leave" | + * | 2. $animate runs any JavaScript-defined animations on the element | class="my-animation" | + * | 3. the .ng-leave class is added to the element | class="my-animation ng-leave" | * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-leave" | * | 5. the .ng-leave-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-leave ng-leave-active | * | 6. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-leave ng-leave-active | @@ -337,8 +337,8 @@ angular.module('ngAnimate', ['ng']) * |----------------------------------------------------------------------------------------------|---------------------------------------------| * | 1. $animate.move(...) is called | class="my-animation" | * | 2. element is moved into the parent element or beside the after element | class="my-animation" | - * | 3. the .ng-move class is added to the element | class="my-animation ng-move" | - * | 4. $animate runs any JavaScript-defined animations on the element | class="my-animation ng-move" | + * | 3. $animate runs any JavaScript-defined animations on the element | class="my-animation" | + * | 4. the .ng-move class is added to the element | class="my-animation ng-move" | * | 5. $animate scans the element styles to get the CSS transition/animation duration and delay | class="my-animation ng-move" | * | 6. the .ng-move-active class is added (this triggers the CSS transition/animation) | class="my-animation ng-move ng-move-active" | * | 7. $animate waits for X milliseconds for the animation to complete | class="my-animation ng-move ng-move-active" | @@ -373,8 +373,8 @@ angular.module('ngAnimate', ['ng']) * | Animation Step | What the element class attribute looks like | * |------------------------------------------------------------------------------------------------|---------------------------------------------| * | 1. $animate.addClass(element, 'super') is called | class="" | - * | 2. the .super-add class is added to the element | class="super-add" | - * | 3. $animate runs any JavaScript-defined animations on the element | class="super-add" | + * | 2. $animate runs any JavaScript-defined animations on the element | class="" | + * | 3. the .super-add class is added to the element | class="super-add" | * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="super-add" | * | 5. the .super-add-active class is added (this triggers the CSS transition/animation) | class="super-add super-add-active" | * | 6. $animate waits for X milliseconds for the animation to complete | class="super-add super-add-active" | @@ -408,8 +408,8 @@ angular.module('ngAnimate', ['ng']) * | Animation Step | What the element class attribute looks like | * |-----------------------------------------------------------------------------------------------|-------------------------------------------------| * | 1. $animate.removeClass(element, 'super') is called | class="super" | - * | 2. the .super-remove class is added to the element | class="super super-remove" | - * | 3. $animate runs any JavaScript-defined animations on the element | class="super super-remove" | + * | 2. $animate runs any JavaScript-defined animations on the element | class="super" | + * | 3. the .super-remove class is added to the element | class="super super-remove" | * | 4. $animate scans the element styles to get the CSS transition/animation duration and delay | class="super super-remove" | * | 5. the .super-remove-active class is added (this triggers the CSS transition/animation) | class="super super-remove super-remove-active" | * | 6. $animate waits for X milliseconds for the animation to complete | class="super super-remove super-remove-active" | @@ -494,15 +494,6 @@ angular.module('ngAnimate', ['ng']) done:done }); - var baseClassName = className; - if(event == 'addClass') { - className = suffixClasses(className, '-add'); - } else if(event == 'removeClass') { - className = suffixClasses(className, '-remove'); - } - - element.addClass(className); - forEach(animations, function(animation, index) { var fn = function() { progress(index); @@ -510,7 +501,7 @@ angular.module('ngAnimate', ['ng']) if(animation.start) { if(event == 'addClass' || event == 'removeClass') { - animation.endFn = animation.start(element, baseClassName, fn); + animation.endFn = animation.start(element, className, fn); } else { animation.endFn = animation.start(element, fn); } @@ -538,7 +529,6 @@ angular.module('ngAnimate', ['ng']) function done() { if(!done.hasBeenRun) { done.hasBeenRun = true; - element.removeClass(className); element.removeData(NG_ANIMATE_STATE); (onComplete || noop)(); } @@ -549,26 +539,45 @@ angular.module('ngAnimate', ['ng']) $animateProvider.register('', ['$window','$sniffer', '$timeout', function($window, $sniffer, $timeout) { var noop = angular.noop; var forEach = angular.forEach; + + //one day all browsers will have these properties + var w3cAnimationProp = 'animation'; + var w3cTransitionProp = 'transition'; + + //but some still use vendor-prefixed styles + var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation'; + var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition'; + + var durationKey = 'Duration', + delayKey = 'Delay', + animationIterationCountKey = 'IterationCount', + ELEMENT_NODE = 1; + function animate(element, className, done) { if (!($sniffer.transitions || $sniffer.animations)) { done(); return; } + else if(['ng-enter','ng-leave','ng-move'].indexOf(className) == -1) { + var existingDuration = 0; + forEach(element, function(element) { + if (element.nodeType == ELEMENT_NODE) { + var elementStyles = $window.getComputedStyle(element) || {}; + existingDuration = Math.max(parseMaxTime(elementStyles[w3cTransitionProp + durationKey]), + parseMaxTime(elementStyles[vendorTransitionProp + durationKey]), + existingDuration); + } + }); + if(existingDuration > 0) { + done(); + return; + } + } - //one day all browsers will have these properties - var w3cAnimationProp = 'animation'; - var w3cTransitionProp = 'transition'; - - //but some still use vendor-prefixed styles - var vendorAnimationProp = $sniffer.vendorPrefix + 'Animation'; - var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition'; - - var durationKey = 'Duration', - delayKey = 'Delay', - animationIterationCountKey = 'IterationCount'; + element.addClass(className); //we want all the styles defined before and after - var duration = 0, ELEMENT_NODE = 1; + var duration = 0; forEach(element, function(element) { if (element.nodeType == ELEMENT_NODE) { var elementStyles = $window.getComputedStyle(element) || {}; @@ -615,6 +624,7 @@ angular.module('ngAnimate', ['ng']) //there is no need to attach this internally to the //timeout done method return function onEnd(cancelled) { + element.removeClass(className); element.removeClass(activeClassName); //only when the animation is cancelled is the done() @@ -626,6 +636,7 @@ angular.module('ngAnimate', ['ng']) } } else { + element.removeClass(className); done(); } @@ -656,16 +667,15 @@ angular.module('ngAnimate', ['ng']) } }; + function suffixClasses(classes, suffix) { + var className = ''; + classes = angular.isArray(classes) ? classes : classes.split(/\s+/); + forEach(classes, function(klass, i) { + if(klass && klass.length > 0) { + className += (i > 0 ? ' ' : '') + klass + suffix; + } + }); + return className; + } }]); - - function suffixClasses(classes, suffix) { - var className = ''; - classes = angular.isArray(classes) ? classes : classes.split(/\s+/); - forEach(classes, function(klass, i) { - if(klass && klass.length > 0) { - className += (i > 0 ? ' ' : '') + klass + suffix; - } - }); - return className; - } }]); diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 2d9d25afc688..75029889ae2c 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -1584,4 +1584,33 @@ describe("ngAnimate", function() { }); }); + it("should skip ngAnimate animations when any pre-existing CSS transitions are present on the element", function() { + inject(function($compile, $rootScope, $animate, $timeout, $sniffer) { + if(!$sniffer.transitions) return; + + var element = html($compile('
')($rootScope)); + var child = html($compile('
')($rootScope)); + + ss.addRule('.animated', 'transition:1s linear all;' + + vendorPrefix + 'transition:1s linear all'); + ss.addRule('.super-add', 'transition:2s linear all;' + + vendorPrefix + 'transition:2s linear all'); + + $rootElement.append(element); + jqLite(document.body).append($rootElement); + + $animate.addClass(element, 'super'); + $timeout.flush(0); + + var empty = true; + try { + $timeout.flush(); + empty = false; + } + catch(e) {} + + expect(empty).toBe(true); + }); + }); + });