I would like to start doing unit testing for my angularjs project. That's far from being straight forward, I find it really difficult. I'm using Karma and Jasmine. For testing my routes and the app dependencies, I'm fine. But how would you test a directive like this one ?
angular.module('person.directives', []).
directive("person", function() {
return {
restrict: "E",
templateUrl: "person/views/person.html",
replace: true,
scope: {
myPerson: '='
},
link: function (scope, element, attrs) {
}
}
});
How would I test for instance that the template was found ?
Here is the way to go https://github.com/vojtajina/ng-directive-testing
Basically, you use beforeEach to create, compile and expose an element and it's scope, then you simulate scope changes and event handlers and see if the code reacts and update elements and scope appropriately. Here is a pretty simple example.
Assume this:
scope: {
myPerson: '='
},
link: function(scope, element, attr) {
element.bind('click', function() {console.log('testes');
scope.$apply('myPerson = "clicked"');
});
}
We expect that when user clicks the element with the directive, myPerson property becomes clicked. This is the behavior we need to test. So we expose the compiled directive (bound to an element) to all specs:
var elm, $scope;
beforeEach(module('myModule'));
beforeEach(inject(function($rootScope, $compile) {
$scope = $rootScope.$new();
elm = angular.element('<div t my-person="outsideModel"></div>');
$compile(elm)($scope);
}));
Then you just assert that:
it('should say hallo to the World', function() {
expect($scope.outsideModel).toBeUndefined(); // scope starts undefined
elm.click(); // but on click
expect($scope.outsideModel).toBe('clicked'); // it become clicked
});
Plnker here. You need jQuery to this test, to simulate click().
Related
I have defined a custom click directive as below:
(function() {
'use strict';
angular.module('myApp.core')
.directive('customClick', customClick);
customClick.$inject = ['$rootScope'];
function customClick() {
return {
restrict: 'A',
/*scope: {
customClick: '&'
},*/
link: function(scope, element, attrs){
$(element).on('click', function(e) {
scope.$apply(function() {
console.log("Hello..customClick..");
scope.customClick();
});
});
}
};
}
})();
And I get the following error on this;
Error logged by WDPR Angular Error handler service {xx.."stacktrace":"TypeError: a.customClick is not a function","cause":"unknown cause"}(anonymous function)bowerComponents.js:5745
How can I resolve this? If I add scope with '&' I get demanding isolated scope. Hence how to resolve it?
If I remove - scope.customClick();, it does not show anything on second html for custom-click, it has impact on only 1 html, and its controller. I want to use it in multiple controller + html.
customClick is a function on the directive itself. It is not a function on the scope. That's why the error has occurred.
link is used to manipulate dom/add event handlers on elements, which you have rightly done with element.bind('click', function() {
Whenever click occurs, the function binded to click automatically gets invoked. There is no need of watch and the invoking statement.
Your link can just be
link: function(scope, element){
element.bind('click', function() {
console.log("Hello..customClick..");
});
}
As you have used camel case in naming the directive, exercise caution in its usage in template.
You can use it as <p custom-click>hi</p>
I would recommend you to avoid using jQuery in angular apps. Try following
angular.module('newEngagementUI.core')
.directive('customClick', customClick);
customClick.$inject = ['$rootScope'];
function customClick() {
return {
restrict: 'A',
scope: {
customClick: '&'
},
link: function(scope, element, attrs){
element.bind('click', function () {
scope.customClick();
})
}
};
}
In your template:
<div custom-click="clickFunction"></div>
And your template controller should be like:
angular.module('myApp', []).controller(['$scope', function ($scope) {
$scope.clickFunction = function () {
alert('function passed');
}
}])
Working fiddle here: https://jsfiddle.net/xSaber/sbqavcnb/1/
I have my directive and I want to change the controller and html of a small section of the page onclick of a submit button. Changeing the HTML is working but changing the controller is not.
The line: attrs.ngController = "wordlistsPageController";
is not working. Please let me know what I can do to dynamically set the controller since this does not work.
It is a little web app game, so I do not want to change the whole page, just the game section between 3 game pages.
Here is my directive:
myApp.directive('gamePanel', function ($compile) {
return {
restrict: 'E',
templateUrl: "templates/wordlistPage.ejs",
link : function(scope, elem, attrs) {
attrs.ngController = "wordlistsPageController";//NOT WORKING
scope.submitWordlist = function() {
//change html
elem.html('<ng-include src="\'templates/gameOptionsDialogPage.ejs\'"></ng-include>');
$compile(elem.contents())(scope);
//change controller
attrs.ngController = "optionsPageController";///NOT WORKING
};
scope.backToWordlist = function() {
elem.html('<ng-include src="\'templates/wordlistPage.ejs\'"></ng-include>');
$compile(elem.contents())(scope);
attrs.ngController = "wordlistsPageController";///NOT WORKING
};
}//end link
}
});//end directive
How to Dynamically Change a Directive Controller
To instantiate different controllers inside a directive linking function, use the $controller service.
angular.module("myApp").directive('gamePanel', function ($controller) {
return {
restrict: 'E',
template: "<p>Game Panel Template</p>",
//controller: "pageController as vm",
link : function(scope, elem, attrs, ctrl, transclude) {
var locals = { $scope: scope,
$element: elem,
$attrs: attrs,
$transclude, transclude
};
scope.vm = $controller("pageController", locals);
}//end link
}
});
The above example instantiates a controller and puts it on scope as vm.
Best practice
The injected locals should follow the conventions of the locals provided by the $compile service. For more information on $compile service injected locals, see AngularJS $compile service API Reference API -- controller.
Beware: memory leaks
When destroying scopes, all watchers created on those scopes should be de-registered. This can be done by invoking scope.$destroy().
When destroying controllers, all watchers created by the controller should be de-registered. This can be done by invoking the de-register function returned by each $watch.
I'm trying to understand how the directives works and how to test the directives with jasmine. I cant test link function in directive. What I am doing wrong?
it('should add class', function(){
//digest the $rootScope
$scope.$digest();
expect(element.hasClass('test')).toBe(true);
});
});
I am trying to write a test that ensures that the element has an added class.
http://plnkr.co/edit/4FgL3uHW8oTuU1GdGCPh?p=preview
I'm receiving error element.addClass it not a function.
You have an error in your directive link function. The first argument is the scope. The element comes the second:
.directive('myDirective', [function() {
return {
restrict: 'E',
template: '<i>',
replace: true,
link: function(scope, element, attrs) {
element.addClass('test');
}
}
}])
Demo: http://plnkr.co/edit/u6u3SYCZJ4rgQSVD85xT?p=preview
I have made a directive which uses ngModel:
.directive('datetimepicker', function () {
return {
restrict: 'E',
require: ['datetimepicker', '?^ngModel'],
controller: 'DateTimePickerController',
replace: true,
templateUrl: ...,
scope: {
model: '=ngModel'
},
link: function (scope, element, attributes, controllers) {
var pickerController = controllers[0];
var modelController = controllers[1];
if (modelController) {
pickerController.init(modelController);
}
}
}
});
But when testing...
var scope, element;
beforeEach(module('appDateTimePicker'));
beforeEach(module('templates'));
beforeEach(inject(function ($compile, $rootScope) {
compile = $compile;
scope = $rootScope;
scope.model = new Date();
element = compile(angular.element('<datetimepicker ng-model="model"></datetimepicker>'))(scope);
scope.$digest();
}));
I can't anyhow set value to ng-model.
Fo example, here scope.model is a date, so scope.year and scope.month should be date and year of that model, but it is undefined.
As seen in the directive's code, I'm using this.init on the controller to initialise all the process.
What am I missing?
EDIT
Example of test:
it('should test', function () {
expect(scope.model).toBe(undefined);
expect(scope.year).toBe(undefined);
});
EDIT
This helped to solve the problem: http://jsfiddle.net/pTv49/3/
The '?^ngModel' mean you are asking for the ng-model on parent elements, but the html in your test has the ng-model on the same element as datetimepicker directive.
If the ng-model really have to be on parent elements, you have to change the html in the test, for example:
element = compile(angular.element('<div ng-model="model"><datetimepicker></datetimepicker></div>'))(scope);
But if it should be on the same element, just remove the ^ symbol in the require:
require: ['datetimepicker', '?ngModel'],
The directive has a scope: {} block so it creates an isolate scope. I assume, in the tests scope refers to the outer scope, while element.isolateScope() should be used to reference the inner scope instead.
I've got a directive which takes part of its template (set via templateUrl) and moves it into $rootElement (so this part becomes the last child of it). And it's defined like this:
.directive(...
return {
restrict: "E",
templateUrl: templUrl,
controller: "MyCtrl",
scope: {
},
compile: function (element, attrs) {
return {
post: function postLink(scope, iElement, iAttrs, controller) {
...
// move modal's div to the end of $rootElement so all modals will be put one after another INSTEAD OF MAKING A HIERARCHY
var modalElement = $("div.modal", iElement);
modalElement.appendTo($rootElement);
// compile and link the selector's modal
$compile(modalElement)(scope);
}
};
}
};
}]);
In my jasmine test I have to invoke $httpBackend.verifyNoOutstandingExpectation() in order the test to work properly:
it("Element must be replaced by the template", function () {
var linkingFn = $compile("<div ng-app><mydir></mydir></div>");
$httpBackend.verifyNoOutstandingExpectation(); // DOES NOT WORK WITHOUT IT
var scope = $rootScope.$new();
var element = linkingFn(scope);
// element - is <mydir></mydir>
var buttonElm = $("div:first-child", element);
expect(buttonElm).not.toBeUndefined();
expect(buttonElm.hasClass("form-group")).toBe(true);
});
If I comment out verifyNoOutstandingExpectation() directive's postLink function gets invoked after all my expect statements. I don't understand why. Any ideas are welcome!