I have built an angular directive onInputChange
that should fire a callback when the users changes a value of an input by either clicking outside of the input (blur) or hitting ENTER
. The directive can be used like:
<input type="number" ng-model="model" on-input-change="callback()"/>
It uses the following code:
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);
});
}
};
}
]);
The directive works as I would expect in my app. Now I've decided to write some tests to really ensure this is the case:
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();
});
});
Now, my problem is that two of my tests are failing after run with karma:
A:
Failed directive should not fire callback when value has not changed Expected spy onchange not to have been called.
B:
Failed directive should fire callback when user changes input by clicking enter Expected spy onchange to have been called.
I've created a Plunker where you can try it yourself.
1. Why does my callback gets called even if the value has not changed?
2. How can I simulate the user hitting ENTER
on my input? I already tried different ways but none works.
Sorry for the long question. I hope I was able to provide enough information so that maybe someone can help me out on this. Thank you :)
Other questions here on SO that I've read regarding my issue:
- How do I trigger a keyup/keydown event in an angularjs unit test?
- AngularJS unit test for keypress event