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
Related
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();
});
});
I have written angular js directive one method, but I don't know how to write unit test for that.
var app = angular.module("myApp",[]);
app.directive('minMax', function() {
return {
require: 'ngModel',
link: function(scope, element, attr, mCtrl) {
function myValidation(value) {
if (value.toString().length > 2 & value.toString().length < 6) {
mCtrl.$setValidity('charE', true);
} else {
mCtrl.$setValidity('charE', false);
}
return value;
}
mCtrl.$parsers.push(myValidation);
}
};
});
How do I test this method?
Have a look here: https://github.com/daniellmb/angular-test-patterns.
It contains a great collection of test patterns.
Example of directive test:
describe('Directive: myDir', function () {
var element, scope, compile, defaultData,
validTemplate = '<my-dir ng-model="data"></my-dir>';
function createDirective(data, template) {
var elm;
// Setup scope state
scope.data = data || defaultData;
// Create directive
elm = compile(template || validTemplate)(scope);
// Trigger watchers
//scope.$apply();
// Return
return elm;
}
beforeEach(function () {
// Load the directive's module
module('myApp');
// Reset data each time
defaultData = 42;
// Provide any mocks needed
module(function ($provide) {
//$provide.value('Name', new MockName());
});
// Inject in angular constructs otherwise,
// you would need to inject these into each test
inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
compile = $compile;
});
});
describe('when created', function () {
// Add specs
});
describe('when the model changes', function () {
// Add specs
});
describe('when destroyed', function () {
// Add specs
});
});
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();
}));
I have the following directive.
directivesModule.directive('wikis', function() {
var urlRegex = new RegExp('^(https?)://.+$');
return {
restrict: 'E',
templateUrl: 'templates/wiki-list.html',
scope: {
wikis: '='
},
link: function(scope, element, attrs) {
scope.newWikiURL = '';
scope.$watch('wikis', function() {
if (scope.wikis == undefined || scope.wikis.length === 0) {
scope.class = 'hide';
} else {
scope.class = 'show';
}
}, false);
scope.addWiki = function() {
if (scope.wikis == undefined) {
scope.wikis = [];
}
var nw = scope.newWikiURL;
if (nw != undefined && nw.trim() != '' && urlRegex.exec(nw)) {
scope.wikis.push(nw);
scope.newWikiURL = '';
}
}
}
};
});
When I test it:
describe('Wikis Directive Test Suite', function() {
var scope, elem, directive, linkFn, html;
beforeEach(function() {
html = '<wikis wikis=''></wikis>';
inject(function($compile, $rootScope) {
scope = $rootScope.$new();
scope.wikis = [];
elem = angular.element(html);
$compile(elem)(scope);
scope.$digest();
});
});
it('add Wiki should add a valid wiki URL to artist', function() {
var url = 'http://www.foo.com';
scope.newWikiURL = url;
scope.addWiki();
expect(scope.wikis.length).toBe(1);
expect(scope.wikis[0]).toBe(url);
expect(scope.newWikiURL).toBe('');
});
});
I get an error saying that Object doesn't have an addWiki method. I tried to debug it, and the link function is not called during the test. I suspect that's why the addWiki method is not part of the scope. Anybody knows why I'm getting this error?
By the way, Is it a normal practice to add some logic into the link function of a directive as it would be a Controller itself? Because looking at the code I know that it's why in reality I'm doing.
As per angular 1.2.0 docs, the way to get the isolate scope is through the method isolateScope
scope() - retrieves the scope of the current element or its parent.
isolateScope() - retrieves an isolate scope if one is attached directly to the current element. This getter should be used only on elements that contain a directive which starts a new isolate scope. Calling scope() on this element always returns the original non-isolate scope.
Angular doc - section jQuery/jqLite Extras
BREAKING CHANGE: jqLite#scope()
You need to load the module containing your directive, otherwise angular doesn't know what <wikis> is
Your directive creates an isolate scope, so once it has been compiled you need to get the new scope using elem.isolateScope()
So with those changes:
describe('Wikis Directive Test Suite', function() {
var $scope, scope, elem, directive, linkFn, html;
beforeEach(module('app'));
beforeEach(function() {
html = '<wikis></wikis>';
inject(function($compile, $rootScope, $templateCache) {
$templateCache.put('templates/wiki-list.html', '<div>wiki template</div>');
$scope = $rootScope.$new();
$scope.wikis = [];
elem = angular.element(html);
$compile(elem)($scope);
scope = elem.isolateScope();
scope.$apply();
});
});
it('add Wiki should add a valid wiki URL to artist', function() {
var url = 'http://www.foo.com';
scope.newWikiURL = url;
scope.addWiki();
expect(scope.wikis.length).toBe(1);
expect(scope.wikis[0]).toBe(url);
expect(scope.newWikiURL).toBe('');
});
});
http://jsfiddle.net/QGmCF/1/
describe("create a simple directive", function () {
var simpleModule = angular.module("directivesSample", []);
simpleModule.controller('Ctrl2', function ($scope) {
$scope.format = 'M/d/yy h:mm:ss a';
});
simpleModule.directive("myCurrentTime", function ($timeout, dateFilter) {
return function (scope, element, attr) {
var format;
var timeoutId;
function updateTime() {
element.text(dateFilter(new Date(), format));
}
scope.$watch(attr.myCurrentTime, function (value) {
format = value;
updateTime();
});
function updateLater() {
timeoutId = $timeout(function () {
updateTime();
updateLater();
}, 1000);
}
element.bind('$destroy', function () {
$timeout.cancel(timeoutId);
});
updateLater();
}
});
beforeEach(module('directivesSample'));
var element = angular.element(
'<div ng-controller="Ctrl2">Date format:<input ng-model="format"> ' +
'<hr/>Current time is: ' +
'<span class="timeout" my-current-time="format" id="timeout-render"></span>' +
'</div>');
var directiveScope;
var scope;
var linkedElement;
var linkFunction;
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
linkFunction = $compile(element);
linkedElement = linkFunction(scope);
scope.$apply();
}));
it("should define element time out", function () {
var angularElement = element.find('span'); // <-- element is not returned if set to var angularElement = element.find('.timeout'); or var angularElement = element.find('#timeout-render');
console.log(angularElement.text());
expect(angularElement.text()).not.toBe('');
})
});
Having the above test, why am I unable to search for element by JQuery selector ? I am aware of the limitations in the documentation of find() method.But, I have checked out the angularUI project inspected the the usage of find() function as in here
var tt = angular.element(elm.find("li > span")[0]);
and find out that the guys are using find to search element by jQuery elector not only tag name while I am not able to do so. Am I missing something ?
That's because the built in jqLite in Angular has only limited support for CSS selectors. But if you include jQuery in a script tag before you include Angular, Angular will see that and use jQuery instead of its jqLite for calls to angular.element().