76

This custom validation directive is an example presented at the official angular site. http://docs.angularjs.org/guide/forms It checks a text input is in number format or not.

var INTEGER_REGEXP = /^\-?\d*$/;
app.directive('integer', function() {
  return {
    require: 'ngModel',
    link: function(scope, elm, attrs, ctrl) {
      ctrl.$parsers.unshift(function(viewValue) {
        if (INTEGER_REGEXP.test(viewValue)) {
          // it is valid
          ctrl.$setValidity('integer', true);
          return viewValue;
        } else {
          // it is invalid, return undefined (no model update)
          ctrl.$setValidity('integer', false);
          return undefined;
        }
      });
    }
  };
});

To unit test this code, I wrote this:

describe('directives', function() {
  beforeEach(module('exampleDirective'));

  describe('integer', function() {
    it('should validate an integer', function() {
      inject(function($compile, $rootScope) {
        var element = angular.element(
          '<form name="form">' +
            '<input ng-model="someNum" name="someNum" integer>' +
          '</form>'
          );
        $compile(element)($rootScope);
        $rootScope.$digest();
        element.find('input').val(5);
        expect($rootScope.someNum).toEqual(5);
      });
    });
  });
});

Then I get this error:

Expected undefined to equal 5.
Error: Expected undefined to equal 5.

I put print statements everywhere to see what is going on, and it looks like the directive is never called. What is a proper way to test a simple directive like this?

user1338062
  • 11,939
  • 3
  • 73
  • 67
ghiden
  • 2,083
  • 1
  • 17
  • 20
  • Thanks for taking the time to bring back an answer! Just FYI, you can extract your answer and mark it as the accepted one for later searchers - that is acceptable around here ;-) – Sean Vieira Jun 25 '13 at 01:05
  • Thank you for your tips. I moved my answer. – ghiden Jul 06 '13 at 07:54

3 Answers3

88

The other answer's tests should be written as:

describe('directives', function() {
  var $scope, form;
  beforeEach(module('exampleDirective'));
  beforeEach(inject(function($compile, $rootScope) {
    $scope = $rootScope;
    var element = angular.element(
      '<form name="form">' +
      '<input ng-model="model.somenum" name="somenum" integer />' +
      '</form>'
    );
    $scope.model = { somenum: null }
    $compile(element)($scope);
    form = $scope.form;
  }));

  describe('integer', function() {
    it('should pass with integer', function() {
      form.somenum.$setViewValue('3');
      $scope.$digest();
      expect($scope.model.somenum).toEqual('3');
      expect(form.somenum.$valid).toBe(true);
    });
    it('should not pass with string', function() {
      form.somenum.$setViewValue('a');
      $scope.$digest();
      expect($scope.model.somenum).toBeUndefined();
      expect(form.somenum.$valid).toBe(false);
    });
  });
});

Note that $scope.$digest() now is invoked after $setViewValue. This sets the form into “dirty” state, otherwise it would remain “pristine”, which probably is not what you want.

SomeKittens
  • 38,868
  • 19
  • 114
  • 143
jrief
  • 1,516
  • 13
  • 9
  • Thank you so much, helped me out a lot today ! But I'm using directly the scope model rather than `$setViewValue()`, I don't know if I'm missing a lot of cases...? – Florent Destremau Jun 16 '16 at 12:56
67

I figured it out by reading angular-app code https://github.com/angular-app/angular-app This video also helps too http://youtu.be/ZhfUv0spHCY?t=31m17s

Two mistakes that I made:

  • Do not bind directly to the scope when you are doing ng-model
  • Use form controller to directly manipulate what to pass for directives

Here is the updated version. The directive is the same, only the test that I changed.

describe('directives', function() {
  var $scope, form;
  beforeEach(module('exampleDirective'));
  beforeEach(inject(function($compile, $rootScope) {
    $scope = $rootScope;
    var element = angular.element(
      '<form name="form">' +
        '<input ng-model="model.somenum" name="somenum" integer />' +
      '</form>'
    );
    $scope.model = { somenum: null }
    $compile(element)($scope);
    $scope.$digest();
    form = $scope.form;
  }));

  describe('integer', function() {
    it('should pass with integer', function() {
      form.somenum.$setViewValue('3');
      expect($scope.model.somenum).toEqual('3');
      expect(form.somenum.$valid).toBe(true);
    });
    it('should not pass with string', function() {
      form.somenum.$setViewValue('a');
      expect($scope.model.somenum).toBeUndefined();
      expect(form.somenum.$valid).toBe(false);
    });
  });
});
ghiden
  • 2,083
  • 1
  • 17
  • 20
  • @ghiden This really helped me, however, have you had any experiencing chaining multiple directives and testing that? Like if you made a directive for telephone or email? I get a bunch of error anytime I tried and add an additional directive. Thoughts? – thescientist Sep 05 '13 at 00:20
  • Chaining multiple directives? I guess even if you apply multiple directives to one element, you still want to test them separately, correct? If each one works properly and priority is set correctly, I guess they should work. I sometimes do something like model validation first then proceed with visual effect directive with very low priority. – ghiden Sep 05 '13 at 09:15
  • @ghiden I found my issue, I was testing a form validation directive, and using ng-model, and I believe my major was issue was also using name as the directive, as well as an attribute on the input element. I'm all good now though, and testing it! Thanks so much for your example though. Really helped a lot. – thescientist Sep 05 '13 at 12:48
  • 1
    This wasn't working for me until I added `$scope.$digest()` after calling `$setViewValue`. Am I doing something weird or is this missing from you example? EDIT: sorry, did not see the answer below. – AndyPerlitch Aug 28 '14 at 19:12
  • i can never get the second test to pass (its always true) im using 1.3.12 did something change? – Mocksy Feb 06 '15 at 22:20
2

I test my custom directives searching in the object "$error" the name of the custom validation. Example:

  'use strict';

describe('Directive: validadorCorreo', function () {

  // load the directive's module
  beforeEach(module('sistemaRegistroProCivilApp'));

  var inputCorreo, formulario, elementoFormulario, scope, $compile;

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

    elementoFormulario = angular.element('<form name="formulario">' + 
      '<input type="text" name="correo" data-ng-model="correo" required data-validador-correo/>' + 
      '</form');
    scope.correo = '';
    elementoFormulario = $compile(elementoFormulario)(scope);
    scope.$digest();
    inputCorreo = elementoFormulario.find('input');
    formulario = scope.formulario;
    console.log(formulario.correo.$error);
  }));

  it('Deberia Validar si un correo ingresado en el input es correcto e incorrecto', inject(function ($compile) {

    inputCorreo.val('eric+@eric.com').triggerHandler('input');
    expect(formulario.correo.$error.email).toBe(true); //Here, the name of the custom validation appears in the $error object.
    console.log(formulario.correo.$error);

    inputCorreo.val('eric@eric.com').triggerHandler('input');
    expect(formulario.correo.$error.email).toBeUndefined();//Here, the name of the custom validation disappears in the $error object. Is Undefined
    console.log(formulario.correo.$error.email)
  }));
});

I Hope i can help you!