2014-02-13

Testing AngularJS directives with isolate scope (.isolateScope() is your friend)

In the previous post we have seen how to test directives with templateUrl
Now we are talking about how to test AngularJS directives with an isolated scope.

Let's say that you have a directive that should help you to select items from a given long list with filtering, ordering and selecting facilities:
<my-list model="exampleList" />
where exampleList is the following:
$scope.exampleList = [
    {id: '001', label:'First item', selected:false},
    {id: '002', label:'Second item', selected: false},
     ...
  ];

It sounds like a complex thing but you can implement it with Angular with few lines of Javascript code (app/scripts/directives/mylist.js): 
        'use strict';
angular.module('myListApp')
  .directive('myList', [function () {
    return {
      templateUrl: 'views/mylist.html',
      restrict: 'E',
      replace: true,
      scope: {model: '='},
      controller: ['$scope', '$element', '$attrs', function ($scope, $element, $attrs) {
        ... other logics       }]
    };
  }]);
with this directive's template (app/views/mylist.html):
<div>
  <div class="input-group filtercontrols">
    <span class="input-group-addon">Filter</span>
    <input type="text" class="form-control" placeholder="filter by" ng-model="filter">
    <span class="input-group-addon" ng-click="filter = ''">reset</span>
  </div>

  <div class="input-group ordercontrols">
    <label class="radio-inline">
      <input type="radio" value="label" ng-model="orderby"> Order by label
    </label>

    <label class="radio-inline">
      <input type="radio" value="id" ng-model="orderby"> Order by id
    </label>

    <label>
      <input type="checkbox" ng-model="reverse" />
      Reverse
    </label>
  </div>

  <div class="checkboxes">
    <div class="checkbox widgetcontrols" ng-repeat="elem in model | filter:{$:filter} | orderBy: orderby:reverse">
      <label>
        <input class="smart" type="checkbox" ng-model="elem.value" value="{{elem.value || false}}" />
        <span class="itemid">[{{elem.id}}]</span> {{elem.label}}
      </label>
    </div>
  </div>
</div>
Done!


How it works? This reusable component let you filter items and order them by label, id or reversed depending on the user input. If the user clicks on reversed, the list is reversed, if the user digits "Fir" as filter the list will be reduced and so on. Selecting one or more items, the binded $scope.exampleList will reflect the user choice.

How to test this directive

The directive has an isolate scope where model is two-way binded to $scope.exampleList (see model: '=' on the directive definition). The other ng-models you see into the template directives are just internal models that lives into the isolated scope of the myList directive (orderby, reverse, filter) and they don't affect the outer scope.

So when you are trying to change the reverse, orderby or filter properties, you'll have to do it on the right isolated scope!
'use strict';

describe('Directive: myList', function () {
  // load the directive's module
  beforeEach(module('myListApp', 'views/mylist.html'));

  var element,
    $rootScope,
    $compile,
    $element,
    scope;

  beforeEach(inject(function (_$rootScope_, _$compile_) {
    $rootScope = _$rootScope_;
    scope = $rootScope.$new();
    $compile = _$compile_;
  
  $rootScope.model = [
        {id: '001', label:'First item'},
        {id: '002', label:'Second item'}
      ];

    $element = angular.element('<my-list model="model"></my-list>');
    element = $compile($element)(scope);
    $rootScope.$apply();
  }));

  it('Labels order (reverse)', function () {
    var isolateScope = $element.isolateScope();

    isolateScope.reverse = true;
    isolateScope.orderby = 'label';
    isolateScope.$apply();

    expect($element.find('.itemid').eq(0).text()).toBe('[002]');
  });
});


So remember: .isolateScope is your friend!

Links:

2 comments:

  1. You are compiling $element into 'element' variable like this:
    element = $compile($element)(scope);
    but then you invoke isolateScope() on $element like this :
    $element.isolateScope()

    shouldn't it be
    element.isolateScope() ?

    ReplyDelete
    Replies
    1. Hi and thank you for your comment. The above code examples come from a real (toy) directive so I assume it is correct $element.isolateScope. Please have a look at here and let me know if it works (it's a long time I don't touch Angular!): https://github.com/davidemoro/angularjs-smartcheckbox/blob/master/test/spec/directives/smartcheckbox.js#L58

      Delete