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.
Related
how to check attribute is present in HTML and match its value. this is a test spec.js I wrote,
define(['angular',
'angularMocks',
'site-config',
'ng_detector',
],
function(angular,
mock,
$app,
ng_detector) {
describe('ng-detector controller', function() {
beforeEach(angular.mock.module("webapp"));
var $compile, $rootScope, tpl, $scope, elm, templateAsHtml;
beforeEach(angular.mock.inject(function(_$compile_, _$rootScope_) {
$compile = _$compile_;
$rootScope = _$rootScope_;
// $scope = _$rootScope_.$new();
}));
it('should initialize the ng-detector directive', inject(function() {
var tpl = $compile("<div ng-detector ></div>")($rootScope);
$rootScope.$digest();
console.log(tpl) // Log: r{0: <div ng-detector="" class="ng-scope" ng-verison="1.6.4"></div>, length: 1}
templateAsHtml = tpl[0].outerHTML;
expect(templateAsHtml.attr('ng-version')).toEqual(angular.version.full);
}));
});
});
directive. that adds angular version to attribute ng-version
'use strict';
define(['app-module'], function(ng) {
$app.info('ng detector initialized. {file: directives/ng-detector.js}');
ng.directive('ngDetector', function() {
return {
restrict: "A",
link: function(scope, elm, attr) {
elm.attr('ng-version', angular.version.full);
}
};
});
return ng;
});
I want to get a ng-version attribute set by the directive and match the attribute value.
I figured out myself. I was looking at the different place.
it('should check the angular version number', angular.mock.inject(function() {
expect(tpl.attr('ng-version')).toEqual(angular.version.full);
}));
I'm trying to unit test my directive that set form validity depending on a controller variable.
My directive code :
angular.module('myModule',[])
.directive('myDirective', function() {
return {
restrict: 'A',
link: function(scope, element, attr, ctrl) {
scope.$watch("mailExist", function(){
if(scope.mailExist) {
ctrl.$setValidity('existingMailValidator', false);
} else {
ctrl.$setValidity('existingMailValidator', true);
}
});
}
};
});
When trying to unit test this directive, I'm trying to isolate the controller ctrl with this code:
describe('directive module unit test implementation', function() {
var $scope,
ctrl,
form;
beforeEach(module('myModule'));
beforeEach(inject(function($compile, $rootScope) {
$scope = $rootScope;
var element =angular.element(
'<form name="testform">' +
'<input name="testinput" user-mail-check>' +
'</form>'
);
var ctrl = element.controller('userMailCheck');
$compile(element)($scope);
$scope.$digest();
form = $scope.testform;
}));
describe('userMailCheck directive test', function() {
it('should test initial state', function() {
expect(form.testinput.$valid).toBe(true);
});
});
});
Running this test, I still obtain:
Cannot read property '$setValidity' of undefined
that's mean I haven't really inject a controller.
What is wrong in my test?
Finally in found the solution:
first in code I have add :
require: 'ngModel',
and then modified the unit test as follow:
describe('directive module unit test implementation', function() {
var scope,
ngModel,
form;
beforeEach(module('myModule'));
beforeEach(inject(function($compile, $rootScope) {
scope = $rootScope.$new();
var element =angular.element(
'<form name="testform">' +
'<input name="testinput" ng-model="model" user-mail-check>' +
'</form>'
);
var input = $compile(element)(scope);
ngModel = input.controller('ngModel');
scope.$digest();
form = scope.testform;
}));
describe('userMailCheck directive test', function() {
it('should test initial state', function() {
expect(form.testinput.$valid).toBe(true);
});
});
});
and everything works fined.
I have a custom directive that uses a service. When I run my unit tests, I keep getting the thrown error 'Library does not exist on window'. How can I avoid getting that error in my Unit test?
example service
angular.module('example')
.factory('thirdParty', ['$window', function($window) {
if (typeof $window.thirdParty === 'undefined') {
throw new Error('Library does not exist on window');
} else {
return $window.thirdParty;
}
}]);
custom directive
angular.module('example')
.directive('customDirective', ['thirdParty',
function(thirdParty) {
var defaults, link;
link = function(scope, element, attrs, ctrls) {
// do something with thirdParty
};
return {
restrict: 'A',
require: 'ngModel',
link: link,
};
}]);
test
describe('customDirective', function() {
var element, compile, scope;
beforeEach(module('example'));
beforeEach(inject(function($compile, $rootScope) {
compile = $compile;
scope = $rootScope.$new();
}));
// Manually compile and link our directive
function getCompiledElement(template) {
var compiledElement;
var validTemplate = '<input ng-model="example.data" custom-directive />';
compiledElement = compile(template || validTemplate)(scope);
scope.$digest();
return compiledElement;
}
it('should do something', function() {
element = getCompiledElement();
// expects
});
});
You need to inject $window (stub that out too, for good measure) and stub/mock out the thirdParty lib.
var $window;
beforeEach(function () {
$window = {};
module('yourmodule', function ($provide) {
$provide.value('$window', $window);
});
$window.thirdParty = {};
});
it('throws', function () {
delete $window.thirdParty;
function fn () {
getCompiledElement();
}
expect(fn).to.throw(/does not exist on window/i);
});
it('just works™', function () {
function fn () {
getCompiledElement();
}
expect(fn).to.not.throw;
});
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.
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();
});
});