2016-07-12 2 views
1

Я построил угловую директиву onInputChange, которая должна срабатывать при обратном вызове, когда пользователи меняют значение ввода, либо щелкнув вне входа (размытие), либо нажав ENTER. Директива может использоваться как:Почему мои тесты на жасмин не срабатывают по этой директиве?

<input type="number" ng-model="model" on-input-change="callback()"/> 

Он использует следующий код:

app.directive('onInputChange', [ 
    "$parse", 
    function ($parse) { 
     return { 
      restrict : "A", 
      require : "ngModel", 
      link : function ($scope, $element, $attrs) { 
       // 
       var dirName  = "onInputChange", 
        callback = $parse($attrs[dirName]), 
        evtNS  = "." + dirName, 
        initial  = undefined; 

       // 
       if (angular.isFunction(callback)) { 
        $element 
         .on("focus" + evtNS, function() { 
          initial = $(this).val(); 
         }) 
         .on("blur" + evtNS, function() { 
          if ($(this).val() !== initial) { 
           $scope.$apply(function() { 
            callback($scope); 
           }); 
          } 
         }) 
         .on("keyup" + evtNS, function ($evt) { 
          if ($evt.which === 13) { 
           $(this).blur(); 
          } 
         }); 
       } 

       // 
       $scope.$on("$destroy", function() { 
        $element.off(evtNS); 
       }); 
      } 
     }; 
    } 
]); 

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

describe("directive", function() { 

    var $compile, $rootScope, $scope, $element; 

    beforeEach(function() { 
     angular.mock.module("app"); 
    }); 

    beforeEach(inject(function ($injector) { 

     $compile = $injector.get("$compile"); 
     $scope = $injector.get("$rootScope").$new(); 

     $scope.model = 0; 

     $scope.onchange = function() { 
      console.log("called"); 
     }; 

     $element = $compile("<input type='number' ng-model='model' on-input-change='onchange()'>")($scope); 
     $scope.$digest(); 

     spyOn($scope, "onchange"); 
    })); 

    afterEach(function() { 
     $scope.$destroy(); 
    }); 

    it("has default values", function() { 
     expect($scope.model).toBe(0); 
     expect($scope.onchange).not.toHaveBeenCalled(); 
    }); 

    it("should not fire callback on internal model change", function() { 
     $scope.model = 123; 
     $scope.$digest(); 

     expect($scope.model).toBe(123); 
     expect($scope.onchange).not.toHaveBeenCalled(); 
    }); 

    //this fails 
    it("should not fire callback when value has not changed", function() { 
     $element.focus(); 
     $element.blur(); 

     $scope.$digest(); 

     expect($scope.model).toBe(0); 
     expect($scope.onchange).not.toHaveBeenCalled(); 
    }); 

    it("should fire callback when user changes input by clicking away (blur)", function() { 
     $element.focus(); 
     $element.val(456).change(); 
     $element.blur(); 

     $scope.$digest(); 

     expect($scope.model).toBe(456); 
     expect($scope.onchange).toHaveBeenCalled(); 
    }); 

    //this fails 
    it("should fire callback when user changes input by clicking enter", function() { 
     $element.focus(); 
     $element.val(789).change(); 
     $element.trigger($.Event("keyup", {keyCode:13})); 

     $scope.$digest(); 

     expect($scope.model).toBe(789); 
     expect($scope.onchange).toHaveBeenCalled(); 
    }); 

}); 

Теперь моя проблема заключается в том, что два из моих тестов неудовлетворительная после запуска с кармой:

A:

Failed директива не срабатывает обратный вызов, когда значение не изменилось Прогнозный шпион Onchan не быть вызванным.

B:

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


Я создал Plunker, где вы можете попробовать сами.

1. Почему мой callback вызывается, даже если значение не изменилось?

2. Как я могу имитировать пользователя, поражающего ENTER на моем входе? Я уже пробовал разные способы, но никто не работает.

Извините за длинный вопрос. Надеюсь, я смог предоставить достаточно информации, чтобы кто-то мог помочь мне в этом. Спасибо :)


Другие вопросы здесь на так что я прочитал по поводу моего вопроса:

ответ

1

$parse всегда возвращает функцию, и angular.isFunction(callback) проверка не нужна.

keyCode не переведен на which при запуске keyup вручную.

$element.trigger($.Event("keyup", {which:13})) 

может помочь.

Обратный вызов инициируется, потому что здесь не может быть вызван focus, и это фактически undefined !== 0 в ($(this).val() !== initial состоянии.

Есть несколько причин для focus, чтобы не работать. Это не мгновенно, и спецификация должна стать асинхронной. И он не будет работать на отсоединенном элементе.

focus поведение может быть исправлено с помощью $element.triggerHandler('focus') вместо $element.focus().

DOM-тестирование относится к функциональным испытаниям, а не к единичным испытаниям, и jQuery может представить много сюрпризов при обращении с этим (спецификация демонстрирует верхушку айсберга). Даже когда спецификации зеленые, поведение in vivo может отличаться от in vitro, это делает тесты единицы почти бесполезными.

Правильная стратегия для модульного тестирования директивы, которая влияет на DOM, чтобы выставить все обработчик событий в объем - или к контроллеру, в случае директивы-области видимости №:

require: ['onInputChange', 'ngModel'], 
controller: function() { 
    this.onFocus =() => ...; 
    ... 
}, 
link: (scope, element, attrs, [instance, ngModelController]) => { ... } 

то контроллер экземпляра может быть полученные в спецификациях с

var instance = $element.controller('onInputChange'); 

Все методы контроллера могут быть протестированы отдельно от соответствующих событий. И обработка событий может быть протестирована, наблюдая за вызовами on. Для того, чтобы сделать этот angular.element.prototype or jQuery.prototype должен быть подсмотрел, как это:

spyOn(angular.element.prototype, 'on').and.callThrough(); 
spyOn(angular.element.prototype, 'off').and.callThrough(); 
spyOn(angular.element.prototype, 'val').and.callThrough(); 
... 
$element = $compile(...)($scope); 
expect($element.on).toHaveBeenCalledWith('focus.onInputChange', instance.onFocus); 
... 
instance.onFocus(); 
expect($element.val).toHaveBeenCalled(); 

Цель модульного теста заключается в проверке блок в отрыве от других движущихся частей (в том числе действия JQuery DOM, для этой цели ngModel может быть издевались тоже), вот как это делается.

Модульные тесты не делают функциональные тесты устаревшими, особенно в случае сложных многорежимных взаимодействий, но могут предлагать надежные испытания со 100% -ным охватом.

+0

Благодарим вас за подробное объяснение. Первая половина вашего ответа помогла мне исправить тесты, чтобы все прошло. В любом случае, я сейчас нахожусь, чтобы исправить мой метод тестирования, как вы описали во второй половине. Я новичок в тестировании угловых модулей, и ваш ответ - отличная помощь! +1 – Fidel90

+0

Не могли бы вы взглянуть на этот обновленный плункер: https://plnkr.co/edit/bxCuhvp5NazZxsA1tD1t?p=preview Я не уверен, как правильно применять и запускать шпионов. – Fidel90

+1

Я обновил ваш плункер с помощью нескольких спецификаций, чтобы показать, как это делается: https://plnkr.co/edit/dMl0UdxBypjGvsBkb281?p=info. Я изменил несколько вещей для удобства тестирования. Частные переменные '_' должны быть доступны для тестов. И прослушиватели 'on *' предоставляются непосредственно слушателям 'on', поэтому мы можем быть уверены, что слушатель - это функция, которую мы ожидаем (в противном случае мы должны поймать анонимную функцию с помощью' var listener $ element.on.calls.argsFor (...) [1] 'и проверьте, выполняет ли он то, что он должен делать, это PITA). – estus

Смежные вопросы