dimanche 20 novembre 2016

Angular 1.5 / jasmine - test directive containing animation fails

I'm upgrading from ng 1.3.15 to 1.5.8, and running into an issue with animations not completing, and thus tests failing.

I've boiled the issue down to the following:

(function() {
    var app = angular.module('wtf', ['ngAnimate']);

    app.controller('SlideHolderController', function($rootScope, $scope, $compile, $animate) {
        var vm = this;
        vm.slides = [];

        vm.addSlide = function(slide) {
            slide._index = vm.slides.length;

            vm.slides.push(slide);

            slide.close = function() {
                vm.removeSlide(slide);
            };

            console.log('Added slide: ' + slide.title);
        };

        vm.removeSlide = function(slide) {
            $animate.leave(slide.element).then(function() {
                vm.animationComplete();
            });
        };

        vm.animationComplete = function() {
            console.log('Animation complete for ' + slide.title);
        };

        $rootScope.$on('AddSlide', function(evt, slide) {
            vm.addSlide(slide);
            slide.element = $scope.element.children().eq(slide._index);
        });

    });


    app.controller('TestAnimationController', function($rootScope) {
        var vm = this;

        var slide_count = 0;

        vm.addNewSlide = function() {
            var slide = {title: 'Slide-#' + (slide_count + 1)};
            $rootScope.$broadcast('AddSlide', slide);
        };
    });


    app.directive('slideHolder', function() {
        return {
            replace: false,
            restrict: 'E',
            template: '<div><span ng-click="slide.close();" ng-repeat="slide in vm.slides"></span></div>',
            controller: 'SlideHolderController',
            controllerAs: 'vm',
            scope: true,
            link: function(scope, elem, attrs, vm) {
                scope.element = elem;
            },
        };
    });


    app.directive('testAnimation', function() {
        return {
            replace: false,
            restrict: 'E',
            template: '<div class="test"><button ng-click="vm.addNewSlide()">Add New Slide</button></div>',
            controller: 'TestAnimationController',
            controllerAs: 'vm',
            scope: true,
        };
    });

})();

and the test:

(function() {
    'use strict';

    describe('Directive: testAnimation', function() {
        var compile;
        var element;
        var root;
        var scope;
        var slideController;

        beforeEach(module('wtf'));

        beforeEach(module('templates'));

        beforeEach(inject(function($rootScope, $compile) {
            compile = $compile;
            scope = $rootScope.$new();
        }));


        beforeEach(function() {
            root = angular.element('<div></div>');
            compile(root)(scope);
            scope.$digest();

            var html = compileDirective('<slide-holder class="slide-holder"></slide-holder>', root);
            slideController = html.controller('slideHolder');
            expect(slideController).toBeDefined();
            expect(slideController.slides.length).toBe(0);

            spyOn(slideController, 'addSlide').and.callThrough();
            spyOn(slideController, 'removeSlide').and.callThrough();
            spyOn(slideController, 'animationComplete').and.callThrough();
        });


        var compileDirective = function(directive, elemToAppendTo) {
            var html = angular.element(directive);
            var elem = compile(html)(scope.$new());

            if (elemToAppendTo) {
                elemToAppendTo.append(html);
            }

            var elem_scope = elem.isolateScope() || elem.scope();
            elem_scope.$digest();

            return elem;
        };

        it('verifies removing a slide', inject(function($timeout) {
            var el = compileDirective('<test-animation></test-animation>', root);

            var slide_holder = root.find('.slide-holder').eq(0);
            var test_div = root.find('.test').eq(0);

            // test animation directive and the slide-holder are now siblings under root
            expect(slide_holder.parent().html()).toBe(el.parent().html());

            expect(slide_holder.find('span').length).toBe(0);
            expect(slideController.slides.length).toBe(0);

            var button = test_div.find('button').eq(0);
            expect(button.length).toBe(1);

            button.click();
            scope.$digest();

            expect(slideController.addSlide).toHaveBeenCalled();

            expect(slide_holder.find('span').length).toBe(1);
            expect(slideController.slides.length).toBe(1);

            var slide_span = slide_holder.children().eq(0).find('span');
            slide_span.click();
            scope.$digest();

            expect(slideController.removeSlide).toHaveBeenCalled();

            // $timeout.flush(); // fails with No deferred tasks to be flushed
            expect(slideController.animationComplete).toHaveBeenCalled();  // failure here
        }));
    });
})();

The problem is the final expectation fails, apparently because there is no root or body element for the animate element to live in. This is a marked change from 1.3.15 (the prior version I was using) and I can't find any documentation on how to address it. This is a blocker for my production app, which relies heavily on a structure very similar to that above, where a slide-holder exists independently of any controllers or directives that might add slides.

Can anyone shed light on what I need to do?

Aucun commentaire:

Enregistrer un commentaire