From 512c08118786a419fabbd063fa17d224aba125cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matias=20Niemel=C3=A4?= Date: Thu, 17 Dec 2015 13:25:33 -0800 Subject: [PATCH] feat(ngMock): add support for `$animate.closeAndFlush()` Use `$animate.closeAndFlush()` to close all running animations. Includes a fix that landed separately in the master branch: a801df719ea8b5996676d4e7a88a26a5ece471e7 --- src/AngularPublic.js | 2 + src/ng/animate.js | 4 ++ src/ngAnimate/animateCss.js | 6 +-- src/ngAnimate/animateJs.js | 33 ++++++++++-- src/ngMock/angular-mocks.js | 86 ++++++++++++++++++++++++++++++-- test/ngMock/angular-mocksSpec.js | 84 ++++++++++++++++++++++++++++++- 6 files changed, 202 insertions(+), 13 deletions(-) diff --git a/src/AngularPublic.js b/src/AngularPublic.js index b5be4e53acdf..70ca29511405 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -56,6 +56,7 @@ $AnchorScrollProvider, $AnimateProvider, $CoreAnimateCssProvider, + $$CoreAnimateJsProvider, $$CoreAnimateQueueProvider, $$AnimateRunnerFactoryProvider, $$AnimateAsyncRunFactoryProvider, @@ -217,6 +218,7 @@ function publishExternalAPI(angular) { $anchorScroll: $AnchorScrollProvider, $animate: $AnimateProvider, $animateCss: $CoreAnimateCssProvider, + $$animateJs: $$CoreAnimateJsProvider, $$animateQueue: $$CoreAnimateQueueProvider, $$AnimateRunner: $$AnimateRunnerFactoryProvider, $$animateAsyncRun: $$AnimateAsyncRunFactoryProvider, diff --git a/src/ng/animate.js b/src/ng/animate.js index 6df23c4179ec..ec1a509774cd 100644 --- a/src/ng/animate.js +++ b/src/ng/animate.js @@ -53,6 +53,10 @@ function prepareAnimateOptions(options) { : {}; } +var $$CoreAnimateJsProvider = function() { + this.$get = function() {}; +}; + // this is prefixed with Core since it conflicts with // the animateQueueProvider defined in ngAnimate/animateQueue.js var $$CoreAnimateQueueProvider = function() { diff --git a/src/ngAnimate/animateCss.js b/src/ngAnimate/animateCss.js index 8bb6bc395956..c2df5b16e3a6 100644 --- a/src/ngAnimate/animateCss.js +++ b/src/ngAnimate/animateCss.js @@ -352,9 +352,9 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { var gcsStaggerLookup = createLocalCacheLookup(); this.$get = ['$window', '$$jqLite', '$$AnimateRunner', '$timeout', - '$$forceReflow', '$sniffer', '$$rAFScheduler', '$animate', + '$$forceReflow', '$sniffer', '$$rAFScheduler', '$$animateQueue', function($window, $$jqLite, $$AnimateRunner, $timeout, - $$forceReflow, $sniffer, $$rAFScheduler, $animate) { + $$forceReflow, $sniffer, $$rAFScheduler, $$animateQueue) { var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); @@ -456,7 +456,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) { var node = getDomNode(element); if (!node || !node.parentNode - || !$animate.enabled()) { + || !$$animateQueue.enabled()) { return closeAndReturnNoopAnimator(); } diff --git a/src/ngAnimate/animateJs.js b/src/ngAnimate/animateJs.js index ef356cb2a30e..d8ec683b5f40 100644 --- a/src/ngAnimate/animateJs.js +++ b/src/ngAnimate/animateJs.js @@ -11,6 +11,8 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { var applyAnimationClasses = applyAnimationClassesFactory($$jqLite); // $animateJs(element, 'enter'); return function(element, event, classes, options) { + var animationClosed = false; + // the `classes` argument is optional and if it is not used // then the classes will be resolved from the element's className // property as well as options.addClass/options.removeClass. @@ -63,8 +65,32 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { applyAnimationClasses(element, options); } + function close() { + animationClosed = true; + applyOptions(); + applyAnimationStyles(element, options); + } + + var runner; + return { + $$willAnimate: true, + end: function() { + if (runner) { + runner.end(); + } else { + close(); + runner = new $$AnimateRunner(); + runner.complete(true); + } + return runner; + }, start: function() { + if (runner) { + return runner; + } + + runner = new $$AnimateRunner(); var closeActiveAnimations; var chain = []; @@ -89,8 +115,7 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { }); } - var animationClosed = false; - var runner = new $$AnimateRunner({ + runner.setHost({ end: function() { endAnimations(); }, @@ -103,9 +128,7 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) { return runner; function onComplete(success) { - animationClosed = true; - applyOptions(); - applyAnimationStyles(element, options); + close(success); runner.complete(success); } diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index ea1879c43874..9964b11ae559 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -751,6 +751,15 @@ angular.mock.TzDate = function(offset, timestamp) { angular.mock.TzDate.prototype = Date.prototype; /* jshint +W101 */ + +/** + * @ngdoc service + * @name $animate + * + * @description + * Mock implementation of the {@link ng.$animate `$animate`} service. Exposes two additional methods + * for testing animations. + */ angular.mock.animate = angular.module('ngAnimateMock', ['ng']) .config(['$provide', function($provide) { @@ -783,9 +792,50 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng']) return queueFn; }); - $provide.decorator('$animate', ['$delegate', '$timeout', '$browser', '$$rAF', + $provide.decorator('$$animateJs', ['$delegate', function($delegate) { + var runners = []; + + var animateJsConstructor = function() { + var animator = $delegate.apply($delegate, arguments); + // If no javascript animation is found, animator is undefined + if (animator) { + runners.push(animator); + } + return animator; + }; + + animateJsConstructor.$closeAndFlush = function() { + runners.forEach(function(runner) { + runner.end(); + }); + runners = []; + }; + + return animateJsConstructor; + }]); + + $provide.decorator('$animateCss', ['$delegate', function($delegate) { + var runners = []; + + var animateCssConstructor = function(element, options) { + var animator = $delegate(element, options); + runners.push(animator); + return animator; + }; + + animateCssConstructor.$closeAndFlush = function() { + runners.forEach(function(runner) { + runner.end(); + }); + runners = []; + }; + + return animateCssConstructor; + }]); + + $provide.decorator('$animate', ['$delegate', '$timeout', '$browser', '$$rAF', '$animateCss', '$$animateJs', '$$forceReflow', '$$animateAsyncRun', '$rootScope', - function($delegate, $timeout, $browser, $$rAF, + function($delegate, $timeout, $browser, $$rAF, $animateCss, $$animateJs, $$forceReflow, $$animateAsyncRun, $rootScope) { var animate = { queue: [], @@ -797,7 +847,35 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng']) return $$forceReflow.totalReflows; }, enabled: $delegate.enabled, - flush: function() { + /** + * @ngdoc method + * @name $animate#closeAndFlush + * @description + * + * This method will close all pending animations (both {@link ngAnimate#javascript-based-animations Javascript} + * and {@link ngAnimate.$animateCss CSS}) and it will also flush any remaining animation frames and/or callbacks. + */ + closeAndFlush: function() { + // we allow the flush command to swallow the errors + // because depending on whether CSS or JS animations are + // used, there may not be a RAF flush. The primary flush + // at the end of this function must throw an exception + // because it will track if there were pending animations + this.flush(true); + $animateCss.$closeAndFlush(); + $$animateJs.$closeAndFlush(); + this.flush(); + }, + /** + * @ngdoc method + * @name $animate#flush + * @description + * + * This method is used to flush the pending callbacks and animation frames to either start + * an animation or conclude an animation. Note that this will not actually close an + * actively running animation (see {@link ngMock.$animate#closeAndFlush `closeAndFlush()`} for that). + */ + flush: function(hideErrors) { $rootScope.$digest(); var doNextRun, somethingFlushed = false; @@ -814,7 +892,7 @@ angular.mock.animate = angular.module('ngAnimateMock', ['ng']) } } while (doNextRun); - if (!somethingFlushed) { + if (!somethingFlushed && !hideErrors) { throw new Error('No pending animations ready to be closed or flushed'); } diff --git a/test/ngMock/angular-mocksSpec.js b/test/ngMock/angular-mocksSpec.js index 05b9b8381088..6990bdadd4ce 100644 --- a/test/ngMock/angular-mocksSpec.js +++ b/test/ngMock/angular-mocksSpec.js @@ -1834,7 +1834,7 @@ describe('ngMockE2E', function() { beforeEach(module('ngAnimate')); beforeEach(module('ngAnimateMock')); - var ss, element, trackedAnimations; + var ss, element, trackedAnimations, animationLog; afterEach(function() { if (element) { @@ -1847,6 +1847,8 @@ describe('ngMockE2E', function() { beforeEach(module(function($animateProvider) { trackedAnimations = []; + animationLog = []; + $animateProvider.register('.animate', function() { return { leave: logFn('leave'), @@ -1855,7 +1857,13 @@ describe('ngMockE2E', function() { function logFn(method) { return function(element) { + animationLog.push('start ' + method); trackedAnimations.push(getDoneCallback(arguments)); + + return function closingFn(cancel) { + var lab = cancel ? 'cancel' : 'end'; + animationLog.push(lab + ' ' + method); + }; }; } @@ -2008,6 +2016,80 @@ describe('ngMockE2E', function() { expect(spy.callCount).toBe(2); })); }); + + describe('$animate.closeAndFlush()', function() { + it('should close the currently running $animateCss animations', + inject(function($animateCss, $animate) { + + if (!browserSupportsCssAnimations()) return; + + var spy = jasmine.createSpy(); + var runner = $animateCss(element, { + duration: 1, + to: { color: 'red' } + }).start(); + + runner.then(spy); + + expect(spy).not.toHaveBeenCalled(); + $animate.closeAndFlush(); + expect(spy).toHaveBeenCalled(); + })); + + it('should close the currently running $$animateJs animations', + inject(function($$animateJs, $animate) { + + var spy = jasmine.createSpy(); + var runner = $$animateJs(element, 'leave', 'animate', {}).start(); + runner.then(spy); + + expect(spy).not.toHaveBeenCalled(); + $animate.closeAndFlush(); + expect(spy).toHaveBeenCalled(); + })); + + it('should run the closing javascript animation function upon flush', + inject(function($$animateJs, $animate) { + + $$animateJs(element, 'leave', 'animate', {}).start(); + + expect(animationLog).toEqual(['start leave']); + $animate.closeAndFlush(); + expect(animationLog).toEqual(['start leave', 'end leave']); + })); + + it('should not throw when a regular animation has no javascript animation', + inject(function($animate, $$animation, $rootElement) { + + if (!browserSupportsCssAnimations()) return; + + var element = jqLite('
'); + $rootElement.append(element); + + // Make sure the animation has valid $animateCss options + $$animation(element, null, { + from: { background: 'red' }, + to: { background: 'blue' }, + duration: 1, + transitionStyle: '1s linear all' + }); + + expect(function() { + $animate.closeAndFlush(); + }).not.toThrow(); + + dealoc(element); + })); + + it('should throw an error if there are no animations to close and flush', + inject(function($animate) { + + expect(function() { + $animate.closeAndFlush(); + }).toThrow('No pending animations ready to be closed or flushed'); + + })); + }); }); });