Search code examples
angularjsunit-testingdirectiveisolate-scopecontrolleras

angularjs directive unit test fail with controllerAs, bindToController & isolateScope()


I am trying to unit test a directive with a two-way bound property (=). The directive works in my app, but I can't get a unit test working that tests the two-way binding.

I have been trying to get this working for days. I've read MANY examples that use some but not all of the features I want to use: controllerAs, bindToController & isolateScope(). (Forget about templateURL, which I also need. I will add that if I can get this working! :)

I'm hoping someone can tell me how to show a change in the parent scope reflected in the isolate scope.

Here is a plunkr that contains the code below:

http://plnkr.co/edit/JQl9fB5kTt1CPtZymwhI

Here is my test app:

var app = angular.module('myApp', []);

angular.module('myApp').controller('greetingController', greetingController);
greetingController.$inject = ['$scope'];
function greetingController($scope) {
  // this controller intentionally left blank (for testing purposes)
}

angular.module('myApp').directive('greetingDirective',
        function () {
            return {
                scope: {testprop: '='},
                restrict: 'E',
                template: '<div>Greetings!</div>',
                controller: 'greetingController',
                controllerAs: 'greetingController',
                bindToController: true
            };
        }
);

And here is the spec:

describe('greetingController', function () {

var ctrl, scope, rootScope, controller, data, template,
        compile, isolatedScope, element;

beforeEach(module('myApp'));

beforeEach(inject(function ($injector) {

    rootScope = $injector.get('$rootScope');
    scope = rootScope.$new();
    controller = $injector.get('$controller');
    compile = $injector.get('$compile');

    data = {
        testprop: 1
    };

    ctrl = controller('greetingController', {$scope: scope}, data);
    element = angular.element('<greeting-directive testprop="testprop"></greeting-directive>');
    template = compile(element)(scope);
    scope.$digest();
    isolatedScope = element.isolateScope();

}));

// PASSES
it('testprop inital value should be 1', function () {
    expect(ctrl.testprop).toBe(1);
});

// FAILS: why doesn't changing this isolateScope value 
// also change the controller value for this two-way bound property?
it('testprop changed value should be 2', function () {
    isolatedScope.testprop = 2;
    expect(ctrl.testprop).toBe(2);
}); 
});

Solution

  • You have to correct the way you're testing your directive. You're directly changing isolatedScope of an object and thereafter verifying the ctrl object which unrelated DOM which you had compiled.

    Basically what you should be doing is as soon as you compiled a DOM with scope (here it is <greeting-directive testprop="testprop"></greeting-directive>). So that scope will hold the context of compiled do. In short you can play testprop property value. or same thing will be available inside element.scope(). As soon as you change any value in scope/currentScope. You can see the value gets updated inside directive isolatedScope. One more thing I'd like to mention is when you do controllerAs with bindToController: true, angular adds property with controller alias inside scope that's we verified isolatedScope.greetingController.testprop inside assert

    Code

    describe('greetingController', function() {
    
      var ctrl, scope, rootScope, controller, data, template,
        compile, isolatedScope, currentScope, element;
    
      beforeEach(module('myApp'));
    
      beforeEach(inject(function($injector) {
    
        rootScope = $injector.get('$rootScope');
        scope = rootScope.$new();
        controller = $injector.get('$controller');
        compile = $injector.get('$compile');
    
        data = { testprop: 1 };
    
        ctrl = controller('greetingController', { $scope: scope }, data);
        element = angular.element('<greeting-directive testprop="testprop"></greeting-directive>');
        template = compile(element)(scope);
        scope.$digest();
        currentScope = element.scope();
        //OR
        //currentScope = scope; //both are same
        isolatedScope = element.isolateScope();
      }));
    
      // First test passes -- commented
    
      it('testprop changed value should be 2', function() {
        currentScope.testprop = 2; //change current element (outer) scope value
        currentScope.$digest(); //running digest cycle to make binding effects
        //assert
        expect(isolatedScope.greetingController.testprop).toBe(2);
      });
    
    });
    

    Demo Plunker