Unit test angular directive that uses ngModel - angularjs

I am trying to unit test a directive that uses ngModel and having difficulties. It seems that the link function of my directive is never being called...
Here is my directive code:
coreModule.directive('coreUnit', ['$timeout', function ($timeout) {
return {
restrict: 'E',
require: '?ngModel',
template: "{{output}}",
link: function (scope, elem, attrs, ngModelCtrl) {
ngModelCtrl.$render = function () {
render(ngModelCtrl.$modelValue);
};
console.log("called");
function render(unit) {
if (unit) {
var output = '(' +
unit.numerator +
(unit.denominator == '' ? '' : '/') +
unit.denominator +
(unit.rate == 'NONE' || unit.rate == '' ? '' : '/' + unit.rate) +
')';
scope.output = output == '()' ? '' : output;
}
}
}
}
}]);
Here is my test spec:
describe('core', function () {
describe('coreUnitDirective', function () {
beforeEach(module('core'));
var scope,
elem;
var tpl = '<core-unit ng-model="myUnit"></core-unit>';
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
scope.myUnit = {};
elem = $compile(tpl)(scope);
scope.$digest();
}));
it('the unit should be empty', function () {
expect(elem.html()).toBe('');
});
it('should show (boe)', function () {
scope.myUnit = {
numerator: 'boe',
denominator: "",
rate: ""
};
scope.$digest();
expect(elem.html()).toContain('(boe)');
});
});
});
The console log output "called" is never occurring and obviously the elem in my test spec is never updating.
What am I doing wrong??

Turns out that I wasn't including the directive in my karma.config file :S. Adding it in resolved all of my issues.

You can try out two things.
First, instead of using just a string tpl, try angular.element().
var tpl = angular.element('<core-unit ng-model="myUnit"></core-unit>');
Second, place the tpl in the beforeEach block. So the result should look like this:
beforeEach(inject(function ($rootScope, $compile) {
var tpl = angular.element('<core-unit ng-model="myUnit"></core-unit>');
scope = $rootScope.$new();
scope.myUnit = {};
elem = $compile(tpl)(scope);
scope.$digest();
}));

Related

unit test case for handler in ng-change in file input angularjs

I used a directive to add a listener function on input file change, I'm going to create a unit test case for this in jasmine, but I'm not able to trigger function in unit case.
unit test code:
describe('customOnChange', function () {
beforeEach(module('app'));
beforeEach(inject(function ($rootScope, $compile, $parse) {
this.$rootScope = $rootScope;
this.compile = $compile;
this.parse = $parse;
this.initDirective = function (config) {
this.scope = this.$rootScope.$new();
this.scope.listnerFunction = function($event){
console.log("event called : ",$event);
};
this.element = angular.element('<input type="text" name="textField" id="textField" dynamic-data-editor-custom-on-change="listnerFunction($event)" ng-model="testText"/>');
this.compile(this.element)(this.scope);
this.$rootScope.$digest();
$('body').append(this.element);
this.$rootScope.$digest();
};
}));
fit('Applied directive successfully', function() {
this.initDirective();
spyOn(this.scope,'listnerFunction');
var ele = this.element.find("input#textField").eq(0);
console.log('element', ele);
ele.triggerHandler('change');
this.$rootScope.$apply();
expect(this.scope.listnerFunction).toHaveBeenCalled();
});
});
The main directive code:
function () {
'use strict';
angular.module('app')
.directive('customOnChange', function ($parse) {
return {
restrict: 'A',
link: function ($scope, element, attrs, ngModel) {
var attrHandler = $parse(attrs.dynamicDataEditorCustomOnChange);
var handler = function (e) {
$scope.$apply(function () {
attrHandler($scope, { $event: e });
});
};
element[0].addEventListener('change', handler, false);
}
};
});
})();
I got the error on running test
Expected spy listnerFunction to have been called.
Please help me to resolve the test case for the directive.
In your test the problem is: spy is created after the method listnerFunction function is called, so you see the error.
You should replace:
this.scope.listnerFunction = function($event){
console.log("event called : ",$event);
};
with (andCallFake for jasmine < 2)
this.scope.listnerFunction =
jasmine.createSpy("listnerFunction")
.and.callfake(function($event){
console.log("event called : ",$event);
});

karma testing failing in directive

I have the following directive. This Directive will trigger after animation end triggers. I now just want to add test for this directive. I have created the test case for this. But it's failing. Can anybody help on this? This is plunker
angular.module('movieApp')
.directive('animationend', function() {
return {
restrict: 'A',
scope: {
animationend: '&'
},
link: function(scope, element) {
var callback = scope.animationend(),
events = 'animationend webkitAnimationEnd MSAnimationEnd' +
'transitionend webkitTransitionEnd';
element.on(events, function(event) {
console.log('[animationend directive] callback');
callback.call(element[0], event);
});
}
};
});
And my test case is
describe('Directive: animationend', function () {
// load the directive's module
beforeEach(module('movieApp'));
var elements,
scope,
compile;
beforeEach(inject(function ($rootScope, _$compile_) {
scope = $rootScope.$new();
compile = _$compile_;
elements = angular.element('<div animationend="" ></div>');
elements = compile(elements)(scope);
scope.$digest();
}));
it('should trigger the callback once animationclass is added ', function () {
scope.ctrl = {
animationend: jasmine.createSpy('animationend')
};
scope.$digest();
var events = 'animationend webkitAnimationEnd MSAnimationEnd' +
'transitionend webkitTransitionEnd';
angular.element(elements).triggerHandler(events);
// element.trigger(events);
expect(scope.ctrl.animationend).toHaveBeenCalled();
});
});

Angular Karma Directive Test Case not compiling html

Below is my directive--
define(['jquery', 'angular'],function($, angular){
var publishToolSummaryDirective = angular.module('sdm.publishTool.directives').directive('krnPublishToolSummary',
function($compile){
return {
restrict: 'EA',
templateUrl: "apps/sdm/pages/partials/publishToolSummary.html",
link : function(scope, element){
var elem =$(element);
scope.previousComment = "";
scope.displayPublishingCommentText = false;
scope.displayCommentBox = function(event){
event.preventDefault();
scope.displayPublishingCommentText = true;
};
scope.displayCommentLink = function(event){
if(event.target.textContent === "cancel"){
scope.publishingCommentText = scope.previousComment;
}else{
scope.previousComment = scope.publishingCommentText;
}
scope.displayPublishingCommentText = false;
}
}
}
});
return publishToolSummaryDirective;
})
Below is my Unit Test--
define([
'apps/sdm/app',
'angular',
'jquery',
'angular-mocks'
], function ($, angular) {
describe("Unit testing Summary directive", function() {
angular.mock.module('sdm.publishTool.directives');
var compile,scope, q,url,elm;
beforeEach(inject(["$templateCache", "$compile","$rootScope", "$q","$httpBackend",function($templateCache, $compile, $rootScope, $q, $httpBackend) {
q = $q;
compile = $compile;
scope = $rootScope.$new();
var html='<krn-publish-tool-summary></krn-publish-tool-summary>';
elm = compile(html)(scope);
scope.$digest();
console.log(elm);
}]));
it("should compile", function(){
expect(elm.find('button').length).toEqual(3);
expect(elm.className).toContain('publishTool-summary');
});
})
})
It is not building my templateURL mentioned in the directive.
console o/p is
Object{0: <krn-publish-tool-summary class="ng-scope"></krn-publish-tool-summary>, length: 1}
Coverage report shows that it is not even going inside the directive 2nd line.
Any help please!
The html variable is not an angular element so it is not giving desired result.
Try changing
var html='<krn-publish-tool-summary></krn-publish-tool-summary>';
to
var html=angular.element('<krn-publish-tool-summary></krn-publish-tool-summary>');

AngularJs Unit Testing isolated scope Directive

I want to test the following directive "spinInput" which requires ngModel, but I can't access the directive's scope. All I get is an empty scope. Angularjs 1.3.13 is used.
Directive:
angular.module('charts.spinInput',[]).directive('spinInput', function() {
return {
restrict: 'E',
replace: true,
require:'ngModel',
scope: {
min:"#"
},
controller:function($scope)
{
$scope.test=12;
$scope.calcAngle=function(point)
{
var xDif=point.x-50;
var yDif=point.y-50;
return (Math.atan2(yDif, xDif) * 180 / Math.PI);
};
},
templateUrl: 'charts/directive/spinInput/spinInput.html',
link:function(scope, element, attr,ngModel) {
...
}
};
});
Unit Test:
throws following error: TypeError: 'undefined' is not an object (evaluating 'innerScope.min')
describe('spinInput', function() {
beforeEach(module('charts'));
var $httpBackend;
var element;
var outerScope;
var innerScope;
beforeEach(inject(function($rootScope, $compile , $injector) {
element = angular.element('<spin-input min="12"></spin-input>');
$httpBackend = $injector.get('$httpBackend');
$httpBackend.whenGET('charts/directive/spinInput/spinInput.html').respond(200, '');
outerScope = $rootScope.$new();
$compile(element)(outerScope);
innerScope = element.isolateScope();
outerScope.$digest();
}));
it('scope.min should be defined', function() {
expect(innerScope.min).toBeDefined();
});
});
The way you are constructing your test seems to be causing issues.
I have been able to successfully test the isolated scope as per below.
You can view the test running on this jsfiddle (with templates code commented out).
describe('Directive: spinInput', function() {
var scope, compile, validHTML, templateHtml;;
validHTML = '<spin-input min="12"></spin-input>';
beforeEach(module('myApp'));
beforeEach(inject(function($compile, $rootScope, $templateCache){
templateHtml = $templateCache.get('charts/directive/spinInput/spinInput.html');
if(!templateHtml) {
templateHtml = $.ajax('charts/directive/spinInput/spinInput.html', {async: false}).responseText;
$templateCache.put('charts/directive/spinInput/spinInput.html', templateHtml)
}
scope = $rootScope.$new();
compile = $compile;
}));
function create() {
var elem, compiledElem;
elem = angular.element(validHTML);
compiledElem = compile(elem)(scope);
scope.$digest();
return compiledElem;
}
it('scope.min should be defined', function() {
var el = create();
expect(el.isolateScope().min).toBeDefined();
});
it('scope.min should equal 12', function() {
var el = create();
expect(el.isolateScope().min).toEqual('12');
});
});
Try putting the outerScope.$digest() before element.isolateScope()

Testing AngularJS directive that modifies other scope in Karma / Jasmine

Reference: Unit Testing AngularJS Directives: scopes not updating?
Case
I have a directive called editable that take an ng-model and creates a toggleable/editable field. The directive works and the parent scope is updated correctly, there are no problems in the actual function of the directive. I cannot seem to write a test that supports this though. It took me a long time to get the directive working properly with all the caveats so I really want to get some tests in place to make sure it continues to work in all of the different cases.
The Directive (which works)
I can't be sure which pieces will be relevant so i included the whole thing.
app.directive('editable',
['$templateCache', '$compile',
function ($templateCache, $compile) {
return {
restrict: 'A',
transclude: true,
templateUrl: 'template/directives/editable.html',
replace: true,
require: 'ngModel',
scope: true,
compile: function(element, attrs, transcludeFn) {
return function (scope, iElement, iAttrs, ctrl) {
var validityId = 'editable-' + scope.$id;
scope.lastSavedValue = iElement.find('input').val();
scope.storeValue = function() {
scope.lastSavedValue = iElement.find('input').val();
};
scope.edit = function() {
scope.storeValue();
scope.editing = true;
$('input', iElement).focus().select();
ctrl.$setValidity(validityId, true);
};
scope.ok = function() {
var inputCtrl = iElement.find('input').controller('ngModel');
if(inputCtrl.$valid === true) {
scope.editing = false;
scope.value = inputCtrl.$viewValue;
ctrl.$setValidity(validityId, false);
ctrl.$setViewValue(inputCtrl.$viewValue); // Not sure (why) this is needed
}
};
scope['delete'] = function() {
scope.deleted = true;
scope.editing = false;
};
scope.undo = function() {
var inputCtrl = iElement.find('input').controller('ngModel');
if(scope.lastSavedValue) {
inputCtrl.$setViewValue(scope.lastSavedValue);
scope.value = scope.lastSavedValue;
ctrl.$setViewValue(scope.lastSavedValue);
}
iElement.find('input').val(scope.value);
scope.editing = false;
scope.deleted = false;
ctrl.$setValidity(validityId, false);
};
transcludeFn(scope, function(clone) {
var $editingReplacement = $(clone).filter('[editing]');
var $viewingReplacement = $(clone).filter('[viewing]');
var $deletedReplacement = $(clone).filter('[deleted]');
var $viewingControls = $('.editable-view-container .controls', iElement);
var $editingControls = $('.editable-input-container .controls', iElement);
var $deletedControls = $('.editable-delete-container .controls', iElement);
if($editingReplacement.length) {
$('.editable-input-container', iElement).html($editingReplacement.html());
$('.editable-input-container', iElement).append($editingControls);
$compile($('.editable-input-container', iElement))(scope);
} else {
$('.editable-input-container', iElement).find('input').attr('ng-model', iAttrs['ngModel']);
$compile($('.editable-input-container', iElement))(scope);
}
if($viewingReplacement.length) {
$('.editable-view-container', iElement).html($viewingReplacement.html());
$('.editable-view-container', iElement).append($viewingControls);
$compile($('.editable-view-container', iElement))(scope);
}
if($deletedReplacement.length) {
$('.editable-delete-container', iElement).html($deletedReplacement.html());
$('.editable-delete-container', iElement).append($deletedControls);
}
});
/**
* Deleted (Isolated Scope)
*
* Tracks if the user has clicked the delete button
*
* #type {Boolean}
*/
scope.deleted = false;
/**
* Editing (Isolated Scope)
*
* Tracks the state of the view
*
* #type {Boolean}
*/
scope.editing = false;
/**
* Initial Loader
*
* Run once after ctrl is loaded
*
* #return {[type]} [description]
*/
var unbindWatcher = scope.$watch(function() { return ctrl.$modelValue; }, function(newVal, oldVal) {
if(typeof ctrl.$modelValue !== 'undefined') {
scope.value = ctrl.$modelValue;
scope.editing = ctrl.$modelValue ? false : true;
unbindWatcher();
}
});
};
}
};
}
]);
Spec
Fails at the end
describe('Editable Directive', function() {
// Keep references to element and scope so that they are available to all tests
var element, scope, ctrl;
beforeEach(module('ltAccountApp'));
beforeEach(module('templates'));
beforeEach(inject(function ($rootScope, $compile) {
var linkFn, el;
// scope = $rootScope;
scope = $rootScope.$new();
scope.testValue = 'xxx';
el = angular.element('\
<div editable="" ng-model="testValue"></div>\
');
// The $compile method returns the directive's link function
linkFn = $compile(el);
// The link function returns the resulting DOM object
element = linkFn(scope);
element.scope().$apply();
ctrl = element.controller('ngModel');
}));
it('should assign input value to scope value', function() {
expect(element.find('input').val()).toEqual(scope.testValue);
});
it('should have access to parent scope variable passed into directive', function() {
expect(ctrl.$viewValue).toEqual(scope.testValue);
expect(ctrl.$modelValue).toEqual(scope.testValue);
});
it('should manage state editing correctly', function() {
expect(element.scope().editing).toBe(false);
element.scope().edit();
expect(element.scope().editing).toBe(true);
});
it('should manage state deleted correctly', function() {
expect(element.scope().deleted).toBe(false);
element.scope()['delete']();
expect(element.scope().deleted).toBe(true);
});
it('should be able to modify parent scope variable passed into directive', function() {
// Not sure what this does, added from referenced SO question
// spyOn(scope, '$apply').andCallThrough();
var newValue = 'yyy';
element.scope().edit();
element.find('input').val(newValue);
element.find('input').trigger('input');
element.scope().ok();
expect(ctrl.$viewValue).toEqual(newValue);
expect(ctrl.$modelValue).toEqual(newValue);
expect(element.scope().value).toEqual(newValue);
expect(scope.$apply).toHaveBeenCalled();
expect(scope.testValue).toEqual(newValue); // <-fails
});
});
So...
Everything seems to be working until I actually expect the parent scope to have the changed value.
I know there is a lot here, I appreciate any guidance you can provide.
Plunk of Angular + Jasmine with directive (work in progress)
Lets say you have to update the value of input field to 'kasrak'. Trying doing it like this:
var elm = element.find("input");
elm.val('kasrak');
elm.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
$scope.$digest()
Key is the third line with $sniffer service. I found this in angular-ui's bootstrap tests.
Here is the link to the function in test spec: https://github.com/angular-ui/bootstrap/blob/master/src/typeahead/test/typeahead.spec.js#L31

Resources