How to Unit Test Isolated Scope Directive in AngularJS

38,708

Solution 1

See angular element api docs. If you use element.scope() you get the element's scope that you defined in the scope property of your directive. If you use element.isolateScope() you get the entire isolated scope. For example, if your directive looks something like this :

scope : {
 myScopeThingy : '='
},
controller : function($scope){
 $scope.myIsolatedThingy = 'some value';
}

Then calling element.scope() in your test will return

{ myScopeThingy : 'whatever value this is bound to' }

But if you call element.isolateScope() you'll get

{ 
  myScopeThingy : 'whatever value this is bound to', 
  myIsolatedThingy : 'some value'
}

This is true as of angular 1.2.2 or 1.2.3, not sure exactly. In previous versions you had only element.scope().

Solution 2

You can do var isolateScope = myDirectiveElement.scope() to get the isolate scope.

You don't really need to test that $watch was called though.. that's more testing angularjs than testing your app. But I guess it's just an example for the question.

Solution 3

move the logic to a separate controller, ie:

//will get your isolate scope
function MyCtrl($scope)
{
  //non-DOM manipulating ctrl logic here
}
app.controller(MyCtrl);

function MyDirective()
{
  return {
    scope     : {},
    controller: MyCtrl,
    link      : function (scope, element, attrs)
    {
      //moved non-DOM manipulating logic to ctrl
    }
  }
}
app.directive('myDirective', MyDirective);

and test latter as you would any controller - passing the scope object in directly (see Controllers section here for an example).

if you need to trigger $watch in your test do:

describe('MyCtrl test', function ()
{
  var $rootScope, $controller, $scope;

  beforeEach(function ()
  {
    inject(function (_$rootScope_, _$controller_)
    {
      // The injector unwraps the underscores (_) from around the parameter names when matching
      $rootScope = _$rootScope_;
      $controller = _$controller_;
    });

    $scope = $rootScope.$new({});
    $scope.foo = {x: 1}; //initial scope state as desired
    $controller(MyCtrl, {$scope: $scope}); //or by name as 'MyCtrl'
  });

  it('test scope property altered on $digest', function ()
  {
    $scope.$digest(); //trigger $watch
    expect($scope.foo.x).toEqual(1); //or whatever
  });
});
Share:
38,708

Related videos on Youtube

SavoryBytes
Author by

SavoryBytes

Updated on July 05, 2022

Comments

  • SavoryBytes
    SavoryBytes almost 2 years

    What is a good way to unit test isolated scope in AngularJS

    JSFiddle showing unit test

    Directive snippet

        scope: {name: '=myGreet'},
        link: function (scope, element, attrs) {
            //show the initial state
            greet(element, scope[attrs.myGreet]);
    
            //listen for changes in the model
            scope.$watch(attrs.myGreet, function (name) {
                greet(element, name);
            });
        }
    

    I want to ensure the directive is listening for changes - this does not work with an isolated scope:

        it('should watch for changes in the model', function () {
            var elm;
            //arrange
            spyOn(scope, '$watch');
            //act
            elm = compile(validHTML)(scope);
            //assert
            expect(scope.$watch.callCount).toBe(1);
            expect(scope.$watch).toHaveBeenCalledWith('name', jasmine.any(Function));
        });
    

    UPDATE: I got it to work by checking if the expected watchers were added to the child scope, but it's very brittle and probably using the accessors in an undocumented way (aka subject to change without notice!).

    //this is super brittle, is there a better way!?
    elm = compile(validHTML)(scope);
    expect(elm.scope().$$watchers[0].exp).toBe('name');
    

    UPDATE 2: As I mentioned this is brittle! The idea still works but in newer versions of AngularJS the accessor has changed from scope() to isolateScope():

    //this is STILL super brittle, is there a better way!?
    elm = compile(validHTML)(scope);                       
    expect(elm.isolateScope().$$watchers[0].exp).toBe('name');
    
    • tusharmath
      tusharmath about 10 years
      Did you find a way to setup the spying?
    • SavoryBytes
      SavoryBytes about 10 years
      @Tushar not really, as before there is a way to get it to work but it's subject to change without notice so use at your own risk.
  • SavoryBytes
    SavoryBytes about 11 years
    I'm not sure I agree it's "testing angular" I'm not testing that $watch works but simply that the directive is property "wired-up" to angular.
  • Andrew Joslin
    Andrew Joslin almost 11 years
    Also daniellmb, the way to test this would be to expose your greet function and spy on that, and check if that is called - not the $watch.
  • SavoryBytes
    SavoryBytes almost 11 years
    Right, this is a contrived example, but I was interested if there is a clean way to test isolate scope. Breaking encapsulation and putting methods on the scope wouldn't work in this case as there's no hook to add the spy before it's called.
  • SavoryBytes
    SavoryBytes over 10 years
    v1.2.3 feat(jqLite): expose isolateScope() getter similar to scope() github.com/angular/angular.js/commit/…
  • tusharmath
    tusharmath about 10 years
    but where do you spy on the $watch method?
  • Yair Tavor
    Yair Tavor about 10 years
    you could expose the function that runs on the $watch and then spy on it. In the directive, set "scope.myfunc = function()...", then in the $watch do "$scope.$watch('myName', scope.myfunc);". Now in the test you can get myFunc from the isolated scope and spy on it.
  • Danger14
    Danger14 about 10 years
    @AndyJoslin, Out of curiosity, why create a isolateScope variable at all? See Ang's comment on this egghead video (egghead.io/lessons/angularjs-unit-testing-directive-scope): As of Angular 1.2, to retrieve the isolated scope, one needs to use element.isolateScope() instead of element.scope() code.angularjs.org/1.2.0/docs/api/angular.element
  • mcv
    mcv almost 10 years
    Doesn't work for me. element.isolateScope() returns undefined. And element.scope() returns a scope that doesn't contain all the stuff that I put on my scope.
  • taylonr
    taylonr almost 10 years
    Wish I had more upvotes to give. You helped me after an hour of scratching my head.
  • Will Keeling
    Will Keeling almost 10 years
    @mcv I found I needed to do element.children().isolateScope()
  • user1821052
    user1821052 about 9 years
    @mcv - are you using templateUrl in your directive? if so you'll need to wrap the isolateScope call in a $timeout to access it. I guess because the $http call for the template load runs after the call to isolateScope (if you don't use the timeout)
  • Matsemann
    Matsemann over 8 years
    Need to call scope.$digest before being able to call element.isolateScope
  • Arashsoft
    Arashsoft over 6 years
    Warning: .scope() and .isolateScope() are not available if debugInfoEnabled is false. It means you miss the performance improvement if you want to use these methods. More information: docs.angularjs.org/guide/production#disabling-debug-data