Подтвердить что ты не робот

Как правильно unit test директивы с DOM-манипуляциями?

Прежде чем задать свой реальный вопрос, у меня есть другой... Имеет ли смысл работать с unit test DOM в директивах Angular?

Например, здесь моя полная функция связывания:

function linkFn(scope, element) {
    var ribbon = element[0];
    var nav = ribbon.children[0];

    scope.ctrl.ribbonItemClick = function (index) {
        var itemOffsetLeft;
        var itemOffsetRight;
        var item;

        if (scope.ctrl.model.selectedIndex === index) {
            return;
        }

        scope.ctrl.model.selectedIndex = index;

        item = nav.querySelectorAll('.item')[index];

        itemOffsetLeft = item.offsetLeft - ribbon.offsetLeft;
        itemOffsetRight = itemOffsetLeft + item.clientWidth;

        if (itemOffsetLeft < nav.scrollLeft) {
            nav.scrollLeft = itemOffsetLeft - MAGIC_PADDING;
        }

        if(itemOffsetRight > nav.clientWidth + nav.scrollLeft) {
            nav.scrollLeft = itemOffsetRight - nav.clientWidth + MAGIC_PADDING;
        }

        this.itemClick({
            item: scope.ctrl.model.items[index],
            index: index
        });

        $location.path(scope.ctrl.model.items[index].href);
    };

    $timeout(function $timeout() {
        var item = nav.querySelector('.item.selected');
        nav.scrollLeft = item.offsetLeft - ribbon.offsetLeft - MAGIC_PADDING;
    });
}

Это для прокручиваемого компонента с вкладками, и я не знаю, как протестировать 3 экземпляра nav.scrollLeft = x.

Первые два оператора if выполняются, когда щелкнут элемент, который только частично виден. Элемент left/right (каждый if) будет привязан к левой/правой границе компонента.

Третий, это указать выбранный элемент, если он не отображается при загрузке компонента.

Как я unit test это с кармой/жасмином. это даже имеет смысл сделать это, или я должен выполнять функциональные тесты с помощью Protractor?

4b9b3361

Ответ 1

При тестировании директив найдите вещи, которые устанавливают или возвращают явные значения. Они, как правило, легко утверждать, и имеет смысл unit test их с Жасмином и Кармой.

Посмотрите Angular тесты для ng-src. Здесь они проверяют, что директива работает, утверждая, что атрибут src для элемента получает правильные значения. Он явный: либо атрибут src имеет определенное значение, либо нет.

it('should not result empty string in img src', inject(function($rootScope, $compile) {
  $rootScope.image = {};
  element = $compile('<img ng-src="{{image.url}}">')($rootScope);
  $rootScope.$digest();
  expect(element.attr('src')).not.toBe('');
  expect(element.attr('src')).toBe(undefined);
}));

То же самое с ng-bind. Здесь они передают строку HTML с компилятором $, а затем утверждают, что возвращаемое значение имеет свой HTML, заполненный фактическими значениями области. Опять же, это явно.

it('should set text', inject(function($rootScope, $compile) {
  element = $compile('<div ng-bind="a"></div>')($rootScope);
  expect(element.text()).toEqual('');
  $rootScope.a = 'misko';
  $rootScope.$digest();
  expect(element.hasClass('ng-binding')).toEqual(true);
  expect(element.text()).toEqual('misko');
}));

Когда вы попадаете в более сложные сценарии, такие как тестирование против видимости в видовом экране или тестирование того, расположены ли определенные элементы в правильных местах на странице, вы можете попробовать проверить, что атрибуты CSS и style установлены правильно, но это становится неудобно реальный быстрый и не рекомендуется. На этом этапе вы должны смотреть на Protractor или аналогичный инструмент тестирования e2e.

Ответ 2

Я бы 100% захотел проверить все пути вашей директивы, даже если это не самая простая вещь. Но есть подходы, которые вы можете предпринять, чтобы упростить этот процесс.

Перерыв сложной логики в службу

Первое, что выделяется мне, - сложная логика о настройке nav scrollLeft. Почему бы не разбить это на отдельную службу, которая может быть проверена самостоятельно?

app.factory('AutoNavScroller', function() {
  var MAGIC_PADDING;
  MAGIC_PADDING = 25;

  return function(extraOffsetLeft) {

    this.getScrollPosition = function(item, nav) {
      var itemOffsetLeft, itemOffsetRight;

      itemOffsetLeft = item.offsetLeft - extraOffsetLeft;
      itemOffsetRight = itemOffsetLeft + item.clientWidth;

      if ( !!nav && itemOffsetRight > nav.clientWidth + nav.scrollLeft) {

        return itemOffsetRight - nav.clientWidth + MAGIC_PADDING;


      } else {

        return itemOffsetLeft - MAGIC_PADDING;

      }
    };
  }
});

Это упрощает проверку всех путей и рефакторинга (что вы можете видеть, что я смог сделать выше. Тесты можно увидеть ниже:

describe('AutoNavScroller', function() {
  var AutoNavScroller;

  beforeEach(module('app'));

  beforeEach(inject(function(_AutoNavScroller_) {
    AutoNavScroller = _AutoNavScroller_;
  }));

  describe('#getScrollPosition', function() {
    var scroller, item;

    function getScrollPosition(nav) {
      return scroller.getScrollPosition(item, nav);
    }

    beforeEach(function() {
      scroller = new AutoNavScroller(50);
      item = {
        offsetLeft: 100
      };
    })

    describe('with setting initial position', function() {
      it('gets the initial scroll position', function() {
        expect(getScrollPosition()).toEqual(25);
      });
    });

    describe('with item offset left of the nav scroll left', function() {
      it('gets the scroll position', function() {
        expect(getScrollPosition({
          scrollLeft: 100
        })).toEqual(25);
      });
    });

    describe('with item offset right of the nav width and scroll left', function() {
      beforeEach(function() {
        item.clientWidth = 300;
      });

      it('gets the scroll position', function() {
        expect(getScrollPosition({
          scrollLeft: 25,
          clientWidth: 50
        })).toEqual(325);
      });
    });
  });
});

Проверить, является ли эта директива вызывающей службой

Теперь, когда мы нарушили нашу директиву, мы можем просто ввести службу и убедиться, что она вызвана правильно.

app.directive('ribbonNav', function(AutoNavScroller, $timeout) {
  return {
    link: function(scope, element) {
      var navScroller;
      var ribbon = element[0];
      var nav = ribbon.children[0];

      // Assuming ribbon offsetLeft remains the same
      navScroller = new AutoNavScroller(ribbon.offsetLeft);

      scope.ctrl.ribbonItemClick = function (index) {

        if (scope.ctrl.model.selectedIndex === index) {
            return;
        }

        scope.ctrl.model.selectedIndex = index;

        item = nav.querySelectorAll('.item')[index];

        nav.scrollLeft = navScroller.getScrollLeft(item, nav);
        // ...rest of directive
      };

      $timeout(function $timeout() {
        var item = nav.querySelector('.item.selected');

        // Sets initial nav scroll left
        nav.scrollLeft = navScroller.getScrollLeft(item);
      });

    }
  }
});

Самый простой способ убедиться, что наша директива продолжает использовать эту услугу, заключается в том, чтобы просто отслеживать методы, которые она вызовет, и убедиться, что они получают правильные параметры:

describe('ribbonNav', function() {
  var $compile, $el, $scope, AutoNavScroller;

  function createRibbonNav() {
    $el = $compile($el)($scope);
    angular.element(document)
    $scope.$digest();
    document.body.appendChild($el[0]);
  }

  beforeEach(module('app'));

  beforeEach(module(function ($provide) {
    AutoNavScroller = jasmine.createSpy();
    AutoNavScroller.prototype.getScrollLeft = function(item, nav) {
      return !nav ? 50 : 100;
    };
    spyOn(AutoNavScroller.prototype, 'getScrollLeft').and.callThrough();

    $provide.provider('AutoNavScroller', function () {
      this.$get = function () {
        return AutoNavScroller;
      }
    });
  }));

  beforeEach(inject(function(_$compile_, $rootScope) {
    $compile = _$compile_;
    $el = "<div id='ribbon_nav' ribbon-nav><div style='width:50px;overflow:scroll;float:left;'><div class='item selected' style='height:100px;width:200px;float:left;'>An Item</div><div class='item' style='height:100px;width:200px;float:left;'>An Item</div></div></div>";
    $scope = $rootScope.$new()
    $scope.ctrl = {
      model: {
        selectedIndex: 0
      }
    };
    createRibbonNav();
  }));

  afterEach(function() {
    document.getElementById('ribbon_nav').remove();
  });

  describe('on link', function() {
    it('calls AutoNavScroller with selected item', inject(function($timeout) {
      expect(AutoNavScroller).toHaveBeenCalledWith(0);
    }));

    it('calls AutoNavScroller with selected item', inject(function($timeout) {
      $timeout.flush();
      expect(AutoNavScroller.prototype.getScrollLeft)
        .toHaveBeenCalledWith($el[0].children[0].children[0]);
    }));

    it('sets the initial nav scrollLeft', inject(function($timeout) {
      $timeout.flush();
      expect($el[0].children[0].scrollLeft).toEqual(50);
    }));
  });

  describe('ribbonItemClick', function() {
    beforeEach(function() {
      $scope.ctrl.ribbonItemClick(1);
    });

    it('calls AutoNavScroller with item', inject(function($timeout) {
      expect(AutoNavScroller.prototype.getScrollLeft)
        .toHaveBeenCalledWith($el[0].children[0].children[1], $el[0].children[0]);
    }));

    it('sets the nav scrollLeft', function() {
      expect($el[0].children[0].scrollLeft).toEqual(100);
    });
  });
});

Теперь, очевидно, эти спецификации могут быть реорганизованы на 100 способов, но вы можете видеть, что более высокий охват намного проще достичь, как только мы начали выходить из сложной логики. Есть некоторые риски вокруг насмешек слишком много, потому что это может сделать ваши тесты хрупкими, но я считаю, что компромисс стоит того. Кроме того, я определенно вижу, что AutoNavScroller обобщается и используется повторно в другом месте. Это было бы невозможно, если бы код существовал в директиве раньше.

Заключение

В любом случае, причина, по которой я верю Angular, - это отличная возможность проверить эти директивы и как они взаимодействуют с DOM. Эти спецификации жасмина могут быть запущены в любом браузере и будут быстро устранять несоответствия или регрессии.

Кроме того, вот plunkr, чтобы вы могли видеть все движущиеся части и эксперимент: http://plnkr.co/edit/wvj4TmmJtxTG0KW7v9rn?p=preview