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!
Related
From what I understand it only runs once before the page is rendered. But is there ever a case where it runs after the page has been rendered?
I tried testing a few things with this plnkr:
angular
.module('app', [])
.directive('test', function($http) {
return {
restrict: 'E',
template: '<input />',
link: function(scope, el, attrs) {
var input = angular.element(el[0].children[0]);
input.on('change', function() {
console.log('change event');
scope.$apply(function() {
console.log('digest cycle');
});
});
input.on('keyup', function() {
console.log('keyup event');
var root = 'http://jsonplaceholder.typicode.com';
$http.get(root+'/users')
.success(function() {
console.log('http request successful');
})
.error(function() {
console.log('http request error');
});
});
console.log('link function run');
}
};
});
Does typing in the input field cause the link function to run?
Do event listeners cause the link function to run?
Do http requests (made with $http?) cause the link function to run?
Do digest cycles cause the link function to run?
The answer to all of these questions seem to be "no".
The link function runs when an instance of the directive is compiled, in this case, when a <test></test> element is created. That can be when angular's bootstrapping compiles the page, when it comes into being from a ng-if, when a ng-repeat makes it, when it's made with $compile, etc.
link will never fire twice for the same instance of the directive. Notably, it fires right after the template has been compiled in the directive's lifecycle.
1 - No, it causes to change the only ng-model if you have it binded.
2 - No, it will only launch the code inside the event binds.
3 - Again no, the event bind will launch the $http.get(). And please don't put an $http directly on your directive. Use a factory or something like that.
4 - Dunno
As Dylan Watt said, the directive link runs only when the directive is compiled (only once) per element/attr.... You can compile it in different ways. Plain http, $compile, ng-repeat....
You can create a $watch inside your directive to "relaunch" some code on a binded element change.
This maybe can help you: How to call a method defined in an AngularJS directive?
I'm writing two directives that wrap ui-bootstrap's tabset and tab directives.
In order for the content of my directives to be passed to the wrapped directives, I'm using transclusion in both of them.
This works quite well, the only problem is that I'm failing at writing a test that checks that. My test uses a replacement directive as a mock for the wrapped directive, which I replace using $compileProvider before each test.
The test code looks something like this:
beforeEach(module('myModule', function($compileProvider) {
// Mock the internally used 'tab' which is a third party and should not be tested here
$compileProvider.directive('tab', function() {
// Provide a directive with a high priority and 'terminal' set to true, makes sure that
// the mock directive will get executed, and that the real directive will not
var mock = {
priority: 100,
terminal: true,
restrict: 'EAC',
replace: true,
transclude: true,
template: '<div class="mock" ng-transclude></div>'
};
return mock;
});
}));
beforeEach(function() {
inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
});
});
beforeEach(function() {
$scope = $rootScope.$new();
});
afterEach(function() {
$scope.$destroy();
});
it('Places the enclosed html inside the tab body', function() {
element = $compile("<div><my-tab>test paragraph</my-tab></div>")($scope);
$scope.$digest();
console.log("element.html() = ", element.html());
expect(element.text().trim()).toEqual("test paragraph");
});
The template of my directive looks something like this:
<div><tab><div ng-transclude></div></tab></div>
The directive module looks something like this:
angular.module('myModule', ['ui.bootstrap'])
.directive('myTab', function() {
return {
restrict: 'E',
replace: true,
transclude: true,
templateUrl: 'templates/my-tab.tpl.html',
scope: {
}
};
});
The result of the print to the console is this:
LOG: 'element.html() = ', '<div class="ng-isolate-scope" id=""><div id="" heading="" class="mock"><ng-transclude></ng-transclude></div></div>'
Any ideas on why the transclusion doesn't take place (again, it works outside of the test just fine) ?
Update
I've since moved on to other things and directives, and ran into this issue again, but now it's more crucial, the reason being, that the directive I place inside the parent directive, requires the parent controller in its link function.
I've done more research into this, and it turns out that for some reason, compiling the mock directive doesn't create an instance of the transcluded content.
The reason I know that, is that I've placed a printout in every hook possible in both directives (both the mock and the transcluded one), i.e. compile, pre-link, post-link and controller constructor, and I see that the only printouts are from the mock directive.
Now, here's the really interesting part: I've tried using the transclude function in the mock directive's link function to "force" the compilation of the transcluded directive, which worked ! (another proof that it didn't take place implicitly).
Where's the catch you ask ? Well, it still doesn't work. This time, since the link function of the transcluded directive fails since it doesn't find the controller of the mock directive. What ?!
Here's the code:
Code
var mod = angular.module('MyModule', []);
mod.directive('parent', function() {
return {
restrict: 'E',
replace: true,
template: '<div class="parent">...</div>',
controller: function() {
this.foo = function() { ... };
}
};
});
mod.directive('child', function() {
return {
restrict: 'E',
require: '^parent',
link: function(scope, element, attrs, parentCtrl) {
parentCtrl.foo();
}
};
});
Test
describe('child directive', function() {
beforeEach(module('MyModule', function($compileProvider) {
$compileProvider.directive('parent', function() {
return {
priority: 100,
terminal: true,
restrict: 'E',
replace: true,
transclude: true,
template: '<div class="mock"><ng-transclude></ng-transclude></div>',
controller: function() {
this.foo = jasmine.createSpy();
},
link: function(scope, element, attrs, ctrls, transcludeFn) {
transcludeFn();
}
};
});
}));
});
This test fails with an error message such as:
Error: [$compile:ctreq] Controller 'parent', required by directive
'child', can't be found!
Any thoughts, ideas, suggestions would be highly appreciated.
Ok, probably the shortest bounty ever in the history of SO ...
The problem was with the terminal: true and priority: 100 properties of the mock directive. I was under the impression (from an article I read online about how to mock directives), that these properties cause the compiler to stop compiling directives with the same name and prioritize the mock directive to be evaluated first.
I was obviously wrong. Looking at this and this, it becomes clear that:
'terminal' stops any other directives that were not processed yet from being processed
'priority' is used to make sure that the mock directive is processed before the directive it is mocking
The problem, is that this causes all other processing to stop, including the ng-transclude directive, which has the default priority of 0.
However, removing these properties causes all hell to break loose, since both directives have been registered, and so forth (I won't burden you with all the gory details). In order to be able to remove these properties, the two directives should reside in different modules, and there should be no dependency between them. In short, when testing the child directive, the only directive named parent that's evaluated should be the mock directive.
In order to support real life usage, I've introduced three modules to the system:
A module for the child directive (no dependencies)
A module for the parent directive (no dependencies)
A module that has no content, but has a dependency on both child and parent modules, which is the only module you'll ever need to add as a dependency in your code
That's pretty much it. I hope it helps anyone else that runs into such problems.
I'm trying to create a directive that will allow me to set certain configurations to ui-grid as well as to add some functionality to it. I got something basic working, but the problem is that the jasmine test is giving me a hard time.
The JS code looks like this:
angular.module('myGridDirective', ['ui.grid'])
.directive('myGrid', function() {
return {
restrict: 'E',
replace: true,
templateUrl: 'templates/my-grid.html'
};
});
The template looks like this:
<div><div id="test-id" class="gridStyle" ui-grid="gridOptions"></div></div>
And the test looks like this:
describe('my grid directive', function() {
var $compile,
$rootScope;
beforeEach(module('myGridDirective'));
beforeEach(module('templates/my-grid.html'));
beforeEach(function() {
inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
});
});
it('Replaces the element with an ui-grid directive element', function() {
var element = $compile("<my-grid></my-grid>")($rootScope);
$rootScope.$digest();
expect(element.html()).toEqual('<div id="test-id" class="gridStyle" ui-grid="gridOptions"></div>');
});
});
The problem is that, while the directive is working (i.e. using <my-grid></my-grid> anywhere in my html file works), the test is failing.
I get the error message:
TypeError: $scope.uiGrid is undefined in .../angular-ui-grid/ui-grid.js (line 2879)
The relevant line in ui-grid.js is (the first line is 2879):
if (angular.isString($scope.uiGrid.data)) {
dataWatchCollectionDereg = $scope.$parent.$watchCollection($scope.uiGrid.data, dataWatchFunction);
}
else {
dataWatchCollectionDereg = $scope.$parent.$watchCollection(function() { return $scope.uiGrid.data; }, dataWatchFunction);
}
The thing is, if I replace the ['ui.grid'] array in the directive module creation with an empty array, the test passes. The only problem, is that if I do that, I'll have to include 'ui.grid' anywhere the directive is used otherwise the directive stops working, which is something I cannot do.
I already tried transcluding, but that didn't seem to help, not to mention that the directive itself works, so it doesn't seem logical to have to do that just for the test.
Any thoughts ?
Ok, I figured out the solution.
At first found one way to solve this, which is:
Initialize the gridOptions variable with some data so that the ui-grid will get constructed
However, once I got that to work, I tried to add 'expect' statements, when it hit me that now I have a lot of 3rd party html to test, which is not what I want.
The final solution was to decide to mock the inner directive (which should be tested elsewhere), and to use the mock's html instead.
Since ngMock does not support directives, I found this great article explaining how to mock directives using $compileProvider, which solved my problem altogether.
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
I have page:
<div>111</div><div id="123" ng-init='foo=555'>{{foo}}</div>
in browser:
111
555
Code js refresh id=123 and get new html. I get:
<div id="123" ng-init='foo="444new"'><span>..</span><b>NEW TEXT<b> {{foo}}</div>
in browser
111
...NEW TEXT {{foo}}
I want get in browser:
111
...NEW TEXT 444new
Is it possible to re-run the initialization angular in this situation?
DEMO: jsfiddle.net/UwLQR
Solution for me: http://jsbin.com/iSUBOqa/8/edit - this BAD PRACTICE!
UPD two months later: My God, what nonsense I wrote. :(
See my notes in the included code and the live demo here (click).
The two reasons that angular will not register data-binding or directives are that the element isn't compiled, or the change happens outside of Angular. Using the $compile service, the compile function in directives, and $scope.$apply are the solutions. See below for usage.
Sample markup:
<div my-directive></div>
<div my-directive2></div>
<button id="bad-button">Bad Button!</button>
Sample code:
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope) {
$scope.foo = '123!';
$scope.bar = 'abc!';
//this is bad practice! just to demonstrate!
var badButton = document.getElementById('bad-button');
badButton.addEventListener('click', function() {
//in here, the context is outside of angular, so use $apply to tell Angular about changes!
$scope.$apply($scope.foo = "Foo is changed!");
});
});
app.directive('myDirective', function($compile) {
return {
restrict: 'A',
link: function(scope, element, attrs) {
//when using a link function, you must use $compile on the element
var newElem = angular.element('<div>{{foo}}</div>');
element.append(newElem);
$compile(newElem)(scope);
//or you can use:
//$compile(element.contents())(scope);
}
};
});
app.directive('myDirective2', function($compile) {
return {
restrict: 'A',
compile: function(element, attrs) {
//compile functions don't have access to scope, but they automatically compile the element
var newElem = angular.element('<div>{{bar}}</div>');
element.append(newElem);
}
};
});
Update based on your comment
It makes me cringe to write this, but this is what you would need to make that code work.
var elem = document.getElementById('123');
elem.innerHTML = "<div ng-init=\"foo='qwe123'\">{{foo}}</div>";
$scope.$apply($compile(elem)($scope));
Just as I said, you need to compile the element AND, since that is in an event listener, you need to use $apply as well, so that Angular will know about the compile you're doing.
That said, if you're doing anything like this at all, you REALLY need to learn more about angular. Anything like that should be done via directives and NEVER with any direct DOM manipulation.
Try next:
$scope.$apply(function() {
// your js updates here..
});
or
$compile('your html here')(scope);
Look $compile example at bottom of page.