I am trying to write a jasmine test that will test if an angular directive I've written is working.
Here is my spec file:
describe('blurb directive', function () {
var scope, httpMock, element, controller;
beforeEach(module('mdotTamcCouncil'));
beforeEach(module('mdotTamcCouncil.core'));
beforeEach(module('blurb'));
beforeEach(inject(function (_$httpBackend_, $rootScope, $compile) {
element = angular.element('<mcgi-blurb text-key="mainPageIntro"></mcgi-blurb>');
var httpResponse = '<textarea name="content" ng-model="content"></textarea>';
scope = $rootScope.$new();
httpMock = _$httpBackend_;
httpMock.whenGET('components/blurb/blurb.html').respond(httpResponse);
element = $compile(element)(scope);
scope.$digest();
}));
it('should have some content', function () {
expect(scope.content).toBeDefined();
});
});
The value "scope.content" is always undefined and when I look at the scope object it seems to be a generic scope object that doesn't have my custom attributes on it.
Here are the other related files:
blurb-directive.js
(function () {
'use strict';
angular.module('blurb')
.directive('mcgiBlurb', blurb);
function blurb() {
return {
restrict: 'E',
replace: true,
templateUrl: jsGlobals.componentsFolder + '/blurb/blurb.html',
controller: 'BlurbController',
controllerAs: 'blurb',
bindToController: false,
scope: {
textKey: "#"
}
};
};
})();
blurb-controller.js
(function () {
angular.module('blurb')
.controller('BlurbController', ['$scope', 'blurbsFactory', 'userFactory', function ($scope, blurbsFactory, userFactory) {
$scope.content = "";
$scope.blurbs = {};
$scope.currentUser = {};
this.editMode = false;
userFactory().success(function (data) {
$scope.currentUser = data;
});
blurbsFactory().success(function (data) {
$scope.blurbs = data;
$scope.content = $scope.blurbs[$scope.textKey];
});
this.enterEditMode = function () {
this.editMode = true;
};
this.saveEdits = function () {
this.editMode = false;
$scope.blurbs[$scope.textKey] = $scope.content;
};
}]);
})();
What am I doing wrong?
The directive has isolated scope, so the scope passed to its controller and link function (if there was one), is the isolated one, different than your scope.
You may have luck getting the scope of the directive using element.isolateScope(); you may not, because of the replace: true - try to make sure. You may also access the controller instance using element.controller('mcgiBlurb').
Related
I am trying to unit-testing for following code, I wrote following code for unit-testing like below, I have tried so many ways to work, but I keep getting error:
'Cannot read property 'num' of undefined'
I do not know why scope is not properly set. If you have any idea about it, can you please give some advices?
var angular = require('angular');
require('angular-mocks');
describe('test directive', function () {
let $rootScope;
let $compile;
let scope;
let newScope;
let element;
beforeEach(angular.mock.inject(function (_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
describe('test directive', function () {
beforeEach(function () {
newScope = $rootScope.$new();
element = $compile('<test-directive></test-directive>')(newScope);
newScope.$digest();
scope = element.isolateScope();
});
fit('scope initialized', function () {
expect(scope.num).toEqual(1);
});
});
});
module.exports = module.directive('testDirective', ['$rootScope', '$scope', function($rootScope, $scope) {
return {
template: require('./test.html'),
restrict: 'E',
controller: [
'$scope',
function ($scope) {
$scope.num = 1
$scope.sum = function(a, b) {
return a + b;
}
}]
}
}]);
The scope variable is undefined because the directive being tested does not have an isolate scope. Instead, use the scope of the element:
beforeEach(function () {
newScope = $rootScope.$new();
element = $compile('<test-directive></test-directive>')(newScope);
newScope.$digest();
̶s̶c̶o̶p̶e̶ ̶=̶ ̶e̶l̶e̶m̶e̶n̶t̶.̶i̶s̶o̶l̶a̶t̶e̶S̶c̶o̶p̶e̶(̶)̶;̶
scope = element.scope();
});
fit('scope initialized', function () {
expect(scope.num).toEqual(1);
});
Be aware that directive has a fatal flaw. It can only use it once within a given scope.
I am using isolate scope in custom directive. I have updated plunker link. http://plnkr.co/edit/NBQqjxW8xvqMgfW9AVek?p=preview
Can someone help me in writing unit test case for script.js file.
script.js
var app = angular.module('app', [])
app.directive('myDirective', function($timeout) {
return {
restrict: 'A',
scope: {
content: '='
},
templateUrl: 'my-directive.html',
link: function(scope, element, attr) {
$timeout(function() {
element = element[0].querySelectorAll('div.outerDiv div.innerDiv3 p.myClass');
var height = element[0].offsetHeight;
if (height > 40) {
angular.element(element).addClass('expandable');
scope.isShowMore = true;
}
})
scope.showMore = function() {
angular.element(element).removeClass('expandable');
scope.isShowMore = false;
};
scope.showLess = function() {
angular.element(element).addClass('expandable');
scope.isShowMore = true;
};
}
}
})
(function() {
'use strict';
describe('Unit testing directive', function() {
var $compile, scope, element, compiledDirective, $rootScope, $timeout;
beforeEach(module("app"));
beforeEach(inject(function(_$compile_, _$rootScope_, _$timeout_) {
$compile = _$compile_;
scope = _$rootScope_.$new();
$timeout = _$timeout_;
element = angular.element(' <div my-directive content="content"></div>');
compiledDirective = $compile(element)(scope);
scope.$digest();
}));
it('should apply template', function() {
expect(compiledDirective.html()).toBe('');
});
it('check for timeout', function() {
$timeout.flush();
});
});
})();
Use $timeout.flush() function for writing testcase for $timeout
it('check for timeout', function() {
scope.digest();
// flush timeout(s) for all code under test.
$timeout.flush();
// this will throw an exception if there are any pending timeouts.
$timeout.verifyNoPendingTasks();
expect(scope.isShowMore).toBeTruthy();
});
Check this article for better understanding.
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 which creates an isolate scope and has a parent directive controller through the require option. In the link function I am adding some methods to the scope.
When I am trying to compile the directive in my test, I can't seem to get access to the methods I added in the link function, even though the link function is definitely executing.
The scope I have just seems to be an empty child scope of $rootScope. I have tried using element.isolateScope() but this just seems to give me the scope of the parent directive.
I am probably compiling something wrong, can someone help?
parent-directive.js
angular.module("app").directive("sortHead", function() {
return {
restrict: "A",
controller: function ($scope) {
$scope.sortField = undefined;
$scope.reverse = false;
this.setSortField = function(value) {
$scope.sortField = value;
};
this.setReverse = function(value) {
$scope.reverse = value;
};
this.getSortField = function(value) {
return $scope.sortField;
};
this.getReverse = function(value) {
return $scope.reverse;
};
}
};
});
directive-to-test.js
angular.module("app").directive("sortHeader", function() {
return {
restrict: "A",
templateUrl: "templates/sortHeader.html",
scope: {
title: "#",
sort: "#"
},
require: "^sortHead",
link: function(scope, element, attrs, controller) {
scope.sortBy = function(name) {
if (controller.getSortField() === name) {
controller.setReverse(!controller.getReverse());
} else {
controller.setSortField(name);
controller.setReverse(false);
}
};
scope.getSortField = function() {
return controller.getSortField();
};
scope.getReverse = function() {
return controller.getReverse();
};
}
};
});
test.js
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
element = angular.element("<th sort-header title='Name' sort='name'></th>");
$compile(element)(scope);
scope.$digest();
}));
The test doesn't seem to be workable in its current form.
Here is a plunker with fixed spec
beforeEach(module('app'));
beforeEach(inject(function ($rootScope, $compile, $templateCache, sortHeaderDirective) {
scope = $rootScope.$new();
$templateCache.put(sortHeaderDirective[0].templateUrl, '');
element = angular.element("<th sort-header title='Name' sort='name'></th>");
element.data('$sortHeadController', {});
$compile(element)(scope);
scope.$digest();
}));
it("should do something", inject(function () {
expect(element.isolateScope().title).toEqual('Name');
}));
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();
});
});