在指令测试中模拟所需的控制器

时间:2016-09-20 10:59:53

标签: angularjs unit-testing jasmine

我很难弄清楚如何模仿我所写的指令所需的控制器,这是另一个人的孩子。

首先让我分享一下我的指令:

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 === '';
    }
}

儿童

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() {

    }
}

正如您可能看到的那样,它的一个表行指令预先设置了复选框单元格,并且在切换时用于填充绑定到父表指令范围的所选项目数组。

当谈到单元测试表行指令时,我遇到了可以使用元素上的data属性模拟所需控制器的解决方案。

我试过这个并且现在正在尝试测试我的表行指令中的toggle函数来检查它是否将一个项添加到父表指令的范围selected属性中:

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);
    });
  });
});

但是我收到了一个错误:

  

undefined不是对象(评估'controller.toggle')

如果我在我的测试中注销了controller的值,则显示为undefined

我毫无疑问在我的方法中做错了什么,有人可以开导我吗?

由于

更新

我已经遇到过这些帖子:

Unit testing a directive that defines a controller in AngularJS

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

我尝试了以下内容,因为我使用的是controllerAs语法:

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'));

但控制器仍然在控制台日志中未定义。

更新2

我遇到过这篇文章 - isolateScope() returning undefined when testing angular directive

认为它可以帮助我,所以我尝试了以下代替

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

但它仍然以未定义的形式返回。 compiledElement.children().scope()会返回一个大对象,其中包含大量有角度$$前缀范围相关的属性,我可以看到我想要进入的vm控制器深埋在内部,但不确定这个是正确的方法

更新3

我遇到了这个article,它涵盖了我正在努力实现的那种事情。

当我尝试在我的测试中实现这种方法时,我可以访问子指令的元素,但我仍然无法检索它的范围:

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
});

我只是想知道使用链接函数和controllerAs语法是否归功于我?

2 个答案:

答案 0 :(得分:3)

您与发布的原始代码非常接近。我认为你只是在错误的元素上使用.controller('myTableRow'),因为此时你的compileElement是整个表元素。您需要获取实际的tr子元素才能使myTableRow控制器脱离它。

见下文,具体来说:

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>

答案 1 :(得分:0)

以下是引用使用父子关系使用angular指令的示例。

带注释图像的定义如下所示:(它是父对象)

&#13;
&#13;
angular.module('annotatedimage').directive('annotatedImage', function() {
  function AnnotatedImageController(scope) {}

  return {
    {
      restrict: 'E',
      template: [
        '<annotated-image-controls annotations="configuration.annotations"></annotated-image-controls>',
        '<annotated-image-viewer src="configuration.image" annotations="configuration.annotations"></annotated-image-viewer>',
        '<annotated-image-current></annotated-image-current>'
      ].join('\n'),
      controller: ['$scope', AnnotatedImageController],
      scope: {
        configuration: '='
      }
    }
  };
});
&#13;
&#13;
&#13;

现在为annotatedImageController,annotatedImageViewer和annotatedImageCurrent这些是孩子。

&#13;
&#13;
 angular.module('annotated-image').directive('annotatedImageControls', function() {
   function link(scope, el, attrs, controller) {
     scope.showAnnotations = function() {
       controller.showAnnotations();
     };

     controller.onShowAnnotations(function() {
       scope.viewing = true;
     });
   }

   return {
     restrict: 'E',
     require: '^annotatedImage',
     template: [
       '<div>',
       '<span span[data-role="show annotations"]     ng-click="showAnnotations()" ng-hide="viewing">Show</span>',
       '<span span[data-role="hide annotations"] ng-click="hideAnnotations()" ng-show="viewing">Hide</span>',
       '<span ng-click="showAnnotations()">{{ annotations.length }} Annotations</span>',
       '</div>'
     ].join('\n'),
     link: link,
     scope: {
       annotations: '='
     }
   };
 });
 angular.module('annotated-image').directive('annotatedImageViewer', function() {
   function link(scope, el, attrs, controller) {
     var canvas = el.find('canvas');
     var viewManager = new AnnotatedImage.ViewManager(canvas[0], scope.src);

     controller.onShowAnnotations(function() {
       viewManager.showAnnotations(scope.annotations);
     });
   }

   return {
     restrict: 'E',
     require: '^annotatedImage',
     template: '<canvas></canvas>',
     link: link,
     scope: {
       src: '=',
       annotations: '='
     }
   };
 });
&#13;
&#13;
&#13;

对于annotatedImageCurrent

也可以这样做

摘要

&#13;
&#13;
<parent-component>
  <child-component></child-component>
  <another-child-component></another-child-component>
</parent-component>
&#13;
&#13;
&#13;

父组件

&#13;
&#13;
 module.directive('parentComponent', function() {
   function ParentComponentController(scope) {
     // initialize scope
   }

   ParentComponentController.prototype.doSomething = function() {
     // does nothing here
   }

   return {
     restrict: 'E',
     controller: ['$scope', ParentComponentController],
     scope: {}
   };
 });
&#13;
&#13;
&#13;

子组件

&#13;
&#13;
module.directive('childComponent', function() {
  function link(scope, element, attrs, controller) {
    controller.doSomething();
  }

  return {
    restrict: 'E',
    require: '^parentComponent',
    link: link,
    scope: {}
  }
});
&#13;
&#13;
&#13;