From 570463a465fae02efc33e5a1fa963437cdc275dd Mon Sep 17 00:00:00 2001 From: Misko Hevery Date: Thu, 11 Apr 2013 22:39:15 -0700 Subject: [PATCH] fix(ngAnimate): prevent animation on initial page load --- src/ng/animator.js | 400 ++++++++++++++-------------- test/ng/animatorSpec.js | 44 ++- test/ng/directive/ngIncludeSpec.js | 3 +- test/ng/directive/ngRepeatSpec.js | 3 +- test/ng/directive/ngShowHideSpec.js | 23 +- test/ng/directive/ngSwitchSpec.js | 3 +- test/ng/directive/ngViewSpec.js | 10 +- test/testabilityPatch.js | 4 +- 8 files changed, 262 insertions(+), 228 deletions(-) diff --git a/src/ng/animator.js b/src/ng/animator.js index 957adaed0596..7f0f7b7a8d1b 100644 --- a/src/ng/animator.js +++ b/src/ng/animator.js @@ -40,9 +40,7 @@ * * The `event1` and `event2` attributes refer to the animation events specific to the directive that has been assigned. * - * Keep in mind that, by default, **all** initial animations will be skipped until the first digest cycle has fully - * passed. This helps prevent any unexpected animations from occurring while the application or directive is initializing. To - * override this behavior, you may pass "animateFirst: true" into the ngAnimate attribute expression. + * Keep in mind that if an animation is running, no child element of such animation can also be animated. * *

CSS-defined Animations

* By default, ngAnimate attaches two CSS3 classes per animation event to the DOM element to achieve the animation. @@ -126,217 +124,211 @@ */ var $AnimatorProvider = function() { - var globalAnimationEnabled = true; + var NG_ANIMATE_CONTROLLER = '$ngAnimateController'; + var rootAnimateController = {running:true}; - this.$get = ['$animation', '$window', '$sniffer', '$rootScope', function($animation, $window, $sniffer, $rootScope) { - /** - * @ngdoc function - * @name ng.$animator - * @function - * - * @description - * The $animator.create service provides the DOM manipulation API which is decorated with animations. - * - * @param {Scope} scope the scope for the ng-animate. - * @param {Attributes} attr the attributes object which contains the ngAnimate key / value pair. (The attributes are - * passed into the linking function of the directive using the `$animator`.) - * @return {object} the animator object which contains the enter, leave, move, show, hide and animate methods. - */ - var AnimatorService = function(scope, attrs) { - var ngAnimateAttr = attrs.ngAnimate; - - // avoid running animations on start - var animationEnabled = false; - var ngAnimateValue = ngAnimateAttr && scope.$eval(ngAnimateAttr); - - if (!animationEnabled) { - if(isObject(ngAnimateValue) && ngAnimateValue['animateFirst']) { - animationEnabled = true; - } else { - var enableSubsequent = function() { - removeWatch(); - scope.$evalAsync(function() { - animationEnabled = true; - }); - }; - var removeWatch = noop; - - if (scope.$$phase) { - enableSubsequent(); - } else { - removeWatch = scope.$watch(enableSubsequent); - } - } + this.$get = ['$animation', '$window', '$sniffer', '$rootElement', '$rootScope', + function($animation, $window, $sniffer, $rootElement, $rootScope) { + $rootElement.data(NG_ANIMATE_CONTROLLER, rootAnimateController); + var unregister = $rootScope.$watch(function() { + unregister(); + if (rootAnimateController.running) { + $window.setTimeout(function() { + rootAnimateController.running = false; + }, 0); } - var animator = {}; - - /** - * @ngdoc function - * @name ng.animator#enter - * @methodOf ng.$animator - * @function - * - * @description - * Injects the element object into the DOM (inside of the parent element) and then runs the enter animation. - * - * @param {jQuery/jqLite element} element the element that will be the focus of the enter animation - * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the enter animation - * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the enter animation - */ - animator.enter = animateActionFactory('enter', insert, noop); - - /** - * @ngdoc function - * @name ng.animator#leave - * @methodOf ng.$animator - * @function - * - * @description - * Runs the leave animation operation and, upon completion, removes the element from the DOM. - * - * @param {jQuery/jqLite element} element the element that will be the focus of the leave animation - * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the leave animation - */ - animator.leave = animateActionFactory('leave', noop, remove); - - /** - * @ngdoc function - * @name ng.animator#move - * @methodOf ng.$animator - * @function - * - * @description - * Fires the move DOM operation. Just before the animation starts, the animator will either append it into the parent container or - * add the element directly after the after element if present. Then the move animation will be run. - * - * @param {jQuery/jqLite element} element the element that will be the focus of the move animation - * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the move animation - * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the move animation - */ - animator.move = animateActionFactory('move', move, noop); - - /** - * @ngdoc function - * @name ng.animator#show - * @methodOf ng.$animator - * @function - * - * @description - * Reveals the element by setting the CSS property `display` to `block` and then starts the show animation directly after. - * - * @param {jQuery/jqLite element} element the element that will be rendered visible or hidden - */ - animator.show = animateActionFactory('show', show, noop); - - /** - * @ngdoc function - * @name ng.animator#hide - * @methodOf ng.$animator - * - * @description - * Starts the hide animation first and sets the CSS `display` property to `none` upon completion. - * - * @param {jQuery/jqLite element} element the element that will be rendered visible or hidden - */ - animator.hide = animateActionFactory('hide', noop, hide); - return animator; - - function animateActionFactory(type, beforeFn, afterFn) { - var className = ngAnimateAttr - ? isObject(ngAnimateValue) ? ngAnimateValue[type] : ngAnimateValue + '-' + type - : ''; - var animationPolyfill = $animation(className); - - var polyfillSetup = animationPolyfill && animationPolyfill.setup; - var polyfillStart = animationPolyfill && animationPolyfill.start; + }); - if (!className) { - return function(element, parent, after) { - beforeFn(element, parent, after); - afterFn(element, parent, after); - } - } else { - var setupClass = className + '-setup'; - var startClass = className + '-start'; - - return function(element, parent, after) { - if (!animationEnabled || !globalAnimationEnabled || - (!$sniffer.supportsTransitions && !polyfillSetup && !polyfillStart)) { + /** + * @ngdoc function + * @name ng.$animator + * @function + * + * @description + * The $animator.create service provides the DOM manipulation API which is decorated with animations. + * + * @param {Scope} scope the scope for the ng-animate. + * @param {Attributes} attr the attributes object which contains the ngAnimate key / value pair. (The attributes are + * passed into the linking function of the directive using the `$animator`.) + * @return {object} the animator object which contains the enter, leave, move, show, hide and animate methods. + */ + var AnimatorService = function(scope, attrs) { + var ngAnimateAttr = attrs.ngAnimate; + var ngAnimateValue = ngAnimateAttr && scope.$eval(ngAnimateAttr); + var animator = {}; + + /** + * @ngdoc function + * @name ng.animator#enter + * @methodOf ng.$animator + * @function + * + * @description + * Injects the element object into the DOM (inside of the parent element) and then runs the enter animation. + * + * @param {jQuery/jqLite element} element the element that will be the focus of the enter animation + * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the enter animation + * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the enter animation + */ + animator.enter = animateActionFactory('enter', insert, noop); + + /** + * @ngdoc function + * @name ng.animator#leave + * @methodOf ng.$animator + * @function + * + * @description + * Runs the leave animation operation and, upon completion, removes the element from the DOM. + * + * @param {jQuery/jqLite element} element the element that will be the focus of the leave animation + * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the leave animation + */ + animator.leave = animateActionFactory('leave', noop, remove); + + /** + * @ngdoc function + * @name ng.animator#move + * @methodOf ng.$animator + * @function + * + * @description + * Fires the move DOM operation. Just before the animation starts, the animator will either append it into the parent container or + * add the element directly after the after element if present. Then the move animation will be run. + * + * @param {jQuery/jqLite element} element the element that will be the focus of the move animation + * @param {jQuery/jqLite element} parent the parent element of the element that will be the focus of the move animation + * @param {jQuery/jqLite element} after the sibling element (which is the previous element) of the element that will be the focus of the move animation + */ + animator.move = animateActionFactory('move', move, noop); + + /** + * @ngdoc function + * @name ng.animator#show + * @methodOf ng.$animator + * @function + * + * @description + * Reveals the element by setting the CSS property `display` to `block` and then starts the show animation directly after. + * + * @param {jQuery/jqLite element} element the element that will be rendered visible or hidden + */ + animator.show = animateActionFactory('show', show, noop); + + /** + * @ngdoc function + * @name ng.animator#hide + * @methodOf ng.$animator + * + * @description + * Starts the hide animation first and sets the CSS `display` property to `none` upon completion. + * + * @param {jQuery/jqLite element} element the element that will be rendered visible or hidden + */ + animator.hide = animateActionFactory('hide', noop, hide); + return animator; + + function animateActionFactory(type, beforeFn, afterFn) { + var className = ngAnimateAttr + ? isObject(ngAnimateValue) ? ngAnimateValue[type] : ngAnimateValue + '-' + type + : ''; + var animationPolyfill = $animation(className); + + var polyfillSetup = animationPolyfill && animationPolyfill.setup; + var polyfillStart = animationPolyfill && animationPolyfill.start; + + if (!className) { + return function(element, parent, after) { beforeFn(element, parent, after); afterFn(element, parent, after); - return; } - - element.addClass(setupClass); - beforeFn(element, parent, after); - if (element.length == 0) return done(); - - var memento = (polyfillSetup || noop)(element); - - // $window.setTimeout(beginAnimation, 0); this was causing the element not to animate - // keep at 1 for animation dom rerender - $window.setTimeout(beginAnimation, 1); - - function beginAnimation() { - element.addClass(startClass); - if (polyfillStart) { - polyfillStart(element, done, memento); - } else if (isFunction($window.getComputedStyle)) { - var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition'; - var w3cTransitionProp = 'transition'; //one day all browsers will have this - - var durationKey = 'Duration'; - var duration = 0; - //we want all the styles defined before and after - forEach(element, function(element) { - var globalStyles = $window.getComputedStyle(element) || {}; - duration = Math.max( - parseFloat(globalStyles[w3cTransitionProp + durationKey]) || - parseFloat(globalStyles[vendorTransitionProp + durationKey]) || - 0, - duration); - }); - $window.setTimeout(done, duration * 1000); - } else { - done(); + } else { + var setupClass = className + '-setup'; + var startClass = className + '-start'; + + return function(element, parent, after) { + if (!parent) { + parent = after ? after.parent() : element.parent(); + } + if ((!$sniffer.supportsTransitions && !polyfillSetup && !polyfillStart) || + (parent.inheritedData(NG_ANIMATE_CONTROLLER) || noop).running) { + beforeFn(element, parent, after); + afterFn(element, parent, after); + return; } - } - function done() { - afterFn(element, parent, after); - element.removeClass(setupClass); - element.removeClass(startClass); + element.data(NG_ANIMATE_CONTROLLER, {running:true}); + element.addClass(setupClass); + beforeFn(element, parent, after); + if (element.length == 0) return done(); + + var memento = (polyfillSetup || noop)(element); + + // $window.setTimeout(beginAnimation, 0); this was causing the element not to animate + // keep at 1 for animation dom rerender + $window.setTimeout(beginAnimation, 1); + + function beginAnimation() { + element.addClass(startClass); + if (polyfillStart) { + polyfillStart(element, done, memento); + } else if (isFunction($window.getComputedStyle)) { + var vendorTransitionProp = $sniffer.vendorPrefix + 'Transition'; + var w3cTransitionProp = 'transition'; //one day all browsers will have this + + var durationKey = 'Duration'; + var duration = 0; + //we want all the styles defined before and after + forEach(element, function(element) { + var globalStyles = $window.getComputedStyle(element) || {}; + duration = Math.max( + parseFloat(globalStyles[w3cTransitionProp + durationKey]) || + parseFloat(globalStyles[vendorTransitionProp + durationKey]) || + 0, + duration); + }); + $window.setTimeout(done, duration * 1000); + } else { + done(); + } + } + + function done() { + afterFn(element, parent, after); + element.removeClass(setupClass); + element.removeClass(startClass); + element.removeData(NG_ANIMATE_CONTROLLER); + } } } } - } - - function show(element) { - element.css('display', ''); - } - - function hide(element) { - element.css('display', 'none'); - } - - function insert(element, parent, after) { - if (after) { - after.after(element); - } else { - parent.append(element); + + function show(element) { + element.css('display', ''); } - } - - function remove(element) { - element.remove(); - } - - function move(element, parent, after) { - // Do not remove element before insert. Removing will cause data associated with the - // element to be dropped. Insert will implicitly do the remove. - insert(element, parent, after); - } - }; + + function hide(element) { + element.css('display', 'none'); + } + + function insert(element, parent, after) { + if (after) { + after.after(element); + } else { + parent.append(element); + } + } + + function remove(element) { + element.remove(); + } + + function move(element, parent, after) { + // Do not remove element before insert. Removing will cause data associated with the + // element to be dropped. Insert will implicitly do the remove. + insert(element, parent, after); + } + }; /** * @ngdoc function @@ -353,9 +345,9 @@ var $AnimatorProvider = function() { */ AnimatorService.enabled = function(value) { if (arguments.length) { - globalAnimationEnabled = !!value; + rootAnimateController.running = !value; } - return globalAnimationEnabled; + return !rootAnimateController.running; }; return AnimatorService; diff --git a/test/ng/animatorSpec.js b/test/ng/animatorSpec.js index 7cf785e0bc52..4d549ee393ca 100644 --- a/test/ng/animatorSpec.js +++ b/test/ng/animatorSpec.js @@ -2,11 +2,12 @@ describe("$animator", function() { - var body, element; + var body, element, $rootElement; function html(html) { - body.html(html); - element = body.children().eq(0); + body.append($rootElement); + $rootElement.html(html); + element = $rootElement.children().eq(0); return element; } @@ -21,7 +22,18 @@ describe("$animator", function() { describe("enable / disable", function() { - it("should disable and enable the animations", inject(function($animator) { + beforeEach(function() { + module(function($animationProvider, $provide) { + $provide.value('$window', angular.mock.createMockWindow()); + }); + }); + + it("should disable and enable the animations", inject(function($animator, $rootScope, $window) { + expect($animator.enabled()).toBe(false); + + $rootScope.$digest(); + $window.setTimeout.expect(0).process(); + expect($animator.enabled()).toBe(true); expect($animator.enabled(0)).toBe(false); @@ -40,9 +52,10 @@ describe("$animator", function() { module(function($animationProvider, $provide) { $provide.value('$window', window = angular.mock.createMockWindow()); }) - inject(function($animator, $compile, $rootScope) { + inject(function($animator, $compile, $rootScope, _$rootElement_) { animator = $animator($rootScope, {}); element = $compile('
')($rootScope); + $rootElement = _$rootElement_; }) }); @@ -131,7 +144,10 @@ describe("$animator", function() { animator = $animator($rootScope, { ngAnimate : '{enter: \'custom\'}' }); + $rootScope.$digest(); // re-enable the animations; + window.setTimeout.expect(0).process(); + expect(element.contents().length).toBe(0); animator.enter(child, element); window.setTimeout.expect(1).process(); @@ -141,7 +157,10 @@ describe("$animator", function() { animator = $animator($rootScope, { ngAnimate : '{leave: \'custom\'}' }); - $rootScope.$digest(); + + $rootScope.$digest(); // re-enable the animations; + window.setTimeout.expect(0).process(); + element.append(child); expect(element.contents().length).toBe(1); animator.leave(child, element); @@ -150,6 +169,7 @@ describe("$animator", function() { })); it("should animate the move animation event", inject(function($animator, $compile, $rootScope) { + $animator.enabled(true); animator = $animator($rootScope, { ngAnimate : '{move: \'custom\'}' }); @@ -165,6 +185,7 @@ describe("$animator", function() { })); it("should animate the show animation event", inject(function($animator, $rootScope) { + $animator.enabled(true); animator = $animator($rootScope, { ngAnimate : '{show: \'custom\'}' }); @@ -178,6 +199,7 @@ describe("$animator", function() { })); it("should animate the hide animation event", inject(function($animator, $rootScope) { + $animator.enabled(true); animator = $animator($rootScope, { ngAnimate : '{hide: \'custom\'}' }); @@ -192,6 +214,7 @@ describe("$animator", function() { it("should assign the ngAnimate string to all events if a string is given", inject(function($animator, $sniffer, $rootScope) { + $animator.enabled(true); if (!$sniffer.supportsTransitions) return; animator = $animator($rootScope, { ngAnimate : '"custom"' @@ -237,6 +260,7 @@ describe("$animator", function() { })); it("should run polyfillSetup and return the memento", inject(function($animator, $rootScope) { + $animator.enabled(true); animator = $animator($rootScope, { ngAnimate : '{show: \'setup-memo\'}' }); @@ -248,6 +272,8 @@ describe("$animator", function() { })); it("should not run if animations are disabled", inject(function($animator, $rootScope) { + $animator.enabled(true); + $rootScope.$digest(); // clear initial animation suppression $animator.enabled(false); animator = $animator($rootScope, { @@ -274,8 +300,10 @@ describe("$animator", function() { beforeEach(function() { module(function($animationProvider, $provide) { $provide.value('$window', window = angular.mock.createMockWindow()); - return function($sniffer) { + return function($sniffer, _$rootElement_, $animator) { vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; + $rootElement = _$rootElement_; + $animator.enabled(true); }; }) }); @@ -288,8 +316,6 @@ describe("$animator", function() { ngAnimate : '{show: \'inline-show\'}' }); - $rootScope.$digest(); // skip no-animate on first digest. - element.css('display','none'); expect(element.css('display')).toBe('none'); animator.show(element); diff --git a/test/ng/directive/ngIncludeSpec.js b/test/ng/directive/ngIncludeSpec.js index 4798baccf55e..9b5319f1e75c 100644 --- a/test/ng/directive/ngIncludeSpec.js +++ b/test/ng/directive/ngIncludeSpec.js @@ -303,8 +303,9 @@ describe('ngInclude ngAnimate', function() { beforeEach(module(function($animationProvider, $provide) { $provide.value('$window', window = angular.mock.createMockWindow()); - return function($sniffer) { + return function($sniffer, $animator) { vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; + $animator.enabled(true); }; })); diff --git a/test/ng/directive/ngRepeatSpec.js b/test/ng/directive/ngRepeatSpec.js index 7376b6704720..4372f57ab424 100644 --- a/test/ng/directive/ngRepeatSpec.js +++ b/test/ng/directive/ngRepeatSpec.js @@ -554,8 +554,9 @@ describe('ngRepeat ngAnimate', function() { beforeEach(module(function($animationProvider, $provide) { $provide.value('$window', window = angular.mock.createMockWindow()); - return function($sniffer) { + return function($sniffer, $animator) { vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; + $animator.enabled(true); }; })); diff --git a/test/ng/directive/ngShowHideSpec.js b/test/ng/directive/ngShowHideSpec.js index 17c47255b171..ecc5b2dce0a5 100644 --- a/test/ng/directive/ngShowHideSpec.js +++ b/test/ng/directive/ngShowHideSpec.js @@ -45,11 +45,12 @@ describe('ngShow / ngHide', function() { describe('ngShow / ngHide - ngAnimate', function() { var window; var vendorPrefix; - var body, element; + var body, element, $rootElement; function html(html) { - body.html(html); - element = body.children().eq(0); + body.append($rootElement); + $rootElement.html(html); + element = $rootElement.children().eq(0); return element; } @@ -61,12 +62,15 @@ describe('ngShow / ngHide - ngAnimate', function() { afterEach(function(){ dealoc(body); dealoc(element); + body.removeAttr('ng-animation-running'); }); beforeEach(module(function($animationProvider, $provide) { $provide.value('$window', window = angular.mock.createMockWindow()); - return function($sniffer) { + return function($sniffer, _$rootElement_, $animator) { vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; + $rootElement = _$rootElement_; + $animator.enabled(true); }; })); @@ -111,11 +115,14 @@ describe('ngShow / ngHide - ngAnimate', function() { expect(element.attr('class')).not.toContain('custom-hide-setup'); })); - it('should skip the initial show state on the first digest', function() { + it('should skip animation if parent animation running', function() { var fired = false; - inject(function($compile, $rootScope, $sniffer) { + inject(function($animator, $compile, $rootScope, $sniffer) { + $animator.enabled(true); + $rootScope.$digest(); $rootScope.val = true; var element = $compile(html('
123
'))($rootScope); + $rootElement.controller('ngAnimate').running = true; element.css('display','none'); expect(element.css('display')).toBe('none'); @@ -123,6 +130,7 @@ describe('ngShow / ngHide - ngAnimate', function() { expect(element[0].style.display).toBe(''); expect(fired).toBe(false); + $rootElement.controller('ngAnimate').running = false; $rootScope.val = false; $rootScope.$digest(); if ($sniffer.supportsTransitions) { @@ -178,7 +186,7 @@ describe('ngShow / ngHide - ngAnimate', function() { expect(element.attr('class')).not.toContain('custom-show-setup'); })); - it('should skip the initial hide state on the first digest', function() { + it('should disable animation when parent animation is running', function() { var fired = false; module(function($animationProvider) { $animationProvider.register('destructive-animation', function() { @@ -193,6 +201,7 @@ describe('ngShow / ngHide - ngAnimate', function() { inject(function($compile, $rootScope) { $rootScope.val = false; var element = $compile(html('
123
'))($rootScope); + $rootElement.controller('ngAnimate').running = true; element.css('display','block'); expect(element.css('display')).toBe('block'); diff --git a/test/ng/directive/ngSwitchSpec.js b/test/ng/directive/ngSwitchSpec.js index 5f0a2bb3427c..ee51e194941b 100644 --- a/test/ng/directive/ngSwitchSpec.js +++ b/test/ng/directive/ngSwitchSpec.js @@ -236,8 +236,9 @@ describe('ngSwitch ngAnimate', function() { beforeEach(module(function($animationProvider, $provide) { $provide.value('$window', window = angular.mock.createMockWindow()); - return function($sniffer) { + return function($sniffer, $animator) { vendorPrefix = '-' + $sniffer.vendorPrefix + '-'; + $animator.enabled(true); }; })); diff --git a/test/ng/directive/ngViewSpec.js b/test/ng/directive/ngViewSpec.js index c0348568983a..e9d531104309 100644 --- a/test/ng/directive/ngViewSpec.js +++ b/test/ng/directive/ngViewSpec.js @@ -4,8 +4,9 @@ describe('ngView', function() { var element; beforeEach(module(function() { - return function($rootScope, $compile) { + return function($rootScope, $compile, $animator) { element = $compile('')($rootScope); + $animator.enabled(true); }; })); @@ -510,8 +511,9 @@ describe('ngAnimate', function() { beforeEach(module(function($provide, $routeProvider) { $provide.value('$window', window = angular.mock.createMockWindow()); $routeProvider.when('/foo', {controller: noop, templateUrl: '/foo.html'}); - return function($templateCache) { + return function($templateCache, $animator) { $templateCache.put('/foo.html', [200, '
data
', {}]); + $animator.enabled(true); } })); @@ -579,8 +581,8 @@ describe('ngAnimate', function() { element = $compile(html( '
' + - '
' + 'ng-animate="{enter: \'customEnter\'}">' + + '' ))($rootScope); $location.path('/foo'); diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js index cef009933712..a4d4b46f667b 100644 --- a/test/testabilityPatch.js +++ b/test/testabilityPatch.js @@ -24,15 +24,17 @@ beforeEach(function() { // reset to jQuery or default to us. bindJQuery(); - jqLite(document.body).html(''); + jqLite(document.body).html('').removeData(); }); afterEach(function() { if (this.$injector) { var $rootScope = this.$injector.get('$rootScope'); + var $rootElement = this.$injector.get('$rootElement'); var $log = this.$injector.get('$log'); // release the injector dealoc($rootScope); + dealoc($rootElement); // check $log mock $log.assertEmpty && $log.assertEmpty();