Cannot test directive scope with mocha - angularjs

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

Related

Unit testing AngularJS directive, postLink D3 not changing DOM

I'm trying to unit test an AngularJS directive using Jasmine. The directive is meant to add SVG elements to the template in the postLink phase using D3js. This works fine in the actual application.
In the unit test it appears that the D3 code is never executed.
This is a simplified version of the code that still reproduces the error:
angular.module('app', []);
angular.module('app').directive('d3Test', function () {
return {
template: '<div id="map"></div>',
restrict:'E',
scope: {
someAttr: '#',
},
link: function postLink(scope, element, attrs) {
d3.select('#map').append('svg');
}
};
});
This is the unit test:
describe('directive test', function () {
var element, scope;
beforeEach(module('app'));
beforeEach(inject(function ($rootScope, $compile) {
scope = $rootScope.$new();
element = '<d3-test></d3-test>';
element = $compile(element)(scope);
scope.$apply();
}));
it('should have an SVG child element', function () {
expect(element.html()).toEqual('<div id="map"><svg></svg></div>');
});
});
The error I receive is:
PhantomJS 1.9.8 (Mac OS X) directive test should have an SVG child element FAILED
Expected '<div id="map"></div>' to equal '<div id="map"><svg></svg></div>'.
Is my expectation wrong on how I can test for the DOM changes D3 will make?
How can I best test this?
the problem is very simple.
The d3.select method is searching for the element in the window.document.
In your test case your element is in a detached dom element and not part of the window.document.
To fix this you can get the node directly via the directive's element instead of using a global selector.
d3.select(element.find('div#map')[0]).append('svg');
Note: This code works with angular+jQuery. If you don't use jQuery inside of your project you may need to adapt the selector because then your are limited to lookups by tag name.
https://docs.angularjs.org/api/ng/function/angular.element

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.

AngularJS templateUrl vs template - isolate scope

I have the following directive:
offerListSorters.directive('offersSorter', ['myState', '$templateCache', function (myState, $templateCache){
return {
scope: {},
controller: function($scope, $element, $attrs, $transclude) {
[...]
},
restrict: 'E',
//templateUrl: 'partials/offersSorterDirective.html',
template: $templateCache.get('partials/offersSorterDirective.html'),
replace: true,
transclude: true
};
}]);
And I use Karma + Jasmine to test this code and it works. But now if I switch to the templateUrl (currently commented out), it doesn't work. I've created a simple Plunker to show this issue. When you compare sorter and bsorter directives, it looks as if the isolateScope() call on the compiled element breaks when I use templateUrl instead of template. Any ideas?
It's the weirdest thing, I think this is actually a bug where if you're using a templateUrl, you don't get an isolated scope. The template itself is loaded correctly, but the scope is never loaded. I've updated the plnkr with some additional logging, checkout out the console and you'll see what I mean, bsorter doesn't get the ng-isolate-scope class and the scope is returned as undefined.
Edit:
I've updated the plnkr further to log a console message when the compile function is called. This is pretty much the limit of my javascript/angularJS knowledge, but the bsorter compile function is logged as being called after the scope should be returned, unlike the sorter compile function which is called before.
You should call isolateScope() in the it() function, not in beforeEach().
describe("Testing...", function(){
var element, isoScope;
beforeEach(function(){
module("myApp");
inject(function($rootScope, $compile){
var scope = $rootScope.$new();
element = $compile(angular.element("<sorter></sorter>"))(scope);
// isoScope = element.isolateScope(); <- Move from here
$rootScope.$digest();
});
});
it("something...", function(){
isoScope = element.isolateScope(); // <- to here
expect(isoScope.someProp).toBe("someValue");
});
});
You have to call $rootScope.$digest() after creating the directive via $compile, then it should work (right now you're calling the $digest() on the on your variable parentScope with is a new scope created with $rootScope.$new())

Karma get scope of Angular directive

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()

Angular directive not linking in test

To get familiar with directive testing I created the simple example shown below. Unfortunately, the test is failing and it seems that the link function is never called. The directive does work when used within an app.
I have tried hardcoding the message attribute, removing the condition within the link function and even extracting the attr set from within the $watch, but the test still fails.
There has been other posts like this and the reason for those was due to the lack of a $digest call, but I do have that and I have tried moving it into the it spec block.
If I run a console.log(elem[0].otherHTML) call the scope binding seems to work
<wd-alert type="notice" message="O'Doole Rulz" class="ng-scope"></wd-alert>
What am I missing?
alert.spec.js
"use strict";
describe('Alert Specs', function () {
var scope, elem;
beforeEach(module('myapp'));
beforeEach(inject(function ($compile, $rootScope) {
scope = $rootScope;
scope.msg = "O'Doole Rulz";
elem = angular.element('<wd-alert type="notice" message="{{msg}}"></wd-alert>');
$compile(elem)(scope);
scope.$digest();
}));
it('shows the message', function () {
expect(elem.text()).toContain("O'Doole Rulz");
});
});
alert.js
angular.module('myapp').directive('wdAlert', function() {
return {
restrict: 'EA',
replace: true,
template: '<div></div>',
link: function(scope, element, attrs) {
attrs.$observe('message', function() {
if (attrs.message) {
element.text(attrs.message);
element.addClass(attrs.type);
}
})
}
}
});
Turns out the issue was a Karma configuration for the way I had the files organized. I will leave this question up just in case it bites someone else in the ass.
files: [
...,
'spec_path/directives/*js' // what I originally had
'spec_path/directives/**/*.js' // what I needed
]
I'm adding this to expand on the answer for anyone else who wants to know.
I installed a third-party directive via bower. It worked in the app, but caused tests to break.
The problem is that Grunt/Karma doesn't read the scripts in your index.html. Instead, you need to make sure you let karma know each installed script.
For example, I used the directive ngQuickDate (a datepicker). I added it to index.html for the app, but I needed to add it to karma.conf.js as well:
files: [
...
'app/bower_components/ngQuickDate/dist/ng-quick-date.js'
]
The answer above does the same thing by means of using the ** wildcard to mean "in all subdirectories recursively".
Hope that helps!

Resources