Search code examples
unit-testingangularjsng-animate

ngAnimate unit test not adding class


I'm new to unit testing as well as the ng-animate module. I made a simple directive to test out ng-animate.

 .directive('slideShow', function ($animate, $compile) {
    return {
      template: '<div class="slide-show-container"></div>',
      restrict: 'EA',
      replace: true,
      link: function (scope, element, attrs) {
        var newElement = $compile('<div class="slide-show-slide"></div>')(scope);

        element.bind('mouseenter',function() {
          element.append(newElement);
          $animate.addClass(newElement, 'slide-enter');
        });
        element.bind('mouseleave',function() {
          $animate.removeClass(newElement, 'slide-enter');
        });
      }
    };
  });

Then I made the following unit test to confirm that the .slide-enter class was being added.

  it('should add slide-enter class', function () {
    element.triggerHandler( "mouseenter" );
    expect(element.children().hasClass("slide-enter")).toEqual(true)
  });

The directive correctly added the class when I moused over it in a manual test. However the unit test failed and showed that the slide-enter class wasn't being added.

Finally I figured out the only way I could fix it was wrapping the unit test in a $timeout:

  it('should add slide-enter class', inject(function ($timeout) {
    element.triggerHandler( "mouseenter" );
    $timeout(function() {
      expect(element.children().hasClass("slide-enter")).toEqual(true);
    });
    $timeout.flush();
  }));

Can anyone help me understand why this $timeout is required for the test to work? Is there another way to get this unit test to work that I'm messing?


Solution

  • NOTE I am using angular-animate 1.2.0-rc.2 and have documented my findings with this version. The need for the $timeout.flush() call seems to be fixed when looking at the 1.2.0-rc.3 code but I have not tested it yet. https://github.com/angular/angular.js/blob/v1.2.0-rc.3/src/ngAnimate/animate.js


    I had the same problem with one of my tests. I was able to get my test to work by just calling $timeout.flush() after I had called the code that was supposed to trigger the adding of the class and before I called the expect. Your test should work if you rewrite it like:

    it('should add slide-enter class', inject(function ($timeout) {
      element.triggerHandler( "mouseenter" );
      $timeout.flush();  
      expect(element.children().hasClass("slide-enter")).toEqual(true);
    }));
    

    I had to dig into the ngAnimate code to figure it out and this is what I found.

    If you take a look at the angular-animate.js file at the addClass function. You will see the following:

    addClass : function(element, className, done) {
      performAnimation('addClass', className, element, null, null, function() {
        $delegate.addClass(element, className, done);
      });
    }
    

    The closure that is the last parameter to performAnimation is what will finally add the class.

    In performAnimation, that last parameter is named 'onComplete`. There is a section of code that deals with calling this closure when animations should be skipped:

    //skip the animation if animations are disabled, a parent is already being animated
    //or the element is not currently attached to the document body.
    if ((parent.inheritedData(NG_ANIMATE_STATE) || disabledAnimation).running) {
      //avoid calling done() since there is no need to remove any
      //data or className values since this happens earlier than that
      //and also use a timeout so that it won't be asynchronous
      $timeout(onComplete || noop, 0, false);
      return;
    }   
    

    And there is the call to $timeout that is causing the problem. When running this code in an angular test, the call to $timeout simply queues up the closure. The test code then has to call $timeout.flush() in order to get that function to run.