¿Por qué mis pruebas de jasmine fallan en esta directiva?

Construí una directiva angular onInputChange que debería disparar una callback cuando los usuarios cambian el valor de una entrada haciendo clic fuera de la entrada (desenfoque) o presionando ENTER . La directiva puede ser utilizada como:

  

Utiliza el siguiente código:

 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); }); } }; } ]); 

La directiva funciona como esperaría en mi aplicación. Ahora he decidido escribir algunas pruebas para asegurar realmente que este es el caso:

 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("")($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(); }); }); 

Ahora, mi problema es que dos de mis pruebas fallan después de ejecutarse con karma:

UNA:

La directiva fallida no debe activar la callback cuando el valor no ha cambiado. Espera de intercambio no se ha llamado.

SEGUNDO:

La directiva fallida debe activar la callback cuando el usuario cambia la entrada haciendo clic en entrar Se ha llamado a espiar onchange esperado.


He creado un Plunker donde puedes probarlo tú mismo.

1. ¿Por qué se llama a mi callback incluso si el valor no ha cambiado?

2. ¿Cómo puedo simular que el usuario pulsa ENTER en mi entrada? Ya probé diferentes maneras pero ninguna funciona.

Perdón por la larga pregunta. Espero haber podido proporcionar suficiente información para que alguien pueda ayudarme con esto. Gracias 🙂


Otras preguntas aquí sobre las que he leído con respecto a mi problema:

  • ¿Cómo puedo activar un evento keyup / keydown en una prueba de unidad angularjs?
  • Prueba de unidad AngularJS para evento de pulsación de tecla

$parse siempre devuelve una función, y la angular.isFunction(callback) es necesaria.

keyCode no se traduce a lo which al activar keyup manualmente.

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

puede ayudar.

La callback se activa porque el focus no se puede activar manualmente aquí, y en realidad undefined !== 0 está undefined !== 0 en ($(this).val() !== initial condición ($(this).val() !== initial .

Hay un par de razones para que el focus no funcione. No es instantáneo, y la especificación debería volverse asíncrona. Y no funcionará en elemento separado.

focus comportamiento del focus se puede arreglar usando $element.triggerHandler('focus') lugar de $element.focus() .

Las pruebas de DOM pertenecen a las pruebas funcionales, no a las pruebas unitarias, y jQuery puede presentar muchas sorpresas cuando se las trata de esa manera (la especificación muestra la punta del iceberg). Incluso cuando las especificaciones son verdes, el comportamiento in vivo puede diferir del in vitro, esto hace que las pruebas unitarias sean casi inútiles.

Una estrategia adecuada para probar en unidad una directiva que afecta a DOM es exponer todos los controladores de eventos al scope, o al controlador, en el caso de una directiva sin scope:

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

Entonces la instancia del controlador se puede obtener en especificaciones con

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

Todos los métodos de controlador se pueden probar por separado de los eventos relevantes. Y el manejo de eventos se puede probar observando las llamadas de método. Para hacer este angular.element.prototype o jQuery.prototype tiene que ser espiado, así:

 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(); 

El propósito de la prueba de unidad es probar una unidad aislada de otras partes móviles (incluidas las acciones de DOM de jQuery, para este propósito se puede burlar de ngModel también), así es como se hace.

Las pruebas unitarias no hacen que las pruebas funcionales se vuelvan obsoletas, especialmente en el caso de interacciones multidireccionales complejas, pero pueden ofrecer pruebas sólidas con una cobertura del 100%.