I'm trying to use karma to test AngularJS directives. But I'm running into issues with templateUrls. Using technique described here, it gets even stranger. It seems to work as advertised and loads my template into the $templateCache, but that cache isn't being used by the directive. Here's some code:
This will work just fine
.directive('messageComposer', function($templateCache) {
return {
restrict: 'E',
template: $templateCache.get('partials/message_composer.html'),
replace: true,
link: function() {
console.log('hello world');
}
};
});
but as soon as I use a templateUrl, it fails to bind in the tests:
.directive('messageComposer', function() {
return {
restrict: 'E',
templateUrl: 'partials/message_composer.html',
replace: true,
link: function() {
console.log('hello world');
}
};
});
Anyone know what's going on here?
Here's my unit test setup:
var $scope;
var $compile;
beforeEach(function() {
module('partials/message_composer.html');
module('messageComposer');
inject(function(_$compile_, $rootScope) {
$scope = $rootScope.$new();
$compile = _$compile_;
});
});
it("works", function() {
$scope.message = {};
elem = angular.element("<message-composer message='message'></message-composer>")
$compile(elem)($scope);
console.log(elem);
expect(true).toBeDefined();
});
According to the url (http://tylerhenkel.com/how-to-test-directives-that-use-templateurl/), I believe you have run the following command:
npm install karma-ng-html2js-preprocessor --save-dev
Now when you are using the above preprocessor, then this preprocessor will convert HTML files into JS strings and will generate Angular modules. These modules, when loaded, puts these HTML files into the $templateCache and therefore Angular won't try to fetch them from the server.
Hope, the following files will clarify you better:
https://github.com/karma-runner/karma-ng-html2js-preprocessor
https://github.com/vojtajina/ng-directive-testing
Related
I'm trying to test a directive using karma and jasmine. I'm using angular 1.4 and I searched different things here in SO and throuhg internet but I can't make it work.
var angular = require('angular');
module.exports = angular.module('myApp.ui.apps.servicesObject.list', [])
.directive('servicesObjectList', function(){
return {
restrict: 'E',
replace: true,
scope: true,
bindToController: {
services: '=',
selectedServices: '='
},
templateUrl: 'app/ui/apps/services/directives/services.html',
controllerAs: 'servicesListCtrl',
controller: 'ServicesListController'
}
})
.controller('ServicesListController', require('./servicesListController'));
This is how I'm trying to test it.
describe('Service app test, listDirective' , function(){
var element, scope, controller;
beforeEach(function(){
angular.mock.module('templates');
angular.mock.module('myApp.ui.apps.servicesObject.list', function($provide) {
$provide.value('gettextCatalog', { getString: function(){}});
$provide.value('translateFilter', function(){});
});
});
beforeEach(inject(function($rootScope, $compile, $controller){
scope = $rootScope;
scope.services= _servicesMock_;
element = '<services-object-list selected-services="[]" services="services"></services-object-list>';
$compile(element)(scope);
scope.$digest();
controller = $controller('ServicesListController', {$scope: scope});
console.log(controller.getServices());
}));
it ("First test", function(){
expect(true).toBe(true);
});
});
The problem that I have is that services is not binding in my controller, only in the scope. What I'm doing wrong? If I do console(controller.getServices()). It returns me undefined instead of the services that I pass as attribute. My production code is working as expected but not tests.
Thank you very much!
After some hours I just discover a new feature added in angular 1.3 to make binding in unitTesting easier. Here is the thread of the discussion https://github.com/angular/angular.js/issues/9425
Basically a third argument is added to the controller constructor service where you can pass the data that is bind to controller.
So the unitTest configuration will be like this.
describe('Service app test, listDirective' , function(){
var element, scope, controller;
beforeEach(function(){
angular.mock.module('templates');
angular.mock.module('myApp.ui.apps.servicesObject.list', function($provide) {
$provide.value('gettextCatalog', { getString: function(){}});
$provide.value('translateFilter', function(){});
});
});
beforeEach(inject(function($rootScope, $compile, $controller){
var data = {
services: _servicesMock_
};
scope = $rootScope;
scope.services= _servicesMock_;
element = '<services-object-list selected-services="[]" services="services"></services-object-list>';
$compile(element)(scope);
scope.$digest();
controller = $controller('ServicesListController', {$scope: scope}, data);
console.log(controller.getServices());
}));
it ("First test", function(){
expect(true).toBe(true);
});
});
I have searched all over and the only thing I can come up with is that I don't understand something fundamental about how the compile function works.
Here is what I have
angular.module("app", [])
.directive("foo", function(){
return {
scope: {},
controller: function() {
this.someValue = 23;
},
contollerAs: "ctrl",
compile: function(tElem, tAttrs) {
tElem.html("<h1>Data: {{ctrl.someValue}}</h1>");
},
template: '<h1>test</h1>'
}
});
This displays: Data: and does not seem to see the "someValue" variable.
However when I use scope instead of the controllerAs syntax, it works.
//this works fine
angular.module("app", [])
.directive("foo", function(){
return {
scope: {},
controller: function($scope) {
$scope.someValue = 23;
},
contollerAs: "ctrl",
compile: function(tElem, tAttrs) {
tElem.html("<h1>Data: {{someValue}}</h1>");
},
template: '<h1>test</h1>'
}
});
This displays: Data: 23
Is there something I am missing here? Am I even using compile correctly?
Manual seems less then helpful.
Because there is a typo. It is controllerAs, not contollerAs.
It is recommended to use template function instead of compile. This makes upgrading to components easier in future, also saves from problems - compile in the directive above won't work correctly if there's no dummy <h1>test</h1> template.
Here is the directive:
app.directive('templates',function() {
return {
restrict:'E',
templateUrl: function(s,e) {
switch (e.template) {
case 'temp1':
return 'temp1.html';
case 'temp2':
return 'temp1.htm2';
default:
// do nothing... ;
}
}
};
});
I can get it to compile in my test but i'm not sure how to test if the correct templates are being called
There is not much to test here. But as a feel good test you can just load a template into cache and do a test if specific element has been rendered or not as a feel good test.
Example:-
describe('templates', function () {
beforeEach(inject(function ($rootScope, $templateCache, $compile) {
// Set an arbitrary template to test
$templateCache.put('temp1.html', '<div class="test">Hello</div>');
element = angular.element("<templates template='temp1'></templates>");
$compile(element)(scope);
$rootScope.$digest();
}));
it('Should load template', function () {
expect(element.find('.test').length).toEqual(1); //Test if element has loaded template properly
expect(element.find('.test').text()).toEqual("Hello");
});
Demo
On a different note your directive can break if there is no template provided, it is required to return a template from templateurl function. Also you can make this simple directive more generic as well.
.directive('templates',function() {
return {
restrict:'E',
templateUrl: function(e,attr) {
return attr.template + ".html"
};
});
How ever, there is nothing to be tested here because you will just end up testing angular's templateUrl function evaluation.
It's a well known issue in angular to need to use the special array syntax when bringing in dependencies into controllers to avoid minification problems, so I have been using that notation. But it seems that the injector is still having issues with this code that appear only after sending it through gulp-uglify.
Do other angular elements like directives also need to have this syntax be used? Also, I'm using object notation to define one of the controllers, so might that be the problem?
Some main config stuff.
var app = angular.module('musicApp', ['ngSanitize']);
//Whitelist Soundcloud
app.config(function($sceDelegateProvider) {
$sceDelegateProvider.resourceUrlWhitelist([
'self',
'https://w.soundcloud.com/**'
]);
});
Directives, one with a controller in it.
app.directive('soundcloudHtml', ['$sce', function($sce){
return {
restrict: 'A',
link: function(scope, element, attrs) {
scope.musicPiece.soundcloud = $sce.trustAsHtml(scope.musicPiece.soundcloud);
}
}
}]);
app.directive('music', function(){
return {
restrict: 'E',
scope:{
type: '='
},
templateUrl: '/resources/data/music/music.html?po=343we',
link: function(scope, element, attrs) {
},
controller: ['$http', '$scope', function($http, $scope){
this.musicList = [];
$scope.Utils = Utils;
var ctrl = this;
$http.get('/resources/data/music/music.json').success(function(data){
ctrl.musicList = data;
Utils.updateTableOfContents();
});
}],
controllerAs: 'musicCtrl'
};
});
Looks like I missed that config also needs that pattern as well in order to be minified.
The config should be
//Whitelist Soundcloud
app.config(['$sceDelegateProvider', function($sceDelegateProvider) {
$sceDelegateProvider.resourceUrlWhitelist([
'self',
'https://w.soundcloud.com/**'
]);
}]);
And not
//Whitelist Soundcloud
app.config(function($sceDelegateProvider) {
$sceDelegateProvider.resourceUrlWhitelist([
'self',
'https://w.soundcloud.com/**'
]);
});
I'm trying to compile directive via angular service but unfortunately it doesn't work.
The idea is to show errors in popups.
I've modified $exceptionHandler service:
crm.factory('$exceptionHandler', function(popup) {
return function(exception) {
popup.open({message: exception});
}
});
The popup service looks like this:
crm.factory('popup', function ($document) {
return {
open: function (data) {
var injector = angular.element(document).injector(),
$compile = injector.get('$compile'),
template = angular.element('<popup></popup>');
// var ctmp = $compile(template.contents());
$compile(template.contents());
$document.find('body').append(template);
}
};
});
And I don't think that this was a good idea to hard code $compile service (but I haven't any ideas how to realize this in angular):
$compile = injector.get('$compile')
Popup directive:
crm.directive('popup', function () {
return {
restrict: 'E',
replace: true,
templateUrl: '/public/js/templates/common/popup.html',
link: function() {
console.log('link()');
},
controller: function () {
console.log('ctrl()');
}
};
});
May be there are some other ways to do this?
Thanks.
You can inject $compile directly into your service, also you're not quite using $compile correctly:
//commented alternative lines for allowing injection and minification since reflection on the minified code won't work
//crm.factory('popup', ['$document', '$compile', function ($document, $compile) {
crm.factory('popup', function ($document, $compile) {
return {
open: function (data) {
var template = angular.element('<popup></popup>'),
compiled = $compile(template);
$document.find('body').append(compiled);
}
};
});
//closing bracket for alternative definition that allows minification
//}]);