I am having some trouble reaching my return statement for my angular test. I am using jasmine with karma, and bard js. I used the ng2html preprocesser to $templateCache
templatesCached is the module name given to the files in my templates folder. ha.module.core Houses the directive that I want to test. I am reaching it when I run my debugging tools.
Below is my test. They pass, but the issue I am having is that rootScope does not hold any particular values. Also
element.html() // returns "" in the console. I was expecting my directive back. Is this wrong?
After I run through controller = element.scope I am getting
html = [div.ng-scope]
describe(" directive", function () {
var element,
template,
controller;
beforeEach(function () {
bard.appModule("templatesCached", "ha.module.core");
bard.inject(
"$compile",
"$controller",
"$rootScope",
"haConfig"
);
});
beforeEach(function () {
var html = angular.element("<div explore-hero></div>");
spyOn(myService, "getTemplateUrl");
//console.log("html ", html);
$rootScope = $rootScope.$new();
element = $compile(html)($rootScope);
$rootScope.$digest(element);
controller = element.scope();
element.controller('heroController');
Element.controller is an anonymous function.
console.log('element', element);
});
it("should have a div element", function () {
var result = element[0].querySelectorAll(".container");
expect(element.length).toBe(1); // not a ideal test for creation
expect(result).toBeDefined();
});
});
Here is my template cached.
module.run(['$templateCache', function($templateCache) {
$templateCache.put('/templates/explore-hero-base-template.html',
'<div class="container">\n' +
' <div share-widget></div>\n' +
'///html divs and info'
'</div><!-- .container -->');
}]);
My directive
angular.module('ha.module.core').directive('exploreHero', function(myService) {
var HeroController = function($scope) {
$scope.emit('methods')
};
return {
restrict: 'A',
scope: true,
templateUrl: myService.getTemplateUrl('explore-hero.html),
controller: 'HeroController
}
)
Any insight would help
Related
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
Thie question is somewhat related to How do I inject a mock dependency into an angular directive with Jasmine on Karma. But I cant figure it out. Heres the thing:
I have a simple angular directive for rendering a head-part of my apllication with several parameters. One is passed, two came from the URL vie $location and $routeParam. The directive looks like this:
'use strict';
myApp.directive('appHeader', ['$routeParams', '$location', function ($routeParams, $location) {
return {
restrict: 'E',
templateUrl: 'path/to/partials/template.html',
scope: {
icon: '#icon'
},
link: function (scope, element, attributes) {
var lastUrlPart = $location.path().split('/').pop();
scope.project = $routeParams.itemName;
scope.context = lastUrlPart === scope.project ? '' : lastUrlPart;
}
};
}]);
This is called via <app-header icon="bullhorn"></app-header>.
Now I want to add some tests. As for the template rendering I'm done. The following works like expected. The test passes.
describe('appHeader', function () {
var element, scope;
beforeEach(module('myApp'));
beforeEach(module('myAppPartials'));
beforeEach(inject(function ($rootScope, $compile) {
element = angular.element('<app-header icon="foo"></app-header>');
scope = $rootScope;
$compile(element)(scope);
scope.$digest();
}));
it('should contain the glyphicon passed to the directive', function () {
expect(element.find('h1').find('.glyphicon').hasClass('glyphicon-foo')).toBeTruthy();
});
});
Now I want to test that scope.context and scope.project are set accordingly to the dependencies $location and $routeParams, which I want to mock of course. How can I acieve this.
I tried for instance the answer from the question linked above:
beforeEach(module(function ($provide) {
$provide.provider('$routeParams', function () {
this.$get = function () {
return {
itemName: 'foo'
};
};
});
}));
But In my test
it('should set scope.project to itemName from $routeParams', function () {
expect(scope.project).toEqual('foo');
});
scope.project is undefined:
Running "karma:unit:run" (karma) task
Chrome 35.0.1916 (Mac OS X 10.9.3) appHeader should set scope.project to itemName from routeParams FAILED
Expected undefined to equal 'foo'.
Error: Expected undefined to equal 'foo'.
As for the location dependency I tried to setUp a Mock mysel like this:
var LocationMock = function (initialPath) {
var pathStr = initialPath || '/project/bar';
this.path = function (pathArg) {
return pathArg ? pathStr = pathArg : pathStr;
};
};
Then injection $location in the before each and set a spyOn to the calling of path() like this:
spyOn(location, 'path').andCallFake(new LocationMock().path);
But then, scope.context is undefined, too.
it('should set scope.context to last part of URL', function () {
expect(scope.context).toEqual('bar');
});
Can someone please point out what I am doing wrong here?
Provider's mock works fine, but the problem is in scopes. Your directive has isolated scope. Thus this directive's scope is the child of the scope defined in test. Quick but not recomended fix is:
it('should set scope.project to itemName from $routeParams', function () {
expect(scope.$$childHead.project).toEqual('foo'); });
Try to avoid use scope when testing directives. Better approach will be to mock template and check data in it. For your case it will be something like this:
var viewTemplate = '<div>' +
'<div id="project">{{project}}</div>' +
'</div>';
beforeEach(inject(function ($templateCache) {
$templateCache.put('path/to/partials/template.html', viewTemplate);
}));
and test:
it('should set scope.project to itemName from $routeParams', function () {
expect(element.find('#project').text()).toEqual('foo');
});
for the context it will be the same.
I have a directive as below which i want to cover as part of my jasmine unit test but not sure how to get the template value and the values inside the link in my test case. This is the first time i am trying to unit test a directive.
angular.module('newFrame', ['ngResource'])
.directive('newFrame', [
function () {
function onAdd() {
$log.info('Clicked onAdd()');
}
return {
restrict: 'E',
replace: 'true',
transclude: true,
scope: {
filter: '=',
expand: '='
},
template:
'<div class="voice ">' +
'<section class="module">' +
'<h3>All Frames (00:11) - Summary View</h3>' +
'<button class="btn" ng-disabled="isDisabled" ng-hide="isReadOnly" ng-click="onAdd()">Add a frame</button>' +
'</section>' +
'</div>',
link: function (scope) {
scope.isDisabled = false;
scope.isReadOnly = false;
scope.onAdd = onAdd();
}
};
}
]);
Here is an example with explanation:
describe('newFrame', function() {
var $compile,
$rootScope,
$scope,
$log,
getElement;
beforeEach(function() {
// Load module and wire up $log correctly
module('newFrame', function($provide) {
$provide.value('$log', console);
});
// Retrieve needed services
inject(function(_$compile_, _$rootScope_, _$log_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
$log = _$log_;
});
// Function to retrieve a compiled element linked to passed scope
getCompiledElement = function(scope) {
var element = $compile('<new-frame></new-frame>')(scope);
$rootScope.$digest();
return element;
}
// Set up spies
spyOn($log, 'info').and.callThrough();
});
it('test', function() {
// Prepare scope for the specific test
$scope.filter = 'Filter';
$scope.expand = false;
// This will be the compiled element wrapped in jqLite
// To get reference to the DOM element do: element[0]
var element = getCompiledElement($scope);
// Get a reference to the button element wrapped in jqLite
var button = element.find('button');
// Verify button is not hidden by ng-hide
expect(button.hasClass('ng-hide')).toBe(false);
// Verify button is not disabled
expect(button.attr('disabled')).toBeFalsy();
// Click the button and verify that it generated a call to $log.info
button.triggerHandler('click');
expect($log.info).toHaveBeenCalled();
});
});
Demo: http://plnkr.co/edit/tOJ0puOd6awgVvRLmfAD?p=preview
Note that I changed the code for the directive:
Injected the $log service
Changed scope.onAdd = onAdd(); to scope.onAdd = onAdd;
After reading the angular documentation for directives,i was able to solve this. Since the restrict is marked as E, the directive can only be injected through a element name. Earlier i was trying through div like below.
angular.element('<div new-frame></div>')
This will work if restrict is marked as A (attributes). Now i changed my injection in he spec file to match the directive with element name.
angular.element('<new-frame></new-frame>')
Now i was able to get the template and scope attributes in my spec. Just to be sure to accept everything, the combination of A (aatributes), E (elements) and C (class name) can be used in the restrict or any 2 as needed.
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();
});
});
I want to create an AlertFactory with Angular.factory.
I defined an html template like follow
var template = "<h1>{{title}}</h1>";
Title is provided by calling controller and applied as follow
var compiled = $compile(template)(scope);
body.append(compiled);
So, how I can pass isolated scope from controller to factory?
I'm using in controller follow code
AlertFactory.open($scope);
But $scope is global controller scope variable. I just want pass a small scope for factory with just title property.
Thank you.
You can create a new scope manually.
You can create a new scope from $rootScope if you inject it, or just from your controller scope - this shouldn't matter as you'll be making it isolated.
var alertScope = $scope.$new(true);
alertScope.title = 'Hello';
AlertFactory.open(alertScope);
The key here is passing true to $new, which accepts one parameter for isolate, which avoids inheriting scope from the parent.
More information can be found at:
http://docs.angularjs.org/api/ng.$rootScope.Scope#$new
If you only need to interpolate things, use the $interpolate service instead of $compile, and then you won't need a scope:
myApp.factory('myService', function($interpolate) {
var template = "<h1>{{title}}</h1>";
var interpolateFn = $interpolate(template);
return {
open: function(title) {
var html = interpolateFn({ title: title });
console.log(html);
// append the html somewhere
}
}
});
Test controller:
function MyCtrl($scope, myService) {
myService.open('The Title');
}
Fiddle
Followings are the steps:
Add your HTML to the DOM by using var comiledHTML =
angular.element(yourHTML);
Create a new Scope if you want var newScope = $rootScope.$new();
Call $comile(); function which returns link function var linkFun =
$compile(comiledHTML);
Bind the new scope by calling linkFun var finalTemplate =
linkFun(newScope);
Append finalTemplate to your DOM
YourHTMLElemet.append(finalTemplate);
check out my plunkr. I'm programmatically generating a widget directive with a render directive.
https://plnkr.co/edit/5T642U9AiPr6fJthbVpD?p=preview
angular
.module('app', [])
.controller('mainCtrl', $scope => $scope.x = 'test')
.directive('widget', widget)
.directive('render', render)
function widget() {
return {
template: '<div><input ng-model="stuff"/>I say {{stuff}}</div>'
}
}
function render($compile) {
return {
template: '<button ng-click="add()">{{name}}</button><hr/>',
link: linkFn
}
function linkFn(scope, elem, attr) {
scope.name = 'Add Widget';
scope.add = () => {
const newScope = scope.$new(true);
newScope.export = (data) => alert(data);
const templ = '<div>' +
'<widget></widget>' +
'<button ng-click="export(this.stuff)">Export</button>' +
'</div>';
const compiledTempl = $compile(templ)(newScope);
elem.append(compiledTempl);
}
}
}
I assume when you are talking about an isolate scope you are talking about a directive.
Here is an example of how to do it.
http://jsfiddle.net/rgaskill/PYhGb/
var app = angular.module('test',[]);
app.controller('TestCtrl', function ($scope) {
$scope.val = 'World';
});
app.factory('AlertFactory', function () {
return {
doWork: function(scope) {
scope.title = 'Fun';
//scope.title = scope.val; //notice val doesn't exist in this scope
}
};
});
app.controller('DirCtrl', function ($scope, AlertFactory) {
AlertFactory.doWork($scope);
});
app.directive('titleVal',function () {
return {
template: '<h1>Hello {{title}}</h1>',
restrict: 'E',
controller: 'DirCtrl',
scope: {
title: '='
},
link: function() {
}
};
});
Basically, attach a controller to a directive that has defined an isolate scope. The scope injected into the directive controller will be an isolate scope. In the directive controller you can inject your AlertFactory with wich you can pass the isolate scope to.