Karma get scope of Angular directive - angularjs

I am using karma to load an angular directive (with the html2js plugin):
beforeEach(module('partials/myDir.html'));
beforeEach(inject(function($injector, $compile, $rootScope){
$gCompile = $compile;
$gScope = $rootScope;
}));
it("test test", function() {
element = $gCompile('<my-dir></my-dir>')($gScope);
$gScope.$digest();
console.log($gScope);
});
This all works fine, what I now want to do is access the directives scope from the $rootScope object injected in the beforeEach.

It depends upon your directive definition object. I don't see one in your question so I will answer for all three options.
Defining Scope or Child Scope:
This would be set by either doing scope: true or using the default scope value, which is false.
element.scope()
Isolate Scope:
This would be used if an Isolate Scope is created by your directive.
element.IsolateScope()

Related

Jasmine unit test for Angular directive

I have the following angular directive, which adds a tooltip when I hover over a span.
angular.module('mainMod')
.directive('toolTip', [function() {
return {
restrict: 'A',
scope: {
theTooltip: '#toolTip'
},
link: function(scope, element, attrs){
element.tooltip({
delay: 0,
showURL: false,
bodyHandler: function() {
return jQuery('<div class="hover">').text(scope.theTooltip);
}
});
}
}
}])
;
<span ng-show="data.tooltip" class="icon" tool-tip="{{data.tooltip}}"></span>
I'm looking to write a unit test for this directive, atm I can't use jasmine-jquery.
I'm fairly new to writing unit tests, could anyone possibly help me out?
Give me some pointers or point me towards some helpful resources?
Any advice or suggestions would be greatly appreciated.
What I have atm isn't much...
describe('Unit testing tooltip', function() {
var $compile;
var $rootScope;
// Load the myApp module, which contains the directive
beforeEach(module('mainMod'));
// Store references to $rootScope and $compile
// so they are available to all tests in this describe block
beforeEach(inject(function(_$compile_, _$rootScope_){
// The injector unwraps the underscores (_) from around the parameter names when matching
$compile = _$compile_;
$rootScope = _$rootScope_;
}));
it(' ', function() {
// Compile a piece of HTML containing the directive
FAILS HERE --> var element = $compile("<span class='icon' tool-tip='{{data.tooltip}}'></span>")($rootScope);
$rootScope.$digest();
});
});
It's failing with a message of
TypeError: undefined is not a function
I think it's being caused by the ($rootScope) at the end of the line I've specified above.
You have to wrap your DOM content with angular.element first before compiling it. I am not sure what the tooltip module you are using but I used the jQuery UI tooltip instead.
//create a new scope with $rootScope if you want
$scope = $rootScope.$new();
var element = angular.element("<span class='icon' tool-tip='This is the tooltip data'></span>");
//use the current scope has just been created above for the directive
$compile(element)($scope);
One more thing, because you are using isolate scope in your directive, to get the current scope from your directive, you need to call
element.isolateScope()
based on this reference : How to Unit Test Isolated Scope Directive in AngularJS
For a working fiddle, you can found it here : http://jsfiddle.net/themyth92/4w52wsms/1/
Any unit test is basically the same - mock the environment, construct the unit, check that the unit interacts with the environment in the expected manner. In this instance, you'd probably want to mock the tooltip creator
spyOn(jQuery.fn, 'tooltip');
then compile some template using the directive (which you're already doing), then simulate the hover event on the compiled element and then check that the tooltip creator was called in the expected manner
expect(jQuery.fn.tooltip).toHaveBeenCalledWith(jasmine.objectContaining({
// ... (expected properties)
}));
How you simulate the event depends on how the element.tooltip is supposed to work. If it really works the way you're using it in the question code, you don't need to simulate anything at all and just check the expected interaction right after template compilation.

Cannot test directive scope with mocha

I have a simple directive with an isolated scope that I'm trying to test. The problem is that I cannot test the scope variables defined in the link function. A watered down sample of my directive and spec below.
Directive:
angular.module('myApp').directive('myDirective', [function(){
return {
scope: {},
restrict: 'AE',
replace: 'true',
templateUrl: 'template.html',
link: function link(scope, element, attrs){
scope.screens = [
'Screen_1.jpg',
'Screen_2.jpg',
'Screen_3.jpg',
];
}
};
}]);
Spec
describe.only('myDirective', function(){
var $scope, $compile;
beforeEach(module('myApp'));
beforeEach(inject(function(_$rootScope_, _$compile_, $httpBackend){
$scope = _$rootScope_.$new();
$compile = _$compile_;
$httpBackend.when('GET', 'template.html').respond();
}));
function create(html) {
var elem, compiledElem;
elem = angular.element(html);
compiledElem = $compile(elem)($scope);
$scope.$digest();
return compiledElem;
};
it('should have an isolated scope', function(){
var element = create('<my-directive></my-directive>');
expect(element.isolateScope()).to.be.exist;
});
});
According to what I've been reading online, this should be working. So after hours of research>fail>repeat I'm leaning to think that it's a bug or a problem with my source code versions. Any ideas?
NOTE I'm testing with Angular 1.2.17, jQuery 1.11.0, Karma ~0.8.3, and latest Mocha
UPDATE 9/19
I updated my directive above, for simplicity's sake I had written down an inline template, but my actual code has an external templateUrl. I also added an $httpBackend.when() to my test to prevent the test from actually trying to get the html file.
I noticed that inlining the template makes everything work fine, but when I use an external template it doesn't fire off the link function.
UPDATE 9/19
I integrated html2js, and now I am able to actually load up the templates from cache and trigger the link function. Unfortunately, isolateScope() is still coming up undefined.
For anyone with the same issue as me, below is the solution.
In MOCHA, if you're using external html templates for you directives, you must use the ng-html2js preprocessor which will cache all your templates in a js file. Once you have this setup, karma will read these instead of trying to fetch the actual html file (this will prevent the UNEXPECTED REQUEST: GET(somepath.html) error).
After this is properly set up. You directive link function or controller scope will be available to your test via isolateScope(). Below is the updated code:
Spec
describe('myDirective', function(){
var $scope, $compile;
beforeEach(module('myApp'));
beforeEach(module('ngMockE2E')); // You need to declare the ngMockE2E module
beforeEach(module('templates')); // This is the moduleName you define in the karma.conf.js for ngHtml2JsPreprocessor
beforeEach(inject(function(_$rootScope_, _$compile_){
$scope = _$rootScope_.$new();
$compile = _$compile_;
}));
function create(html) {
var compiledElem,
elem = angular.element(html);
compiledElem = $compile(elem)($scope);
$scope.$digest();
return compiledElem;
};
it('should have an isolated scope', function(){
var elm = create('<my-directive></my-directive>');
expect(elm.isolateScope()).to.be.defined;
});
});
NOTE I ran into issues where I still got the UNEXPECTED REQUEST: GET... error after I thought I had everything set up correctly, and it turned out that my cached files had a different path than the actual file being requested, look at this post for more help:
Karma 'Unexpected Request' when testing angular directive, even with ng-html2js

AngularJS - exposing controller api to directive

How do I make controller functions visible to a directive? Do I attach the methods to the scope, and inject the scope into the directive? Is it a good idea in the first place? I want to manipulate model data from within the UI.
It really dependes on what you want to do.
Since you want to access the controller's scope from the directive, I suggest you declare your directive with it's scope shared with the parent controller by setting it's scope prop to false:
app.directive('directiveName', function() {
scope: false,
link: function(scope) {
// access foo from the controler's scope
scope.foo;
}
});
This is a nice example with how directives can be hooked up to a controller
http://jsfiddle.net/simpulton/GeAAB/
DIRECTIVE
myModule.directive('myComponent', function(mySharedService) {
return {
restrict: 'E',
controller: function($scope, $attrs, mySharedService) {
$scope.$on('handleBroadcast', function() {
$scope.message = 'Directive: ' + mySharedService.message;
});
},
replace: true,
template: '<input>'
};
});
HOOKUP IN CONTROLLER
sharedService.broadcastItem = function() {
$rootScope.$broadcast('handleBroadcast');
};
VIEW
<my-component ng-model="message"></my-component>
Adding to #grion_13 answer, scope:true would also work since it creates a new scope that is child of the parent scope so has access to parent scope data.
But a true reusable directive is one which get it input data using isolated scope. This way as long as your html+ controller can provide the right arguments to the directive isolated scope, you can use the directive in any view.

Why $rootScope.$new does not let template into the directive?

I'm building unit testing using Karma and Mocha. Testing my directives, and using html2js (It converts the htmls to cached strings in $templateCache).
Interestingly, when using $rootScope.$new() in my test, the template html will not get into the directive . Here's the code:
it('should show a thumb name', function() {
inject(function($compile, $rootScope,$controller) {
var scope = $rootScope;//.$new() ($new not working. Why?)
var linkFn = $compile('<thumb></thumb>');
var element = linkFn(scope);
scope.$digest(); // <== needed so that $templateCache will bring the html
// (that html2js put in it)
console.log(element.html());// correctly returns thumb's directive templateUrl content
}));
...
However, if I use scope = $rootScope.$new(), the element.html() will return an empty string
any ideas?
many thanks
Lior
According to the docs for $digest (http://docs.angularjs.org/api/ng.$rootScope.Scope), this will only process watchers etc for the current scope and its children.
This suggests to me that when you set scope = $rootScope and then $digest you will be processing watchers etc on the $rootScope, I think this is where promises will be resolved too, releasing your templates. When you do scope = $rootScope.$new() and call $digest on that, I expect anything that should happen from the $rootScope doesn't happen.
So, does this work if you change scope.$digest() to $rootScope.$digest() or scope.$apply()?

Testing angular with karma: how to set scope

I got a fairly complex Angular directive, called hy-plugin-window, that I use inside an ng-repeat block (complete directive code here):
<div ng-repeat="pluginD in pluginsDisplayed">
<div hy-plugin-window content="pluginD" remove="remove"></div>
</div>
The directive uses an isolate scope, wich is bi-directionally linked to the parent's content member:
return {
restrict: 'A',
replace: true,
link: linker,
scope: {
content:'=',
}
};
This means that, when it's created, every directive in the list can access its unique content (which is a single member of the object pluginsDisplayed) and do things like:
scope.statusMessage = 'Loading ' + scope.content.name + '...';
Everything works, except that I don't know how to test it with Karma. With a fairly common test like this I hoped to manually set the scope inside of the directive:
'use strict';
describe('Directive: hyPluginWindow', function () {
beforeEach(module('hyacinthHostApp'));
var element, $compile, scope;
beforeEach(inject(function(_$compile_, $rootScope) {
$compile = _$compile_;
scope = $rootScope.$new();
}));
it('should do something', inject(function () {
scope.content = {
name: "testElement",
id: "testElement000",
uiType: "canvas"
};
element = angular.element('<div hy-plugin-window></div>');
element = $compile(element)(scope);
//expect(...);
}));
});
But it miserably fails with:
TypeError: Cannot read property 'name' of undefined
The first time my directive tries to access its scope.content.name.
Considering that I'm a testing complete newbie, what am I missing?
Short story: change the compiled element to <div hy-plugin-window content="content"></div> and I think it should work.
The directive has an ISOLATE scope, which means it does not prototypically inherit from the "regular" scope.
In your test, you are defining content on the regular scope, but the directive itself is encapsulated - it has isolated scope and therefore does not see the content property defined on the regular scope.
You need to communicate that through the explicit interface of the component, which is content attribute.

Resources