19

I have the following AngularJS directive that creates an input element. Input has ng-change attribute that runs doIt() function. In my directive's unit test I want to check if doIt function is called when users changes the input. But the test does not pass. Though it works in the browser when testing manually.

Directive:

...
template: "<input ng-model='myModel' ng-change='doIt()' type='text'>" 

Test:

el.find('input').trigger('change') // Dos not trigger ng-change

Live demo (ng-change): http://plnkr.co/edit/0yaUP6IQk7EIneRmphbW?p=preview


Now, the test passes if I manually bind change event instead of using ng-change attribute.

template: "<input ng-model='myModel' type='text'>",
link: function(scope, element, attrs) {
  element.bind('change', function(event) {
    scope.doIt();
  });
}

Live demo (manual binding): http://plnkr.co/edit/dizuRaTFn4Ay1t41jL1K?p=preview


Is there a way to use ng-change and make it testable? Thank you.

2
  • 1
    ng-change is triggered on model changes, not events. why not just test the controller code for your "doIt" fn? Commented Jul 13, 2013 at 11:25
  • @marko, thanks for good question. All I want to do in directive's test is to check that doIt is called when user changes the input. doIt itself will be mocked in directive's test, because it is already tested in controller's test, as you suggested. Commented Jul 14, 2013 at 6:00

5 Answers 5

31

From your explanatory comment:

All I want to do in directive's test is to check that doIt is called when user changes the input.

Whether or not the expression indicated by ng-change is correctly evaluated or not is really the responsibility of the ngModel directive, so I'm not sure I'd test it in this way; instead, I'd trust that the ngModel and ngChange directives have been correctly implemented and tested to call the function specified, and just test that calling the function itself affects the directive in the correct manner. An end-to-end or integration test could be used to handle the full-use scenario.

That said, you can get hold of the ngModelController instance that drives the ngModel change callback and set the view value yourself:

it('trigger doIt', function() {
  var ngModelController = el.find('input').controller('ngModel');
  ngModelController.$setViewValue('test');
  expect($scope.youDidIt).toBe(true);
});

As I said, though, I feel like this is reaching too far into ngModel's responsibilities, breaking the black-boxing you get with naturally composable directives.

Example: http://plnkr.co/edit/BaWpxLuMh3HvivPUbrsd?p=preview


[Update]

After looking around at the AngularJS source, I found that the following also works:

it('trigger doIt', function() {
  el.find('input').trigger('input');
  expect($scope.youDidIt).toBe(true);
});

It looks like the event is different in some browsers; input seems to work for Chrome.

Example: http://plnkr.co/edit/rbZ5OnBtKMzdpmPkmn2B?p=preview

Here is the relevant AngularJS code, which uses the $sniffer service to figure out which event to trigger:

changeInputValueTo = function(value) {
  inputElm.val(value);
  browserTrigger(inputElm, $sniffer.hasEvent('input') ? 'input' : 'change');
};

Even having this, I'm not sure I'd test a directive in this way.

Sign up to request clarification or add additional context in comments.

3 Comments

Regarding the events, I think input is for text inputs and the like and change is for select boxes & radio buttons. At least that's what's working for me running tests on PhantomJS.
It seems browserTrigger is only present in angular-scenario.js. This means that it's only intended for e2e test, right? I am not able to use this function into a unit test, which is on purpose I believe.
@unludo Perhaps; my point was only that the AngularJS source was written to trigger a different event in certain browsers.
1

I googled "angular directive trigger ng-change" and this StackOverflow question was the closest I got to anything useful, so I'll answer "How to trigger ng-change in a directive", since others are bound to land on this page, and I don't know how else to provide this information.

Inside the link function on the directive, this will trigger the ng-change function on your element:

element.controller('ngModel').$viewChangeListeners[0]();

element.trigger("change") and element.trigger("input") did not work for me, neither did anything else I could find online.

As an example, triggering the ng-change on blur:

wpModule.directive('triggerChangeOnBlur', function () {
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {
            element.on('blur', function () {
                element.controller('ngModel').$viewChangeListeners[0]();
            });
        }
    };
}]);

I'm sorry that this is not directly answering OP's question. I will be more than happy to take some sound advice on where and how to share this information.

1 Comment

Ideally, you would ask a separate question and then answer it yourself (sounds weird, but it's encouraged). I'm also looking for something that is not quite the OP, or your issue, and would find the answer faster if there were a question specifically about it.
1

simple and it works in your unit test env:

spyOn(self, 'updateTransactionPrice');


var el = compile('<form name="form" latest novalidate json-schema="main.schema_discount" json-schema-model="main._data"><input type="text" ng-model="main._data.reverse_discount" ng-class="{ \'form-invalid\': form.reverse_discount.$invalid }" ng-change="main.transactionPrice(form);" name="reverse_discount" class="form-control-basic" placeholder="" ng-disabled="!main.selectedProduct.total_price"></form>')(scope);
el.find('input').triggerHandler('change');

expect(self.updateTransactionPrice).toHaveBeenCalled();

Comments

0

I was looking for this simple line for long hours. Just to save that in here.

How to select value from html-select, using Karma, and so get ng-change function working?

HTML:

Controller or directive JS:

  $scope.itemTypes = [{name: 'Some name 1', value: 'value_1'}, {name: 'Some name 2', value: 'value_2'}]

  $scope.itemTypeSelected = function () {
    console.log("Yesssa !!!!");
  };

Karma test fragment:

  angular.element(element.find("#selectedItemType")[0]).val('value_1').change();
  console.log("selected model.selectedItemType", element.isolateScope().model.selectedItemType);

Console:

'Yesssa !!!!'
'selected model.selectedItemType', 'value_1'

Comments

0

Have been trying to get this to work, but failed on every attempt. Finally concluded that my ng-model-options with a debounce setting on the onUpdate, was the problem.

If you have a debounce, make sure that you flush with the $timeout service. In angular mock, this timeout service has been extended with a flush operation, which handles all unfulfilled requests/actions.

    var tobetriggered = angular.element(element[0].querySelector('.js-triggervalue'));
    tobetriggered.val('value');
    tobetriggered.trigger('change');
    $timeout.flush();

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.