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



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

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

父母

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属性模拟所需控制器的解决方案。

我已经尝试过了,现在正试图测试我的表行指令中的切换函数,以检查它将一个项目添加到父表指令的范围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

毫无疑问,我在这里做错了什么,有人能告诉我吗?

感谢

我已经看过这些帖子了:

在AngularJS中对定义控制器的指令进行单元测试

如何访问控制器名称空间在单元测试与编译元素?

我已经尝试了以下,假设我使用的是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

我遇到了这个post - isolateScope()在测试angular指令时返回undefined

认为它可以帮助我,所以我尝试了下面的

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

但是它仍然返回undefined。compiledElement.children().scope()确实返回了一个大的对象,有很多有角度的$$前缀范围相关的属性,我可以看到我的vm控制器,我试图得到深埋在里面,但不确定这是正确的方法

更新3

我偶然发现了一篇文章,它恰好涵盖了我想要达到的目标。

当我尝试在测试中实现这种方法时,我可以得到子指令的元素,但仍然无法检索它的作用域:

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

我只是想知道这是否取决于我使用链接函数和控制器语法?

您与您发布的原始代码非常接近。我认为您只是在错误的元素上使用.controller('myTableRow'),因为此时您的compiledElement是整个表元素。您需要获得实际的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>

下面是一个使用父子关系来引用angular指令的例子。

注释图像的定义是这样的:(这是父元素)

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

现在对于annotatedImageController, annotatedImageViewer和annotatedimagecurcurrent它们是子元素

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

对于annotatedimageccurrent

也可以这样做

总结

<parent-component>
  <child-component></child-component>
  <another-child-component></another-child-component>
</parent-component>

父组件

 module.directive('parentComponent', function() {
   function ParentComponentController(scope) {
     // initialize scope
   }
   ParentComponentController.prototype.doSomething = function() {
     // does nothing here
   }
   return {
     restrict: 'E',
     controller: ['$scope', ParentComponentController],
     scope: {}
   };
 });

子组件

module.directive('childComponent', function() {
  function link(scope, element, attrs, controller) {
    controller.doSomething();
  }
  return {
    restrict: 'E',
    require: '^parentComponent',
    link: link,
    scope: {}
  }
});

最新更新