Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mocking out required controllers in directive tests

I am having a hard time trying to figure out how I mock out a required controller for a directive I have written that's the child of another.

First let me share the directives I have:

PARENT

angular
    .module('app.components')
    .directive('myTable', myTable);

function myTable() {
    var myTable = {
        restrict: 'E',
        transclude: {
            actions: 'actionsContainer',
            table: 'tableContainer'
        },
        scope: {
            selected: '='
        },
        templateUrl: 'app/components/table/myTable.html',
        controller: controller,
        controllerAs: 'vm',
        bindToController: true
    };

    return myTable;

    function controller($attrs, $scope, $element) {
        var vm = this;
        vm.enableMultiSelect = $attrs.multiple === '';
    }
}

CHILD

angular
    .module('app.components')
    .directive('myTableRow', myTableRow);

myTableRow.$inject = ['$compile'];

function myTableRow($compile) {
    var myTableRow = {
        restrict: 'A',
        require: ['myTableRow', '^^myTable'],
        scope: {
            model: '=myTableRow'
        },
        controller: controller,
        controllerAs: 'vm',
        bindToController: true,
        link: link
    };

    return myTableRow;

    function link(scope, element, attrs, ctrls) {

        var self = ctrls.shift(),
            tableCtrl = ctrls.shift();

        if(tableCtrl.enableMultiSelect){
            element.prepend(createCheckbox());
        }

        self.isSelected = function () {
            if(!tableCtrl.enableMultiSelect) {
                return false;
            }
            return tableCtrl.selected.indexOf(self.model) !== -1;
        };

        self.select = function () {
            tableCtrl.selected.push(self.model);
        };

        self.deselect = function () {
            tableCtrl.selected.splice(tableCtrl.selected.indexOf(self.model), 1);
        };

        self.toggle = function (event) {
            if(event && event.stopPropagation) {
                event.stopPropagation();
            }

            return self.isSelected() ? self.deselect() : self.select();
        };

        function createCheckbox() {
            var checkbox = angular.element('<md-checkbox>').attr({
                'aria-label': 'Select Row',
                'ng-click': 'vm.toggle($event)',
                'ng-checked': 'vm.isSelected()'
            });

            return angular.element('<td class="md-cell md-checkbox-cell">').append($compile(checkbox)(scope));
        }
    }

    function controller() {

    }
}

So as you can probably see, its a table row directive that prepends checkbox cells and when toggled are used for populating an array of selected items bound to the scope of the parent table directive.

When it comes to unit testing the table row directive I have come across solutions where can mock required controllers using the data property on the element.

I have attempted this and am now trying to test the toggle function in my table row directive to check it adds an item to the parent table directive's scope selected property:

describe('myTableRow Directive', function() {
  var $compile,
    scope,
    compiledElement,
    tableCtrl = {
      enableMultiSelect: true,
      selected: []
    },
    controller;

  beforeEach(function() {
    module('app.components');
    inject(function(_$rootScope_, _$compile_) {
      scope = _$rootScope_.$new();
      $compile = _$compile_;
    });

    var element = angular.element('<table><tbody><tr my-table-row="data"><td></td></tr></tbody></table>');

    element.data('$myTableController', tableCtrl);
    scope.data = {foo: 'bar'};
    compiledElement = $compile(element)(scope);
        scope.$digest();
    controller = compiledElement.controller('myTableRow');

  });

  describe('select', function(){
    it('should work', function(){
      controller.toggle();
      expect(tableCtrl.selected.length).toEqual(1);
    });
  });
});

But I'm getting an error:

undefined is not an object (evaluating 'controller.toggle')

If I console log out the value of controller in my test it shows as undefined.

I am no doubt doing something wrong here in my approach, can someone please enlighten me?

Thanks

UPDATE

I have come across these posts already:

Unit testing a directive that defines a controller in AngularJS

How to access controllerAs namespace in unit test with compiled element?

I have tried the following, given I'm using controllerAs syntax:

var element = angular.element('<table><tr act-table-row="data"><td></td></tr></table>');
  element.data('$actTableController', tableCtrl);
  $scope.data = {foo: 'bar'};
  $compile(element)($scope);
  $scope.$digest();
  console.log(element.controller('vm'));

But the controller is still coming up as undefined in the console log.

UPDATE 2

I have come across this post - isolateScope() returning undefined when testing angular directive

Thought it could help me, so I tried the following instead

console.log(compiledElement.children().scope().vm);

But still it returns as undefined. compiledElement.children().scope() does return a large object with lots of angular $$ prefixed scope related properties and I can see my vm controller I'm trying to get at is buried deep within, but not sure this is the right approach

UPDATE 3

I have come across this article which covers exactly the kind of thing I'm trying to achieve.

When I try to implement this approach in my test, I can get to the element of the child directive, but still I am unable to retrieve it's scope:

beforeEach(function(){
    var element = angular.element('<table><tr act-table-row="data"><td></td></tr></table>');
    element.data('$actTableController', tableCtrl);
    $scope.data = {foo: 'bar'};
    compiledElement = $compile(element)($scope);
    $scope.$digest();
    element = element.find('act-table-row');
    console.log(element);
    console.log(element.scope()); //returns undefined
});

I just wonder if this is down to me using both a link function and controllerAs syntax?

like image 469
mindparse Avatar asked Oct 19 '22 01:10

mindparse


1 Answers

You were very close with the original code you'd posted. I think you were just using .controller('myTableRow') on the wrong element, as your compiledElement at this point was the whole table element. You needed to get a hold of the actual tr child element in order to get the myTableRow controller out of it.

See below, specifically:

controller = compiledElement.find('tr').controller('myTableRow');

/* Angular App */
(function() {
  "use strict";

  angular
    .module('app.components', [])
    .directive('myTableRow', myTableRow);

  function myTableRow() {
    return {
      restrict: 'A',
      require: ['myTableRow', '^^myTable'],
      scope: {
        model: '=myTableRow'
      },
      controller: controller,
      controllerAs: 'vm',
      bindToController: true,
      link: link
    };

    function link($scope, $element, $attrs, $ctrls) {
      var self = $ctrls.shift(),
        tableCtrl = $ctrls.shift();

      self.toggle = function() {
        // keeping it simple for the unit test...
        tableCtrl.selected[0] = self.model;
      };
    }

    function controller() {}
  }

})();

/* Unit Test */
(function() {
  "use strict";

  describe('myTableRow Directive', function() {
    var $compile,
      $scope,
      compiledElement,
      tableCtrl = {},
      controller;

    beforeEach(function() {
      module('app.components');
      inject(function(_$rootScope_, _$compile_) {
        $scope = _$rootScope_.$new();
        $compile = _$compile_;
      });

      tableCtrl.enableMultiSelect = true;
      tableCtrl.selected = [];

      var element = angular.element('<table><tbody><tr my-table-row="data"><td></td></tr></tbody></table>');

      element.data('$myTableController', tableCtrl);
      $scope.data = {
        foo: 'bar'
      };
      compiledElement = $compile(element)($scope);
      $scope.$digest();
      controller = compiledElement.find('tr').controller('myTableRow');
      //console.log(controller); // without the above .find('tr'), this is undefined
    });

    describe('select', function() {
      it('should work', function() {
        controller.toggle();
        expect(tableCtrl.selected.length).toEqual(1);
      });
    });

  });

})();
<link rel="stylesheet" href="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine.css" />
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine.js"></script>
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/jasmine-html.js"></script>
<script src="//cdn.jsdelivr.net/jasmine/2.0.0/boot.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular-mocks.js"></script>
like image 58
JcT Avatar answered Oct 20 '22 17:10

JcT