javascript - Why does my jasmine tests fail on this directive? -
i have built angular directive oninputchange
should fire callback when users changes value of input either clicking outside of input (blur) or hitting enter
. directive can used like:
<input type="number" ng-model="model" on-input-change="callback()"/>
it uses 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 expect in app. i've decided write tests ensure 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 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 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, problem 2 of tests failing after run karma:
a:
failed directive should not fire callback when value has not changed expected spy onchange not have been called.
b:
failed directive should fire callback when user changes input clicking enter expected spy onchange have been called.
i've created plunker can try yourself.
1. why callback gets called if value has not changed?
2. how can simulate user hitting enter
on input? tried different ways none works.
sorry long question. hope able provide enough information maybe can me out on this. thank :)
other questions here on i've read regarding issue:
$parse
returns function, , angular.isfunction(callback)
check unnecessary.
keycode
not translated which
when triggering keyup
manually.
$element.trigger($.event("keyup", {which:13}))
may help.
the callback triggered because focus
can't triggered manually here, , undefined !== 0
in ($(this).val() !== initial
condition.
there couple of reason focus
not work. isn't instant, , spec should become asynchronous. , won't work on detached element.
focus
behaviour can fixed using $element.triggerhandler('focus')
instead of $element.focus()
.
dom testing belongs functional tests, not unit tests, , jquery may introduce lot of surprises when being treated (the spec demonstrates tip of iceberg). when specs green, in vivo behaviour may differ in vitro, renders unit tests useless.
a proper strategy unit-testing directive affects dom expose event handlers scope - or controller, in case of no-scope directive:
require: ['oninputchange', 'ngmodel'], controller: function () { this.onfocus = () => ...; ... }, link: (scope, element, attrs, [instance, ngmodelcontroller]) => { ... }
then controller instance can obtained in specs with
var instance = $element.controller('oninputchange');
all controller methods can tested separately relevant events. , events handling can tested watching on
method calls. in order angular.element.prototype
or jquery.prototype
has spied, that:
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();
the purpose of unit test test unit in isolation other moving parts (including jquery dom actions, purpose ngmodel
can mocked too), that's how done.
unit tests don't make functional tests obsolete, in case of complex multidirective interactions may offer solid testing 100% coverage.
Comments
Post a Comment