Skip to content

Commit

Permalink
feat(ngMock): add support for $animate.closeAndFlush()
Browse files Browse the repository at this point in the history
Use `$animate.closeAndFlush()` to close all running animations.

Includes a fix that landed separately in the master branch:
a801df7
  • Loading branch information
matsko authored and Narretz committed Jan 12, 2016
1 parent 0a641c0 commit 29891ba
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 13 deletions.
2 changes: 2 additions & 0 deletions src/AngularPublic.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
$AnchorScrollProvider,
$AnimateProvider,
$CoreAnimateCssProvider,
$$CoreAnimateJsProvider,
$$CoreAnimateQueueProvider,
$$AnimateRunnerFactoryProvider,
$$AnimateAsyncRunFactoryProvider,
Expand Down Expand Up @@ -217,6 +218,7 @@ function publishExternalAPI(angular) {
$anchorScroll: $AnchorScrollProvider,
$animate: $AnimateProvider,
$animateCss: $CoreAnimateCssProvider,
$$animateJs: $$CoreAnimateJsProvider,
$$animateQueue: $$CoreAnimateQueueProvider,
$$AnimateRunner: $$AnimateRunnerFactoryProvider,
$$animateAsyncRun: $$AnimateAsyncRunFactoryProvider,
Expand Down
4 changes: 4 additions & 0 deletions src/ng/animate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
6 changes: 3 additions & 3 deletions src/ngAnimate/animateCss.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -456,7 +456,7 @@ var $AnimateCssProvider = ['$animateProvider', function($animateProvider) {
var node = getDomNode(element);
if (!node
|| !node.parentNode
|| !$animate.enabled()) {
|| !$$animateQueue.enabled()) {
return closeAndReturnNoopAnimator();
}

Expand Down
33 changes: 28 additions & 5 deletions src/ngAnimate/animateJs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 = [];

Expand All @@ -89,8 +115,7 @@ var $$AnimateJsProvider = ['$animateProvider', function($animateProvider) {
});
}

var animationClosed = false;
var runner = new $$AnimateRunner({
runner.setHost({
end: function() {
endAnimations();
},
Expand All @@ -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);
}

Expand Down
86 changes: 82 additions & 4 deletions src/ngMock/angular-mocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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: [],
Expand All @@ -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;
Expand All @@ -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');
}

Expand Down
84 changes: 83 additions & 1 deletion test/ngMock/angular-mocksSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -1847,6 +1847,8 @@ describe('ngMockE2E', function() {

beforeEach(module(function($animateProvider) {
trackedAnimations = [];
animationLog = [];

$animateProvider.register('.animate', function() {
return {
leave: logFn('leave'),
Expand All @@ -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);
};
};
}

Expand Down Expand Up @@ -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('<div></div>');
$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');

}));
});
});
});

Expand Down

0 comments on commit 29891ba

Please sign in to comment.