I just realized that none of the tests I have written for my directive are working in a way that actually tests anything so I am rewriting them. Problem is I cant figure out a way to test the link functions.
If anyone has a suggestion I would really really appreciate it.
Thanks for looking
angular.module('pb.webSites.directives')
.directive('pbOrganizationImagePicker', [ function () {
return {
restrict: "E",
template: '<img data-ng-src="{{ imageSource }}" width="{{width}}" height="{{height}}" alt="Image Picker" class="img-rounded" />',
scope: {
fileId: '=pbFileId',
defaultSrc: '#pbDefaultSrc',
width: '#pbWidth',
height: '#pbHeight'
},
controller: 'pbOrganizationImagePickerController',
link: function (scope, element, attrs) {
scope.$watch('defaultSrc', function (value) {
if (value !== undefined) {
scope.imageSource = value;
}
});
element.on('click', function () {
scope.pickImage().then(function (image) {
scope.imageSource = image.storageUrl;
scope.fileId = image.fileId;
}, function () {
console.log('Modal dismissed at: ' + new Date());
});
});
}
};
}]);
Someone asked what I was doing so far to test this directive. Right now the link sections tests are a mess but I thought I would post it here in case anyone else was curious.
describe('pbOrganizationImagePicker', function () {
beforeEach(module('pb.webSites.controllers'));
beforeEach(module('pb.webSites.directives'));
beforeEach(module('ui.router'));
beforeEach(module('ui.bootstrap'));
var compile;
var scope;
var html = angular.element('<pb-organization-image-picker data-pb-default-src="{{ webSite.profileImageUrl || \'/content/img/placeholder-lg.jpg\' }}" data-pb-file-id="webSite.profileImageId" data-pb-width="200"></pb-organization-image-picker>');
beforeEach(inject(function ($compile, $rootScope) {
compile = $compile
scope = $rootScope.$new();
}));
beforeEach(inject(function ($q, $injector) {
$httpBackend = $injector.get('$httpBackend');
$httpBackend.whenGET('/app/webSites/directives/OrganizationImagePicker.html').respond(200, '');
scopeObject = {
profileImageUrl: 'http://www.gravatar.com/avatar/12345?s=40&d=identicon',
profileImageId: 54634
};
scope.webSite = {
profileImageId: 6436
};
scope.pickImage = function () {
var defer = $q.defer();
defer.resolve(scopeObject);
return defer.promise;
};
}));
describe('template with profileImageUrl', function () {
beforeEach(function () {
scope.webSite.profileImageUrl = 'http://www.gravatar.com/avatar/12345?s=40&d=identicon';
element = compile(html)(scope);
});
it('should render HTML based on scope correctly', function () {
expect(element.isolateScope().width).toEqual('200');
expect(element.isolateScope().fileId).toEqual(scope.webSite.profileImageId);
expect(element.isolateScope().defaultSrc).toEqual(scope.webSite.profileImageUrl);
});
});
describe('template without profileImageUrl', function () {
beforeEach(function () {
element = compile(html)(scope);
});
it('use default URL if there is no profileImageUrl', function () {
expect(element.isolateScope().defaultSrc).toEqual('/content/img/placeholder-lg.jpg');
});
});
describe('element.click()', function () {
beforeEach(function () {
var html = angular.element('<img data-ng-src="{{ imageSource }}" width="{{width}}" height="{{height}}" alt="Image Picker" class="img-rounded" />');
});
it('should assign value to scope variables', function () {
template = compile(html)(scope);
template.triggerHandler('click');
template.scope().$apply();
scope.pickImage();
scope.$digest();
expect(scope.imageSource).toEqual(scopeObject.profileImageUrl);
expect(template.isolateScope().fileId).toEqual(scopeObject.profileImageId);
});
});
describe('$watch', function () {
beforeEach(function () {
element.isolateScope().defaultSrc = 'foobar';
element = compile(html)(scope);
});
it('should have expected watchers added to the child scope', function () {
expect(element.isolateScope().$$watchers[0].exp).toBe('defaultSrc');
});
it('$watch should watch for changes on defaultSrc', function () {
element.isolateScope().defaultSrc = "barfoo";
element.isolateScope().$apply();
expect(scope.imageSource).toEqual('barfoo');
});
});
});
Related
I have the following directive. This Directive will trigger after animation end triggers. I now just want to add test for this directive. I have created the test case for this. But it's failing. Can anybody help on this? This is plunker
angular.module('movieApp')
.directive('animationend', function() {
return {
restrict: 'A',
scope: {
animationend: '&'
},
link: function(scope, element) {
var callback = scope.animationend(),
events = 'animationend webkitAnimationEnd MSAnimationEnd' +
'transitionend webkitTransitionEnd';
element.on(events, function(event) {
console.log('[animationend directive] callback');
callback.call(element[0], event);
});
}
};
});
And my test case is
describe('Directive: animationend', function () {
// load the directive's module
beforeEach(module('movieApp'));
var elements,
scope,
compile;
beforeEach(inject(function ($rootScope, _$compile_) {
scope = $rootScope.$new();
compile = _$compile_;
elements = angular.element('<div animationend="" ></div>');
elements = compile(elements)(scope);
scope.$digest();
}));
it('should trigger the callback once animationclass is added ', function () {
scope.ctrl = {
animationend: jasmine.createSpy('animationend')
};
scope.$digest();
var events = 'animationend webkitAnimationEnd MSAnimationEnd' +
'transitionend webkitTransitionEnd';
angular.element(elements).triggerHandler(events);
// element.trigger(events);
expect(scope.ctrl.animationend).toHaveBeenCalled();
});
});
I've simple directive with an event handler.
angular.module('app')
.directive('acc', function () {
return {
restrict: 'A',
link: function (scope, element) {
scope.a = 0;
scope.func = function() {
console.log('h2.clicked !!');
scope.a = 1;
};
element.on('click', 'h2', scope.func);
}
};
});
And simple test
describe('Directive: acc', function () {
beforeEach(module('app'));
var element,
scope;
var clickContext = 'h2';
var onSpy;
beforeEach(inject(function ($compile, $rootScope) {
scope = $rootScope.$new();
onSpy = spyOn($.fn, 'on').and.callThrough();
element = angular.element('<nav acc><h2></h2></nav>');
element = $compile(element)(scope);
scope.$digest();
}));
it('should set click handler', function () {
expect(onSpy).toHaveBeenCalledWith('click', clickContext, jasmine.any(Function));
});
describe('click handler behaviour', function () {
beforeEach(function () {
element.find('h2').triggerHandler('click');
});
it('handler should be called', function () {
expect(scope.a).toBe(1);
});
});
});
I want to invoke handler before each test case for verify behaviour.
How to do it?
I use jasmine for unit testing.
Have you tried calling scope.$digest(); after the .click()?
I am testing the directive below that uses an isolated scope. I know the triggerHandler is working but for some reason I keep getting the error
Expected undefined to equal 'http://www.gravatar.com/avatar/12345?s=40&d=identicon'.
Directive:
angular.module('pb.webSites.directives')
.directive('pbOrganizationImagePicker', [ function () {
return {
restrict: "E",
template: '<img data-ng-src="{{ imageSource }}" width="{{width}}" height="{{height}}" alt="Image Picker" class="img-rounded" />',
scope: {
fileId: '=pbFileId',
defaultSrc: '#pbDefaultSrc',
width: '#pbWidth',
height: '#pbHeight'
},
controller: 'pbOrganizationImagePickerController',
link: function (scope, element, attrs) {
scope.$watch('defaultSrc', function (value) {
if (value !== undefined) {
scope.imageSource = value;
}
});
element.on('click', function () {
scope.pickImage().then(function (image) {
scope.imageSource = image.storageUrl;
scope.fileId = image.fileId;
}, function () {
console.log('Modal dismissed at: ' + new Date());
});
});
}
};
}]);
Tests:
describe('pbOrganizationImagePicker', function () {
beforeEach(module('pb.webSites.controllers'));
beforeEach(module('pb.webSites.directives'));
beforeEach(module('ui.router'));
beforeEach(module('ui.bootstrap'));
var compile;
var scope;
var mockModal = {};
var image;
beforeEach(inject(function ($compile, $rootScope) {
compile = $compile
scope = $rootScope.$new();
}));
beforeEach(inject(function ($q, $injector) {
$httpBackend = $injector.get('$httpBackend');
$httpBackend.whenGET('/app/webSites/directives/OrganizationImagePicker.html').respond(200, '');
scopeObject = {
profileImageUrl: 'http://www.gravatar.com/avatar/12345?s=40&d=identicon',
profileImageId: 54634
};
scope.webSite = {
profileImageId: 6436
};
scope.pickImage = function () {
var defer = $q.defer();
defer.resolve(scopeObject);
return defer.promise;
};
}));
describe('element.click()', function () {
beforeEach(function () {
var html = angular.element('<pb-organization-image-picker data-pb-default-src="{{ webSite.profileImageUrl || \'/content/img/placeholder-lg.jpg\' }}" data-pb-file-id="webSite.profileImageId" data-pb-width="200"></pb-organization-image-picker>');
element = compile(html)(scope);
element.triggerHandler('click');
});
it('should assign value to scope variables', function () {
scope.pickImage();
scope.$digest();
expect(scope.imageSource).toEqual(scopeObject.profileImageUrl);
expect(scope.fileId).toEqual(scopeObject.profileImageId);
});
});
});
I have also tried changing the test to the following since Im pretty sure in the test above I am faking the test a bit. However here I get pickImage() was never called. Even if you dont see the problem which method do you think is better for testing?
describe('element.click()', function () {
it('should assign value to scope variables', function () {
element = compile(html)(scope);
spyOn(scope, 'pickImage');
element.triggerHandler('click');
scope.$apply();
//scope.pickImage();
expect(scope.pickImage).toHaveBeenCalled();
scope.$digest();
expect(scope.imageSource).toEqual(scopeObject.profileImageUrl);
expect(scope.fileId).toEqual(scopeObject.profileImageId);
});
});
element.on('click', function () {
scope.$apply(function() {
scope.pickImage().then(function (image) {
scope.imageSource = image.storageUrl;
scope.fileId = image.fileId;
}, function () {
console.log('Modal dismissed at: ' + new Date());
});
});
});
Wrap the code in your click handler in an $apply.
I suspect the real problem is that your directive uses an isolate scope, so it actually won't have a "pickImage()" method and when you assign imageSource and fileId, you are putting them on the directive scope, and not the scope that your test is trying to validate (the parent scope).
Your test assigns pickImage() and webSite to the scope of your test element. Since you use an isolated scope, your directive won't have access to these methods and properties. You should probably move these to a service and inject them into the directive.
It's not "correct", but to test the theory you can change your directive to:
element.on('click', function () {
scope.$apply(function(){
scope.$parent.pickImage().then(function (image) {
scope.$parent.imageSource = image.storageUrl;
scope.$parent.fileId = image.fileId;
}, function () {
console.log('Modal dismissed at: ' + new Date());
});
});
});
This IS NOT something you want in production code, but I'm just trying to demonstrate how the different scopes are related to each other.
I have a custom directive that uses an external template and is passed data from a service. I decided to ensure that the promise was resolved before modifying the data, which was fine in the actual code but broke my unit tests, which is annoying. I have tried a number of variations but am now stuck. I am using 'ng-html2js' preprocessor.
Here is the unit test
describe('ccAccordion', function () {
var elm, scope, deferred, promise, things;
beforeEach(module('ccAccordion'));
// load the templates
beforeEach(module('components/accordion/accordion.html'));
beforeEach(inject(function ($rootScope, $compile, $q) {
elm = angular.element(
'<cc-accordion items="genres"></cc-accordion>'
);
scope = $rootScope;
things = [{
title: 'Scifi',
description: 'Scifi description'
}, {
title: 'Comedy',
description: 'Comedy description'
}];
deferred = $q.defer();
promise = deferred.promise;
promise.then(function (things) {
scope.items = things;
});
// Simulate resolving of promise
deferred.resolve(things);
// Propagate promise resolution to 'then' functions using $apply().
scope.$apply();
// compile the template?
$compile(elm)(scope);
scope.$digest();
}));
it('should create clickable titles', function () {
var titles = elm.find('.cc-accord h2');
expect(titles.length).toBe(2);
expect(titles.eq(0).text().trim()).toBe('Scifi');
expect(titles.eq(1).text().trim()).toBe('Comedy');
});
I have left out the custom addMatchers and the rest of the tests. The error I get is
TypeError: 'undefined' is not an object (evaluating 'scope.items.$promise')
Here is the directive
var ccAccordion = angular.module("ccAccordion", []);
ccAccordion.directive("ccAccordion", function () {
return {
restrict: "AE",
templateUrl: "components/accordion/accordion.html",
scope: {
items: "="
},
link: function (scope) {
scope.items.$promise.then(function (items) {
angular.forEach(scope.items, function (item) {
item.selected = false;
});
items[0].selected = true;
});
scope.select = function (desiredItem) {
(desiredItem.selected === true) ? desiredItem.selected = false : desiredItem.selected = true;
angular.forEach(scope.items, function (item) {
if (item !== desiredItem) {
item.selected = false;
}
});
};
}
};
});
This is where the directive is used in main.html
<cc-accordion items="genres"></cc-accordion>
In the main controller the genres service is passed in ie
angular.module('magicApp')
.controller('GenresCtrl', ['$scope', 'BREAKPOINTS', 'Genre',
function ($scope, BREAKPOINTS, Genre) {
$scope.bp = BREAKPOINTS;
$scope.genres = Genre.query();
}]);
Okay, I would move that code you put in link into the controller. The data processing should probably happen in a service. I know you've been told big controllers are bad, but big linking functions are generally worse, and should never do that kind of data processing.
.controller('GenresCtrl', ['$scope', 'BREAKPOINTS', 'Genre',
function ($scope, BREAKPOINTS, Genre) {
$scope.bp = BREAKPOINTS;
$scope.genres = Genre.query().then(function (items) {
angular.forEach(scope.items, function (item) {
item.selected = false;
});
items[0].selected = true;
});
scope.select = function (desiredItem) {
(desiredItem.selected === true) ? desiredItem.selected = false : desiredItem.selected = true;
angular.forEach(scope.items, function (item) {
if (item !== desiredItem) {
item.selected = false;
}
});
};
});
Your link function is now empty. Define items on the rootScope instead, this ensures that the isolateScope and your directive interface are working correctly.
beforeEach(inject(function ($rootScope, $compile, $q) {
elm = angular.element(
'<cc-accordion items="genres"></cc-accordion>'
);
scope = $rootScope;
things = [{
title: 'Scifi',
description: 'Scifi description'
}, {
title: 'Comedy',
description: 'Comedy description'
}];
scope.items = things; // Tests your directive interface
// compile the template?
$compile(elm)(scope);
scope.$digest();
}));
The behavior of the promise should be tested in a controller test, by mocking the return value of the service. Your problem with the $promise test has been solved.
The actual issue was that you were assuming that $q.defer() gave you the same kind of promise as the angular $http, but that is solved by design instead.
As peter said remove the promise from the directive and add it to the controller
angular.module('magicApp')
.controller('MainCtrl', ['$scope', 'Genre',
function ($scope, Genre) {
$scope.genres = Genre.query();
$scope.genres.$promise.then(function () {
angular.forEach($scope.genres, function (genre) {
genre.selected = false;
});
$scope.genres[0].selected = true;
});
}]);
This will also allow the controller to specify which tab is selected to begin with.
In the directive
var ccAccordion = angular.module("ccAccordion", []);
ccAccordion.directive("ccAccordion", function () {
return {
restrict: "AE",
templateUrl: "components/accordion/accordion.html",
scope: {
items: "="
},
link: function (scope) {
scope.select = function (desiredItem) {
(desiredItem.selected === true) ? desiredItem.selected = false : desiredItem.selected = true;
angular.forEach(scope.items, function (item) {
if (item !== desiredItem) {
item.selected = false;
}
});
};
}
};
});
The directive unit test now looks like this
describe('ccAccordion', function () {
var elm, scope, deferred, promise, things;
beforeEach(module('ccAccordion'));
beforeEach(function () {
jasmine.addMatchers({
toHaveClass: function () {
return {
compare: function (actual, expected) {
var classTest = actual.hasClass(expected);
classTest ? classTest = true : classTest = false;
return {
pass: classTest,
message: 'Expected ' + angular.mock.dump(actual) + ' to have class ' + expected
};
}
};
}
});
});
// load the templates
beforeEach(module('components/accordion/accordion.html'));
beforeEach(inject(function ($rootScope, $compile, $q) {
elm = angular.element(
'<cc-accordion items="genres"></cc-accordion>'
);
scope = $rootScope;
scope.genres = [{
title: 'Scifi',
description: 'Scifi description'
}, {
title: 'Comedy',
description: 'Comedy description'
}];
$compile(elm)(scope);
scope.$digest();
}));
it('should create clickable titles', function () {
var titles = elm.find('.cc-accord h2');
expect(titles.length).toBe(2);
expect(titles.eq(0).text().trim()).toBe('Scifi');
expect(titles.eq(1).text().trim()).toBe('Comedy');
});
it('should bind the content', function () {
var contents = elm.find('.cc-accord-content div:first-child');
expect(contents.length).toBe(2);
expect(contents.eq(0).text().trim()).toBe('Scifi description');
expect(contents.eq(1).text().trim()).toBe('Comedy description');
});
it('should change active content when header clicked', function () {
var titles = elm.find('.cc-accord h2'),
divs = elm.find('.cc-accord');
// click the second header
titles.eq(1).find('a').click();
// second div should be active
expect(divs.eq(0)).not.toHaveClass('active');
expect(divs.eq(1)).toHaveClass('active');
});
});
And the unit test for main controller now has the added property of selected
'use-strict';
describe('magicApp controllers', function () {
// using addMatcher because $resource is not $http and returns a promise
beforeEach(function () {
jasmine.addMatchers({
toEqualData: function () {
return {
compare: function (actual, expected) {
return {
pass: angular.equals(actual, expected)
};
}
};
}
});
});
beforeEach(module('magicApp'));
beforeEach(module('magicServices'));
describe('MainCtrl', function () {
var scope, ctrl, $httpBackend;
beforeEach(inject(function (_$httpBackend_, $rootScope, $controller) {
$httpBackend = _$httpBackend_;
$httpBackend.expectGET('/api/genres').
respond([{title: 'Scifi', selected: true}, {title: 'Comedy', selected: false}]);
scope = $rootScope.$new();
ctrl = $controller('MainCtrl', {$scope: scope});
}));
it('should create "genres" model with 2 genres fetched from xhr', function () {
expect(scope.genres).toEqualData([]);
$httpBackend.flush();
expect(scope.genres).toEqualData(
[{title: 'Scifi', selected: true}, {title: 'Comedy', selected: false}]);
});
});
});
I am having a ton of trouble testing my directive.
Right now I am stuck with these two errors
Test 'pbImagePicker template:should render HTML based on scope correctly' failed
TypeError: 'undefined' is not a function (evaluating 'element.click')
Test 'pbImagePicker element.click():should assign data from resolved promise when clicked' failed
TypeError: 'undefined' is not a function (evaluating 'scope.$new(true)')
Directive:
angular.module('pb.campaigns.directives')
.directive('pbImagePicker', ['$window', '$document', function ($window, $document) {
return {
restrict: "E",
template: '<img data-ng-src="{{ imageSource }}" width="{{width}}" height="{{height}}" alt="Image Picker" class="img-rounded" />',
scope: {
fileId: '=pbFileId',
accountId: '=pbAccountId',
defaultSrc: '#pbDefaultSrc',
width: '#pbWidth',
height: '#pbHeight'
},
controller: 'pbImagePickerController',
link: function (scope, element, attrs) {
scope.$watch('defaultSrc', function (value) {
if (value !== undefined) {
scope.imageSource = value;
}
});
element.click(function () {
scope.pickImage(scope.accountId).then(function (image) {
scope.imageSource = image.storageUrl;
scope.fileId = image.fileId;
}, function () {
console.log('Modal dismissed at: ' + new Date());
});
});
}
};
}]);
I am also trying to test the element.click by using the triggerHandler but im not having any luck. Testing the link: $watch function has also proven to be impossible for me. Has anyone ever done this? I have found simular examples but none that I seem to be able to get working
this is what I currently have:
describe('pbImagePicker', function () {
beforeEach(module('pb.campaigns.controllers'));
beforeEach(module('pb.campaigns.directives'));
beforeEach(module('ui.router'));
beforeEach(module('ui.bootstrap'));
var compile;
var scope;
var mockModal = {};
// var element = {
// click: function () { }
//};
beforeEach(inject(function ($compile, $rootScope) {
compile = $compile;
scope = $rootScope.$new();
}));
describe('template', function () {
it('should render HTML based on scope correctly', function () {
//scope = {
scope.fileId = '43253';
scope.accountId = '3874';
scope.imageSource = 'http://www.gravatar.com/avatar/12345?s=40&d=identicon';
scope.width = '250';
scope.height = '250';
//};
var html = angular.element('<pb-image-picker data-pb-default-src="{{ campaign.sponsorSmallImageUrl || \'/content/img/placeholder-sm.jpg\' }}" data-pb-file-id="campaign.sponsorSmallImageId" data-pb-account-id="currentAccountId" data-pb-height="55"></pb-image-picker>');
compiled = compile(html)(scope);
compiled.triggerHandler('click');
scope.$digest();
expect(scope.width).toEqual('250');
expect(scope.accountId).toEqual('3874');
expect(element.hasClass('img-rounded')).toBe(true);
expect(element.attr('width')).toEqual('250');
expect(element.attr('height')).toEqual('250');
expect(element.attr('alt')).toMatch('Image Picker');
//or even expect it to match the scope value
expect(element.attr('width')).toEqual(scope.width);
});
});
describe('element.click()', function () {
var image;
beforeEach(inject(function ($q) {
image = {
storageUrl: 'http://www.pressboard.com/avatar/7453',
fileId: 6432342
};
scope.pickImage = function (accountId) {
var defer = $q.defer();
defer.resolve(image);
return defer.promise;
};
scope = {
fileId: '43253',
accountId: '3874',
imageSource: 'http://www.gravatar.com/avatar/12345?s=40&d=identicon',
width: '250',
height: '250'
};
}));
it('should assign data from resolved promise when clicked', function() {
var element = compile('<pb-image-picker data-pb-default-src="{{ campaign.sponsorSmallImageUrl || \'/content/img/placeholder-sm.jpg\' }}" data-pb-file-id="campaign.sponsorSmallImageId" data-pb-account-id="currentAccountId" data-pb-height="55"></pb-image-picker>')(scope);
element.triggerHandler('click');
scope.$digest();
expect(scope.imageSource).toEqual(image.storageUrl);
expect(scope.fileId).toEqual(image.fileId);
});
});
});
the pickImage function is defined in my controller as
$scope.pickImage = function (accountId) {
var modalOptions = {
templateUrl: '/app/campaigns/directives/ImagePicker.html',
controller: 'pbImagePickerModalController',
resolve: {
accountId: function () {
return accountId;
}
}
};
var modalInstance = $modal.open(modalOptions);
return modalInstance.result;
};