Misha Moroshko
Misha Moroshko

Reputation: 171479

Angular tests: How to expect element events to be triggered?

I'm decorating forms like this:

angular.module('Validation').directive('form', function() {
  return {
    restrict: 'E',
    link: function(scope, element) {
      var inputs = element[0].querySelectorAll('[name]');

      element.on('submit', function() {
        for (var i = 0; i < inputs.length; i++) {
          angular.element(inputs[i]).triggerHandler('blur');
        }
      });
    }
  };
});

Now, I'm trying to test this directive:

describe('Directive: form', function() {
  beforeEach(module('Validation'));

  var $rootScope, $compile, scope, form, input, textarea;

  function compileElement(elementHtml) {
    scope = $rootScope.$new();
    form = angular.element(elementHtml);
    input = form.find('input');
    textarea = form.find('textarea');
    $compile(form)(scope);
    scope.$digest();
  }

  beforeEach(inject(function(_$rootScope_, _$compile_) {
    $rootScope = _$rootScope_;
    $compile = _$compile_;

    compileElement('<form><input type="text" name="email"><textarea name="message"></textarea></form>');
  }));

  it('should trigger "blur" on all inputs when submitted', function() {
    spyOn(input, 'trigger');
    form.triggerHandler('submit');
    expect(input.trigger).toHaveBeenCalled(); // Expected spy trigger to have been called.
  });
});

But, the test fails.

What's the right Angular way to test this directive?

Upvotes: 3

Views: 6669

Answers (3)

Jon
Jon

Reputation: 4295

This is probably something to do with raising the 'submit' event during the test.

The angular team have created a pretty funky class to help them do this it seems to cover a lot of edge cases - see https://github.com/angular/angular.js/blob/master/src/ngScenario/browserTrigger.js

While this helper is from ngScenario I use it in my unit tests to overcome problems raising some events in headless browsers such as PhantomJS.

I had to use this to test a very similar directive that performs an action when a form is submitted see the test here https://github.com/jonsamwell/angular-auto-validate/blob/master/tests/config/ngSubmitDecorator.spec.js (see line 38).

I had to use this as I am using a headless browser for development testing purposes. It seems that to trigger an event in this type of browser the element that is triggering the event has to be attached to the dom as well.

Also as the form directive is one that angular already has you should either decorate this directive or give this directive a new name. I would actually suggest you decorate the ngSubmit directive instead of the form directive as this is more gear towards submitting a form. I actually have a very good example of this as I did this in the validation module I have open sourced. This should give you a very good start.

The directive source is here

The directive tests are here

Upvotes: 1

Khanh TO
Khanh TO

Reputation: 48982

You have some problems:

1) input = form.find('input'); and angular.element(inputs[i]); are 2 different wrapper objects wrapping the same underlying DOM object.

2) You should create a spy on triggerHandler instead.

3) You're working directly with DOM which is difficult to unit-test.

An example of this is: angular.element(inputs[i]) is not injected so that we have difficulty faking it in our unit tests.

To ensure that point 1) returns the same object. We can fake the angular.element to return a pre-trained value which is the input = form.find('input');

//Jasmine 1.3: andCallFake
//Jasmine 2.0: and.callFake
angular.element = jasmine.createSpy("angular.element").and.callFake(function(){
         return input; //return the same object created by form.find('input');
});

Side note: As form is already an angularJs directive, to avoid conflicting with an already defined directive, you should create another directive and apply it to the form. Something like this:

<form mycustomdirective></form>

I'm not sure if this is necessary. Because we're spying on a global function (angular.element) which may be used in many places, we may need to save the previous function and restore it at the end of the test. Your complete source code looks like this:

it('should trigger "blur" on all inputs when submitted', function() {

    var angularElement = angular.element; //save previous function

    angular.element = jasmine.createSpy("angular.element").and.callFake(function(){
         return input;
    });

    spyOn(input, 'triggerHandler');
    form.triggerHandler('submit');

    angular.element = angularElement; //restore
    expect(input.triggerHandler).toHaveBeenCalled(); // Expected spy trigger to have been called.
  });

Running DEMO

Upvotes: 7

Gil Birman
Gil Birman

Reputation: 35920

Try hooking into the blur event:

  it('should trigger "blur" on all inputs when submitted', function() {
    var blurCalled = false;
    input.on('blur', function() { blurCalled = true; });
    form.triggerHandler('submit');
    expect(blurCalled).toBe(true);
  });

Upvotes: 0

Related Questions