我构建了一个角度指令onInputChange
,当用户通过点击输入外部(模糊)或点击ENTER
来更改输入值时,该指令应触发回调。该指令可以像:
<input type="number" ng-model="model" on-input-change="callback()"/>
它使用以下代码:
app.directive('onInputChange', [
"$parse",
function ($parse) {
return {
restrict : "A",
require : "ngModel",
link : function ($scope, $element, $attrs) {
//
var dirName = "onInputChange",
callback = $parse($attrs[dirName]),
evtNS = "." + dirName,
initial = undefined;
//
if (angular.isFunction(callback)) {
$element
.on("focus" + evtNS, function () {
initial = $(this).val();
})
.on("blur" + evtNS, function () {
if ($(this).val() !== initial) {
$scope.$apply(function () {
callback($scope);
});
}
})
.on("keyup" + evtNS, function ($evt) {
if ($evt.which === 13) {
$(this).blur();
}
});
}
//
$scope.$on("$destroy", function () {
$element.off(evtNS);
});
}
};
}
]);
该指令的工作方式正如我在我的应用中所期望的那样。现在我决定编写一些测试来确保实现这种情况:
describe("directive", function () {
var $compile, $rootScope, $scope, $element;
beforeEach(function () {
angular.mock.module("app");
});
beforeEach(inject(function ($injector) {
$compile = $injector.get("$compile");
$scope = $injector.get("$rootScope").$new();
$scope.model = 0;
$scope.onchange = function () {
console.log("called");
};
$element = $compile("<input type='number' ng-model='model' on-input-change='onchange()'>")($scope);
$scope.$digest();
spyOn($scope, "onchange");
}));
afterEach(function () {
$scope.$destroy();
});
it("has default values", function () {
expect($scope.model).toBe(0);
expect($scope.onchange).not.toHaveBeenCalled();
});
it("should not fire callback on internal model change", function() {
$scope.model = 123;
$scope.$digest();
expect($scope.model).toBe(123);
expect($scope.onchange).not.toHaveBeenCalled();
});
//this fails
it("should not fire callback when value has not changed", function () {
$element.focus();
$element.blur();
$scope.$digest();
expect($scope.model).toBe(0);
expect($scope.onchange).not.toHaveBeenCalled();
});
it("should fire callback when user changes input by clicking away (blur)", function () {
$element.focus();
$element.val(456).change();
$element.blur();
$scope.$digest();
expect($scope.model).toBe(456);
expect($scope.onchange).toHaveBeenCalled();
});
//this fails
it("should fire callback when user changes input by clicking enter", function () {
$element.focus();
$element.val(789).change();
$element.trigger($.Event("keyup", {keyCode:13}));
$scope.$digest();
expect($scope.model).toBe(789);
expect($scope.onchange).toHaveBeenCalled();
});
});
现在,我的问题是我的两个测试在使用karma运行后失败了:
答
失败指令在值未更改时不应触发回调 未被调用的预期间谍改变。
B:
失败指令应在用户通过点击输入更改输入时触发回调 已被调用的预期间谍。
我已经创建了Plunker,您可以自己尝试。
1。为什么即使值没有改变也会调用我的回调?
2。如何模拟用户在我的输入上点击ENTER
?我已经尝试了不同的方法,但都没有。
很抱歉这个问题很长。我希望我能够提供足够的信息,以便有人可以帮助我解决这个问题。谢谢:))
关于我的问题,我在这里读到的其他问题:
答案 0 :(得分:1)
$parse
始终返回一个函数,并且不需要angular.isFunction(callback)
检查。
keyCode
时, which
未转换为keyup
。
$element.trigger($.Event("keyup", {which:13}))
可能会有所帮助。
触发回调是因为focus
无法在此处手动触发,而undefined !== 0
条件实际上是($(this).val() !== initial
。
focus
有两个原因无效。它不是即时的,规范应该变得异步。它不会在分离元素上工作。
focus
代替$element.triggerHandler('focus')
修复 $element.focus()
行为。
DOM测试属于功能测试,而不属于单元测试,jQuery在被这样对待时可能会引入很多惊喜(规范证明了冰山一角)。即使规格是绿色的,体内行为也可能与体外不同,这使得单元测试几乎无用。
对影响DOM的指令进行单元测试的正确策略是在无范围指令的情况下将所有事件处理程序公开到范围 - 或控制器:
require: ['onInputChange', 'ngModel'],
controller: function () {
this.onFocus = () => ...;
...
},
link: (scope, element, attrs, [instance, ngModelController]) => { ... }
然后可以使用
获取控制器实例var instance = $element.controller('onInputChange');
所有控制器方法都可以与相关事件分开测试。并且可以通过观察on
方法调用来测试事件处理。为了做到这一点,angular.element.prototype
or jQuery.prototype
必须被监视,如:
spyOn(angular.element.prototype, 'on').and.callThrough();
spyOn(angular.element.prototype, 'off').and.callThrough();
spyOn(angular.element.prototype, 'val').and.callThrough();
...
$element = $compile(...)($scope);
expect($element.on).toHaveBeenCalledWith('focus.onInputChange', instance.onFocus);
...
instance.onFocus();
expect($element.val).toHaveBeenCalled();
单元测试的目的是单独测试一个单元与其他移动部件(包括jQuery DOM操作,为此目的ngModel
也可以被模拟),以及它是如何完成的。< / p>
单元测试不会使功能测试过时,特别是在复杂的多指向交互的情况下,但可以提供100%覆盖率的可靠测试。