Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit test angular directive updating the ngModel

I'm trying to test a directive that I've written to validate an input and I have some trouble. The input managed by the directive should contain a valid hex color value and if the user modifies it with an invalid value, I want to cancel this modification. My directive is the following and is working as expected:

module.directive('colorValidate', function() {
    return {
        restrict: 'A',
        scope: {
            color: '=ngModel'
        },
        link: function(scope, element) {
            var previousValue = '#ffffff';
            //pattern that accept #ff0000 or #f00
            var colorPattern = new RegExp('^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$');
            element.on('focus', function() {
                previousValue = scope.color;
            });
            element.on('blur', function() {
                if (!colorPattern.test(scope.color)) {
                    scope.$apply(function() {
                        scope.color = previousValue;
                    });
                }
                else {
                    scope.$apply(function() {
                        scope.color = scope.color.toLowerCase();
                    });
                }
            });
        }
    };
});

and here is a example of input using this directive:

<input color-validate type="text" ng-model="color.color"/>

First question: Is this way of accessing and modifying the ngModel of the element correct?

Then my main problem is the testing part. Here are two simple tests that I wrote and that are not really working:

describe('colorValidate directive', function() {
    var scope,
        elem,
        compiled,
        html;

    beforeEach(function() {
        html = '<input color-validate type="text" ng-model="color.color"/>';

        inject(function($compile, $rootScope) {
            scope = $rootScope.$new();
            scope.color = {color: '#aaaaaa'};
            elem = angular.element(html);
            compiled = $compile(elem);
            compiled(scope);
            scope.$digest();
        });
    });

    it('should permit valid 6-chars color value', function() {
        elem.triggerHandler('focus');
        elem.val('#FF0000');
        elem.triggerHandler('blur');
        scope.$digest();
        expect(elem.val()).toBe('#FF0000');
    });

    it('should reject non valid color values', function() {
        elem.triggerHandler('focus');
        elem.val('#F00F');
        scope.$digest();
        elem.triggerHandler('blur');
        expect(elem.val()).toBe('#aaaaaa');
    });
});

The first test succeed and the second failed because the tested value is '#F00F' instead of '#aaaaaa'. Basically, none of my tests are actually modifying the ngModel value handled by the directive...

like image 573
ValentinH Avatar asked Nov 26 '14 13:11

ValentinH


1 Answers

Calling elem.val() does not actually cause the scope.color to get updated. In other words, this test will fail:

it("should set scope", function () {
   elem.triggerHandler("focus");
   elem.val("#FF0000");
   scope.$digest();
   elem.triggerHandler("blur");

   //Will fail: expected #aaaaaa to be #ff0000
   expect(scope.color.color).toBe("#ff0000");
});

This is because ngModel binds to key events on the input and updates the model (scope) at that point. Calling val() or value on the element doesn't trigger an event that angular would think something changed (even in a $digest loop). Therefore, you should be running your tests by changing the model values and asserting they are being accepted or reset:

it('should permit valid 6-chars color value', function() {
    elem.triggerHandler('focus');
    scope.color.color = '#FF0000';
    //need to trigger a digest here for the two-way binding
    scope.$digest();
    elem.triggerHandler('blur');
    //Don't need a $digest here because you call scope.$apply() within the blur in both if/else conditions
    //scope.$digest();
    expect(scope.color.color).toBe('#ff0000');
});

it('should reject non valid color values', function() {
    elem.triggerHandler('focus');
    scope.color.color = '#F00F';
    //need to trigger a digest here for the two-way binding
    scope.$digest();
    elem.triggerHandler('blur');
    expect(scope.color.color).toBe('#aaaaaa');
});

You don't need to test that the value is updated because presumably angular would have already written the tests to make sure that when the directive has two-way binding (=ngModel) the view is updated when the scope value changes within the directive.

like image 165
Patrick Avatar answered Sep 30 '22 14:09

Patrick