Unit test directive with $window and $event in Angular - angularjs

I have a super simple directive that overrides click behavior and does a full page reload on click. I'm having trouble writing a Unit Test for this directive. It looks like the $window doesn't get injected properly as well as this error when running the test:
TypeError: 'undefined' is not an object (evaluating '$event.preventDefault')
reload.directive.js
angular.module('myModule')
.directive('reload', ['$window', function($window) {
return {
restrict: 'A',
scope: {},
transclude: true,
replace: true,
template: '<a ng-click="reload($event)" ng-transclude></a>',
link: function(scope, element, attrs) {
scope.reload = function($event) {
$event.preventDefault();
$window.location.href = attrs.href;
};
}
};
}]);
An example of how I'm using it
<a ui-sref="home", reload>Home Example</a>
Here is my unit test: reload-test.directive.js
describe('Testing reload directive', function() {
beforeEach(module('myModule'));
var window, element, scope;
beforeEach(inject(function($compile, $rootScope, $window) {
scope = $rootScope.$new();
window = $window;
element = $compile('<a reload href="/"></a>')(scope);
scope.$digest();
}));
it('should reload the page with the right url', function() {
var compiledElementScope = element.isolateScope();
compiledElementScope.reload();
expect(window.location.href).toEqual('/');
});
});
UPDATED
Instead of doing any of this, I can just use target="_self" on links which triggers a full page reload.

Your test would be more natural if you will trigger an event.
element.triggerHandler('click');
Then your handler will be called by internal angular mechanisms.
Also your test will be failed when you trying to update window.location, because it causes full page reload. So, you need to mock window here:
var fakeWindow, element, scope;
beforeEach(module('myModule'));
beforeEach(function() {
// define fake instance for $window
module(function($provide) {
fakeWindow = {location: {}};
$provide.value('$window', fakeWindow)
});
});
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
element = $compile('<a reload href="/"></a>')(scope);
scope.$digest();
}));
it('should reload the page with the right url', function() {
var event = jasmine.createSpyObj('clickEvent', ['preventDefault']);
event.type = 'click';
element.triggerHandler(event)
expect(fakeWindow.location.href).toEqual('/');
expect(event.preventDefault).toHaveBeenCalled();
});
Now you can safely test your behaviour without side-effects.

Related

Unit Test angularjs directive with watch on element height

I am planning to unit test a angular directive using jasmine. My directive looks like this
angular.module('xyz.directive').directive('sizeListener', function ($scope) {
return {
link: function(scope, element, attrs) {
scope.$watch(
function() {
return Math.max(element[0].offsetHeight, element[0].scrollHeight);
},
function(newValue, oldValue) {
$scope.sizeChanged(newValue);
},
true
);
}
};
});
My unit test case is as follows
describe('Size listener directive', function () {
var $rootScope, $compile, $scope, element;
beforeEach(inject(function(_$rootScope_, _$compile_) {
$rootScope = _$rootScope_;
$compile = _$compile_;
$scope = $rootScope.$new();
element = angular.element('<span size-listener><p></p></span>');
$compile(element)($scope);
$scope.$digest();
}));
describe("Change in size", function () {
it("should call sizeChanged method", function () {
element[0].offsetHeight = 1;
$compile(element)($scope);
$scope.$digest();
$scope.$apply(function() {});
expect($scope.sizeChanged).toHaveBeenCalled();
});
});
});
The code works fine. But the unit test fails. The watch function gets called but element[0].offsetHeight in watch always returns 0. How can we update the element so that watch can observe the new height. Is there a way to even test this, because we don't really have a DOM here. Please guide me on changes that need to be done with unit test.
To get the offsetHeight of an element, it needs to be on the page, so you need to attach it to the document:
it("should call sizeChanged method", function () {
element[0].style.height = '10px';
$compile(element)($scope);
angular.element($document[0].body).append(element);
$scope.$apply();
expect($scope.sizeChanged).toHaveBeenCalled();
});
By the way, offsetHeight is read-only property so use style.height.
For more info: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetHeight
This post helped me find the answer: Set element height in PhantomJS

Angular $scope property undefined

I'm trying to set the $scope.sang property to an instance of SangService in a directive. Problem is, it doesn't appear to be getting set.
The Setup
sang-player.directive.js:
angular.module('sang').directive('sangPlayer', [
'SangService',
function(SangService) {
return {
restrict: 'EA',
scope: true,
link: function(scope, _elem, attr) {
window.console.log('link');
scope.sang = SangService.$new();
}
};
}]);
sang-player.directive.spec.js:
describe('The Sang Directive', function() {
var compile, scope, element, sang;
beforeEach(function() {
sang = jasmine.createSpyObj('sang',
['play', 'pause', 'playPause', 'previous', 'next', 'seek']);
module(function($provide) {
$provide.value('Sang', sang);
});
inject(function($compile, $rootScope) {
compile = $compile;
scope = $rootScope.$new();
element = getCompiledElement();
});
});
function getCompiledElement() {
var element = angular.element('<div sang-player></div>');
var compiledElement = compile(element)(scope);
scope.$digest();
return compiledElement;
}
it('creates a scope.sang object', function() {
expect(scope.sang).toBeDefined();
expect(typeof scope.sang).toBe('object');
});
});
Output from failed Jasmine spec run:
The Sang Directive
X creates a scope.sang object
Expected undefined to be defined. (1)
Expected 'undefined' to be 'object'. (2)
Not sure why there's no output from the link callback. I would expect this to get called within compile(element)(scope) or scope.$digest().
The Deets
Angular 1.4.1
Jasmine 2.4.1
grunt-contrib-jasmine 0.9.1
The full project repo lives here if you want to tinker.

Testing angularjs directive attributs with jasmine

I am relatively new to jasmine tests, and I've got some problem with it. I try to test this directive :
DIRECTIVE
myApp.LoadingsDirective = function() {
return {
restrict: 'E',
replace: true,
template: '<div class="loading"><img src="http://www.nasa.gov/multimedia/videogallery/ajax-loader.gif" width="20" height="20" /></div>',
link: function (scope, element, attrs) {
scope.$watch(
function(scope) {
return scope.$eval(attrs.show);
},
function(val) {
if (val){
$(element).show();
}
else{
$(element).hide();
}
})
}
}
}
myApp.directive('loading', myApp.LoadingsDirective);
This directive just show a loading icon until the result of a asynchronious request replace it.
I try something like this :
TEST
describe('Testing directives', function() {
var $scope, $compile, element;
beforeEach(function() {
module('myApp');
inject(function($rootScope, _$compile_) {
$scope = $rootScope.$new();
$compile = _$compile_;
});
});
it('ensures directive show the loading when show attribut is true', function() {
// GIVEN
var element = $compile('<div><loading show="true"> </loading></div>')($scope);
var loadingScope = element.find('loading').scope();
// WHEN
loadingScope.$watch();
// THEN
expect(loadingScope.show).toBe('true');
});
});
What is the best way to test this type of directive ? How to get access to attributs and test it ?
I always do it this way (coffeescript, but you'll get the idea):
'use strict';
describe 'Directive: yourDirective', ->
beforeEach module('yourApp')
# angular specific stuff
$rootScope = $compile = $scope = undefined
beforeEach inject (_$rootScope_, _$compile_) ->
$rootScope = _$rootScope_
$scope = $rootScope.$new()
$compile = _$compile_
# object specific stuff
element = createElement = undefined
beforeEach inject () ->
createElement = () ->
element = angular.element("<your-directive></your-directive>")
$compile(element)($scope)
$scope.$digest()
it "should have a headline", ->
createElement()
element.find("a").click()
$scope.$apply()
expect(element.find("input").val()).toEqual("foobar")
expect($scope.inputModel).toEqual("foobar")
And this could be the directive:
<your-directive>
<a ng-click="spanModel='foobar'">set inputModel</a>
<input ng-model="inputModel">
</your-directive>
First, I extract the creation of your element into a function. This allows you to do some initial setup before the directive is created.
Then I perform some actions on my directive. If you want to apply this actions into your scope (remember in jasmine you are NOT inside angulars' digest circle), you have to call $scope.$apply() or $scope.$digest() (can't remember right now what the exact difference was).
In the example above, you click on the <a> element, and this has a ng-click attached. This sets the inputModel scope variable.
Not tested, but you'll get the idea

$timeout, triggering app initialization in angular directive test and $httpBackend mock error

I'm having an issue with a test (Karma+Mocha+Chai). I'm testing a pretty simple directive, part of a bigger angular module (webapp). The issue is that, when calling $timeout.flush() in my test, the module/app get's initialized and makes a request to get the template for the homepage. As $httpBackend (part of ng-mock) is not expecting any request it fails:
Unexpected request: GET /partials/homepage
No more request expected
$httpBackend#/Users/doup/Sites/projects/visitaste-web/bower_components/angular-mocks/angular-mocks.js:1208:1
...
continues
How is possible that a directive is triggering the module initialization?? Any idea how to avoid this issue? Preferably without cutting this code into another module.
Thanks!
Here the directive:
module.exports = ['$timeout', function ($timeout) {
return {
restrict: 'A', // only for attribute names
link: function ($scope, element, attrs) {
$scope.$on('vtFocus', function (event, id) {
if (id === attrs.vtFocus) {
$timeout(function () {
element.focus();
}, 0, false);
}
});
}
};
}];
And here the actual test:
describe('vtFocus', function() {
var $scope, $timeout, element;
beforeEach(module('visitaste'));
beforeEach(inject(function ($injector, $compile, $rootScope) {
$scope = $rootScope.$new();
$timeout = $injector.get('$timeout');
element = angular.element('<input vt-focus="my-focus-id"/>');
$compile(element)($scope);
}));
it('should focus the element when a vtFocus event is broadcasted with the correct focus ID', function () {
expect(element.is(':focus')).to.be.false;
$scope.$broadcast('vtFocus', 'my-focus-id');
$timeout.flush();
expect(element.is(':focus')).to.be.true;
});
it('should NOT focus the element when a vtFocus event is broadcasted with a different focus ID', function () {
expect(element.is(':focus')).to.be.false;
$scope.$broadcast('vtFocus', 'wrong-id');
$timeout.flush();
expect(element.is(':focus')).to.be.false;
});
});
This is the part where I configure UI-Router for path / in app.config():
// ...
$stateProvider
.state('homepage', {
url: '/',
templateUrl: '/partials/homepage',
});
// ...
As a workaround, I just moved the directives into it's own module visitaste.directives and loaded that module in the test, so now it's sepparated from UI-Router and it doesn't trigger a request to the template.
Still I'll wait for another solution, before I accept this answer.
describe('vtFocus', function() {
var $scope, $timeout, element;
beforeEach(module('visitaste.directives'));
beforeEach(inject(function ($compile, $rootScope, _$timeout_) {
$scope = $rootScope.$new();
$timeout = _$timeout_;
element = angular.element('<input vt-focus="my-focus-id"/>');
element.appendTo(document.body);
$compile(element)($scope);
}));
afterEach(function () {
element.remove();
});
it('should focus the element when a vtFocus event is broadcasted with the correct focus ID', function () {
expect(document.activeElement === element[0]).to.be.false;
$scope.$broadcast('vtFocus', 'my-focus-id');
$timeout.flush();
expect(document.activeElement === element[0]).to.be.true;
});
it('should NOT focus the element when a vtFocus event is broadcasted with a different focus ID', function () {
expect(document.activeElement === element[0]).to.be.false;
$scope.$broadcast('vtFocus', 'wrong-id');
expect(document.activeElement === element[0]).to.be.false;
});
});

isolateScope() returns undefined when using templateUrl

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

Resources