【问题标题】:Unit test Angular directive that accesses external element访问外部元素的单元测试 Angular 指令
【发布时间】:2016-03-15 10:16:08
【问题描述】:

我有一个自定义指令,它使用一个属性来指定它修改的另一个控件。

指令定义对象:

{
    restrict: 'E',
    templateUrl: 'myTemplate.html',
    scope: {
        targetId: '@'
    },
    controller: MyController,
    controllerAs: 'vm',
    bindToController: true
}

指令控制器上的函数修改目标元素(输入字段)的内容:

function onSelection (value) {
    var $element = $('#' + vm.targetId);

    $element.val('calculated stuff');
    $element.trigger('input');
}

单元测试(Jasmine/Karma/PhantomJS)当前将元素附加到页面。这行得通,但它似乎是代码异味。

beforeEach(inject(function($rootScope, $compile) {
    var elementHtml = '<my-directive target-id="bar"></my-directive>' +
        '<input type="text" id="bar">';

    scope = $rootScope.$new();    
    angularElement = angular.element(elementHtml);
    angularElement.appendTo(document.body);  // HELP ME KILL THIS!
    element = $compile(angularElement)(scope);
    scope.$digest();
}));

afterEach(function () {
    angularElement.remove();  // HELP ME KILL THIS!
});

我已经尝试重写控制器函数来避免使用 jQuery;这没有帮助。

如何修改指令或测试以消除 appendTo/remove?

【问题讨论】:

  • 在指令中使用 id 会产生更大的代码气味。它是非常 jQuery 风格,而不是“Angular 方式”。考虑在 DOM 树的同一分支上的指令中使用 require,或者“公共父级”或“全局服务”方法(如果您不知道该怎么做,您可以搜索或询问另一个问题 - 解释肯定已经出来了这个问题的范围)
  • @ValentynShybanov 我对涉及修改指令的解决方案持开放态度。该指令的实际目的是在输入或文本区域字段中插入邮件合并占位符(它不应该是指令模板的一部分,我宁愿避免嵌入)。如果您想发布推荐方法的示例或指向此的链接,我会考虑接受它作为答案。
  • 我认为如果不重构 onSelection() 函数,您将无法从代码中删除添加/删除元素。您是否有任何理由无法将ng-model 绑定到您的#bar 输入?如果你能做到这一点,那么你可以在控制器中设置值,而无需使用 jQuery 抓取元素。
  • @jperezov 问题是指令需要目标元素中的当前选择和/或光标位置,而不仅仅是绑定文本。它在那个地方插入一个选定的值。

标签: angularjs unit-testing angularjs-directive


【解决方案1】:

最好的办法是将指令迁移到属性而不是元素。这消除了对target-id 属性的需要,并且您无需寻找目标元素。

http://jsfiddle.net/morloch/621rp33L/

指令

angular.module('testApp', [])
  .directive('myDirective', function() {
    var targetElement;
    function MyController() {
      var vm = this;
      vm.onSelection = function() {
        targetElement.val('calculated stuff');
        targetElement.trigger('input');
      }
    }
    return {
      template: '<div></div>',
      restrict: 'A',
      scope: {
        targetId: '@'
      },
      link: function postLink(scope, element, attrs) {
        targetElement = element;
      },
      controller: MyController,
      controllerAs: 'vm',
      bindToController: true
    };
  });

测试

describe('Directive: myDirective', function() {
  // load the directive's module
  beforeEach(module('testApp'));

  var element, controller, scope;

  beforeEach(inject(function($rootScope, $compile) {
    scope = $rootScope.$new();
    element = angular.element('<input my-directive type="text" id="bar">');
    $compile(element)(scope);
    scope.$digest();
    controller = element.controller('myDirective');
  }));

  it('should have an empty val', inject(function() {
    expect(element.val()).toBe('');
  }));

  it('should have a calculated val after select', inject(function() {
    controller.onSelection();
    expect(element.val()).toBe('calculated stuff');
  }));
});

【讨论】:

  • 谢谢 - 这绝对是一种方法。一个缺点是更难说“我想要页面上的指令控件”和“我希望它影响页面上这个位置的控件”。
  • 那么另一个选项(因为您需要直接访问输入标签)是使用两个指令,一个作为控件(用户进行选择)和另一个(在输入上)应用结果,使用信号来传达已发生的操作。
【解决方案2】:

还有一个建议可以使您的逻辑几乎完全相同:使用第二个指令使目标 element 在控制器上可用,然后您可以将其传递给您的主要指令进行处理:http://jsfiddle.net/morloch/p8r2Lz1L/

getElement

  .directive('getElement', function() {
    return {
      restrict: 'A',
      scope: {
        getElement: '='
      },
      link: function postLink(scope, element, attrs) {
        scope.getElement = element;
      }
    };
  })

我的指令

  .directive('myDirective', function() {
    function MyController() {
      var vm = this;
      vm.onSelection = function() {
        vm.targetElement.val('calculated stuff');
        vm.targetElement.trigger('input');
      }
    }
    return {
      template: '<div></div>',
      restrict: 'E',
      scope: {
        targetElement: '='
      },
      controller: MyController,
      controllerAs: 'vm',
      bindToController: true
    };
  })

测试

describe('Directive: myDirective', function() {
  // load the directive's module
  beforeEach(module('testApp'));

  var element, controller, scope;

  beforeEach(inject(function($rootScope, $compile) {
    scope = $rootScope.$new();
    element = angular.element('<input get-element="elementBar" type="text" id="bar"><my-directive target-element="elementBar"></my-directive>');
    $compile(element)(scope);
    scope.$digest();
    controller = $(element[1]).controller('myDirective');
  }));

  it('should have an empty val', inject(function() {
    expect($(element[0]).val()).toBe('');
  }));

  it('should have a calculated val after select', inject(function() {
    controller.onSelection();
    expect($(element[0]).val()).toBe('calculated stuff');
  }));
});

【讨论】:

    猜你喜欢
    • 2013-10-18
    • 2015-01-18
    • 1970-01-01
    • 2012-11-10
    • 1970-01-01
    • 2013-10-26
    • 1970-01-01
    • 2014-08-06
    • 1970-01-01
    相关资源
    最近更新 更多