I facing difficulty in covering the inline controller function defined in a directive using Jasmine and Karma. Below is the code of that directive.
myApp.directive("list", function() {
return {
restrict: "E",
scope: {
items: "=",
click: "&onClick"
},
templateUrl: "directives/list.html",
controller: function($scope, $routeParams) {
$scope.selectItem = function(item) {
$scope.click({
item: item
});
}
}
}
});
Unit Test case is below:
describe('Directive:List', function () {
var element, $scope, template, controller;
beforeEach(inject(function ($rootScope, $injector, $compile, $templateCache, $routeParams) {
$httpBackend = $injector.get('$httpBackend');
$httpBackend.when("GET", "directives/list.html").respond({});
$scope = $rootScope.$new();
element = $compile('<list></list>')($scope);
template = $templateCache.get('directives/list.html');
$scope.$digest();
controller = element.controller($scope, $routeParams);
}));
it('Test Case-1: should contain the template', function () {
expect(element.html()).toMatch(template);
});
});
In my code coverage report, the controller function is not covered. Also I am not able to get the controller instance and test the selectItem method.
Any idea would be of great help!
Related
I'm trying to do unit testing on a directive that calls a service to retrieve posts and am confused on how to test the directive. Usually directive testing is easy with just compiling the directive element but this directive element is calling posts via a get call. How do I test this?
(function() {
'use strict';
describe('Post directive', function() {
var element, scope, postService, $rootScope, $compile, $httpBackend;
beforeEach(module('madkoffeeFrontend'));
beforeEach(inject(function(_postService_, _$rootScope_, _$compile_, _$httpBackend_) {
postService = _postService_;
$httpBackend = _$httpBackend_;
$rootScope = _$rootScope_;
$compile = _$compile_;
// $httpBackend.whenGET('http://madkoffee.com/wp-json/wp/v2/posts?per_page=3').passThrough();
scope = $rootScope.$new();
spyOn(postService, 'getPosts');
element = $compile('<posts post-num="3"></posts>')(scope);
scope.$digest();
}));
it('should get the posts successfully', function () {
expect(postService.getPosts).toHaveBeenCalled();
});
// it('should expect post to be present', function () {
// expect(element.html()).not.toEqual(null);
// });
});
})();
This is the controller:
(function() {
'use strict';
angular
.module('madkoffeeFrontend')
.directive('posts', postsDirective);
/** #ngInject */
function postsDirective() {
var directive = {
restrict: 'E',
scope: {
postNum: '='
},
templateUrl: 'app/components/posts/posts.html',
controller: PostController,
controllerAs: 'articles',
bindToController: true
};
return directive;
/** #ngInject */
function PostController($log, postService) {
var vm = this;
postService.getPosts(vm.postNum).then(function(data) {
$log.debug(data);
vm.posts = data;
}).catch(function (err) {
$log.debug(err);
});
}
}
})();
Don't call getPosts() from your test: that doesn't make sense.
Tell the spied service what to return. The fact that it uses http to get posts is irrelevant for the directive. You're unit-testing the directive, not the service. So you can just assume the service returns what it's supposed to return: a promise of posts.
Tell the spied postService what to return:
spyOn(postService, 'getPosts').and.returnValue(
$q.when(['some fake post', 'some other fake post']));
I have a directive that accesses the $routeParams of the page as such:
myApp.directive("myList", function ($routeParams) {
return {
restrict: 'E',
templateUrl: 'tabs/my-list.html',
link: function (scope) {
scope.year = $routeParams.year;
}
};
});
The directive works as expected and correctly accesses the $routeParams
I am trying to test using angular-mock/jasmine. I can't figure out how to pass mock $routeParams to the directive. This is what I have:
describe('myList', function () {
var scope, compile, element, compiledDirective;
var mockParams = { 'year': 1996 };
beforeEach(function () {
module('templates', 'MyApp');
inject(function ($compile, $rootScope, $routeParams) {
compile = $compile;
scope = $rootScope.$new();
});
element = angular.element('<my-list></my-list>');
compiledDirective = compile(element)(scope);
scope.$digest();
});
it('should fill in the year', function () {
expect(scope.year).toEqual(mockParams.year);
});
});
Which obviously doesn't work because I never passed passed mockParams to the directive. Is there a way to do this?
Mock the $routeParams object mockParams using angular.extend OR do assign mockParams object directly to $routeParams. In that way $routeParams will be available before directive gets compiled.
inject(function ($compile, $rootScope, $routeParams) {
compile = $compile;
scope = $rootScope.$new();
angular.extend($routeParams, mockParams);
});
I have directive myItem. I want to change one property that is passed from parent to directive, so I use controller in myItem where I divide value by 60.
Everything works fine on the website.
Directive
define(['angular',
'core',
'ui-bootstrap',
],
function(angular, coreModule, uiBootStrap) {
'use strict';
function myItemDirective(APPS_URL) {
return {
restrict: 'E',
replace: 'true',
scope: {
item: '='
},
templateUrl: APPS_URL.concat('item.tpl.html'),
controller: ['$scope', function($scope) {
$scope.item.property = Math.round($scope.item.property / 60);
}]
};
}
return angular.module('apps.myItemModule', [coreModule.name])
.directive('myItem', ['APPS_URL', myItemDirective]); });
Now I would like to write a test where I can check that rendered value in directive is value passed from parent divided by 60.
Unit Test
define([
'angular',
'angularMocks',
'apps/directives/mydirective' ], function (angular, mocks, myItemDirective) {
var $httpBackend, $controller, $rootScope, $scope, directive,
item = {
property: 60
};
describe('my directive', function () {
beforeEach(function() {
mocks.module(myItemDirective.name);
mocks.inject(function(_$rootScope_, $injector, $window) {
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$compile = $injector.get('$compile');
compileDirective();
});
});
afterEach(function() {
$scope.$destroy();
element.remove();
});
function compileDirective() {
element = angular.element('<my-item item=' + JSON.stringify(item) + '></my-item>');
directive = $compile(element)($scope);
$scope.$digest();
directiveScope = element.isolateScope();
}
it('test', function(){
// console.log(element.find('#item-holder').innerText)
// > 60
expect(element.find('#item-holder').innerText).toEqual(Math.round(item.propert/60));
// this will fail because item.property was not divided by 60
});
}); });
Problem
I am not able to render directive in unit test with value divided by 60. I can see in console that controller in directive has been called but the value is not changed.
The problem was related to tests using the same reference to object item.
To fix this:
moved item to beforeEach
changed the way to create element
changed the way to get directive scope
use $scope.$apply()
So test looks like:
define([
'angular',
'angularMocks',
'apps/directives/mydirective' ], function (angular, mocks, myItemDirective) {
var $httpBackend, $controller, $rootScope, $scope, directive,
item;
describe('my directive', function () {
beforeEach(function() {
item = {
property: 60
};
mocks.module(myItemDirective.name);
mocks.inject(function(_$rootScope_, $injector, $window) {
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$compile = $injector.get('$compile');
compileDirective();
});
});
afterEach(function() {
$scope.$destroy();
element.remove();
});
function compileDirective() {
$scope.item = item;
element = angular.element('<my-item item="item"></my-item>');
directive = $compile(element)($scope);
$scope.$apply();
directiveScope = directive.isolateScope();
}
it('test', function(){
// console.log(element.find('#item-holder').innerText)
// > 60
expect(element.find('#item-holder').innerText).toEqual(Math.round(item.propert/60));
// this will fail because item.property was not divided by 60
});
}); });
I'm trying to test a directive. using controllerAs creates a toplevel scope where we can access its properties. however when debugging I try to access element.scope().property --I am getting undefined. Any help on why would be greatly appreciated.
--BugDirective
(function() {
'use strict';
angular
.module('debug-blog-app')
.directive('avBug', avBug);
function avBug() {
return {
restrict: 'E',
templateUrl: 'views/directives/bug.html',
scope: {
bug: '='
},
controller: BugFormController,
controllerAs: 'bugCtrl',
bindToController: true
};
};
BugFormController.$inject = ['$window', '$scope', 'BugService'];
function BugFormController($window, $scope, BugService) {
var vm = this;
vm.updateBug = function(){
BugService.updateBug(vm.bug);
};
vm.deleteBug = function(){
if($window.confirm("Delete the Bug?!")){
return BugService.deleteBug(vm.bug.id)
.then(function(){
$scope.$emit('bug.deleted', vm.bug);
});
}
};
};
})();
--Spec
'use strict'
describe('avBug Directive', function () {
var bugCtrl,
element,
BugService,
$scope,
$rootScope;
beforeEach(module('app'));
beforeEach(inject(function($q, $compile, _$rootScope_, _BugService_) {
$rootScope = _$rootScope_;
var directiveMarkup = angular.element("<av-bug></av-Bug>");
element = $compile(directiveMarkup)($rootScope);
bugCtrl = element.scope().bugCtrl;
BugService = _BugService_;
spyOn(BugService, 'deleteBug').and.callFake(function() {
var deferred = $q.defer();
deferred.resolve('data');
return deferred.promise;
});
spyOn($rootScope,'$emit').and.callThrough();
}));
it('should delete a bug', function() {
bugCtrl.deleteBug(0);
expect(BugService.deleteBug).toHaveBeenCalledWith(0);
$rootScope.$digest();
expect($rootScope.$emit).toHaveBeenCalledWith('bug.deleted');
});
});
--index.html
<div class="container">
<div ui-view></div>
</div>
--home.html
<av-bug bug="bug" ng-repeat="bug in homeCtrl.bugs"></av-bug>
I would also add, for unit testing purposes, that you can get the parent controllerAs with the same function that #Ramy Deeb post.
vm = element.scope().$$childTail.nameOfParentControllerAs
And for testing the element isolate scope, just get
isolateScope = element.isolateScope()
I hope this can help anyone like me ending here searching how to unit test directives and controllers calling them, all of them being using ControllerAs syntax.
Thanks you.
I would comment, but I cannot.
Your controller is located at:
element.scope().$$childTail.bugCtrl
I have a directive that I want to unittest, but I'm running into the issue that I can't access my isolated scope. Here's the directive:
<my-directive></my-directive>
And the code behind it:
angular.module('demoApp.directives').directive('myDirective', function($log) {
return {
restrict: 'E',
templateUrl: 'views/directives/my-directive.html',
scope: {},
link: function($scope, iElement, iAttrs) {
$scope.save = function() {
$log.log('Save data');
};
}
};
});
And here's my unittest:
describe('Directive: myDirective', function() {
var $compile, $scope, $log;
beforeEach(function() {
// Load template using a Karma preprocessor (http://tylerhenkel.com/how-to-test-directives-that-use-templateurl/)
module('views/directives/my-directive.html');
module('demoApp.directives');
inject(function(_$compile_, _$rootScope_, _$log_) {
$compile = _$compile_;
$scope = _$rootScope_.$new();
$log = _$log_;
spyOn($log, 'log');
});
});
it('should work', function() {
var el = $compile('<my-directive></my-directive>')($scope);
console.log('Isolated scope:', el.isolateScope());
el.isolateScope().save();
expect($log.log).toHaveBeenCalled();
});
});
But when I print out the isolated scope, it results in undefined. What really confuses me though, if instead of the templateUrl I simply use template in my directive, then everything works: isolateScope() has a completely scope object as its return value and everything is great. Yet, somehow, when using templateUrl it breaks. Is this a bug in ng-mocks or in the Karma preprocessor?
Thanks in advance.
I had the same problem. It seems that when calling $compile(element)($scope) in conjunction with using a templateUrl, the digest cycle isn't automatically started. So, you need to set it off manually:
it('should work', function() {
var el = $compile('<my-directive></my-directive>')($scope);
$scope.$digest(); // Ensure changes are propagated
console.log('Isolated scope:', el.isolateScope());
el.isolateScope().save();
expect($log.log).toHaveBeenCalled();
});
I'm not sure why the $compile function doesn't do this for you, but it must be some idiosyncracy with the way that templateUrl works, as you don't need to make the call to $scope.$digest() if you use an inline template.
With Angularjs 1.3, if you disable debugInfoEnabled in the app config:
$compileProvider.debugInfoEnabled(false);
isolateScope() returns undefined also!
I had to mock and flush the $httpBackend before isolateScope() became defined. Note that $scope.$digest() made no difference.
Directive:
app.directive('deliverableList', function () {
return {
templateUrl: 'app/directives/deliverable-list-directive.tpl.html',
controller: 'deliverableListDirectiveController',
restrict = 'E',
scope = {
deliverables: '=',
label: '#'
}
}
})
test:
it('should be defined', inject(function ($rootScope, $compile, $httpBackend) {
var scope = $rootScope.$new();
$httpBackend.expectGET('app/directives/deliverable-list-directive.tpl.html').respond();
var $element = $compile('<deliverable-list label="test" deliverables="[{id: 123}]"></deliverable-list>')(scope);
$httpBackend.flush();
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
expect($element).toBeDefined();
expect($element.controller).toBeDefined();
scope = $element.isolateScope();
expect(scope).toBeDefined();
expect(scope.label).toEqual('test');
expect(scope.deliverables instanceof Array).toEqual(true);
expect(scope.deliverables.length).toEqual(1);
expect(scope.deliverables[0]).toEqual({id: 123});
}));
I'm using Angular 1.3.
You could configure karma-ng-html2js-preprocessor plugin. It will convert the HTML templates into a javascript string and put it into Angular's $templateCache service.
After set a moduleName in the configuration you can declare the module in your tests and then all your production templates will available without need to mock them with $httpBackend everywhere.
beforeEach(module('partials'));
You can find how to setup the plugin here: http://untangled.io/how-to-unit-test-a-directive-with-templateurl/
In my case, I kept running into this in cases where I was trying to isolate a scope on a directive with no isolate scope property.
function testDirective() {
return {
restrict:'EA',
template:'<span>{{ message }}</span>'
scope:{} // <-- Removing this made an obvious difference
};
}
function testWithoutIsolateScopeDirective() {
return {
restrict:'EA',
template:'<span>{{ message }}</span>'
};
}
describe('tests pass', function(){
var compiledElement, isolatedScope, $scope;
beforeEach(module('test'));
beforeEach(inject(function ($compile, $rootScope){
$scope = $rootScope.$new();
compiledElement = $compile(angular.element('<div test-directive></div>'))($scope);
isolatedScope = compiledElement.isolateScope();
}));
it('element should compile', function () {
expect(compiledElement).toBeDefined();
});
it('scope should isolate', function () {
expect(isolatedScope).toBeDefined();
});
});
describe('last test fails', function(){
var compiledElement, isolatedScope, $scope;
beforeEach(module('test'));
beforeEach(inject(function ($compile, $rootScope){
$scope = $rootScope.$new();
compiledElement = $compile(angular.element('<div test-without-isolate-scope-directive></div>'))($scope);
isolatedScope = compiledElement.isolateScope();
}));
it('element should compile', function () {
expect(compiledElement).toBeDefined();
});
it('scope should isolate', function () {
expect(isolatedScope).toBeDefined();
});
});