Goal:
Write a passing test for the waCarousel directive scope variable: self.awesomeThings. Expect this test pass when self.awsomeThings.length.toBe(3) to is true?
Question:
How can I properly write this test? rather how do I inject a directives controller?
Directive:
angular.module('carouselApp')
.directive('waCarousel', function() {
return {
templateUrl: '../../../views/carousel/wa.carousel.html',
controller: function($scope) {
var self = this;
self.awesomeThings = [1, 2, 3];
return $scope.carousel = self;
}
}
});
Unit Test:
describe('waCarousel Unit', function() {
// am I missing a $controller & namespace variable init?
var $compile,
$rootScope;
// Load the myApp module, which contains the directive
beforeEach(module('carouselApp'));
// Store references to $rootScope and $compile and $controller
// so they are available to all tests in this describe block
beforeEach(inject(function(_$compile_, _$rootScope_, _$controller_){
// The injector unwraps the underscores (_) from around the parameter names when matching
$compile = _$compile_;
$rootScope = _$rootScope_;
$controller = _$controller_;
// WaCarouselCtrl = $controller('WaCarouselCtrl', {
// $scope: scope
// });
}));
it('should have a list of awesomeThings', function() {
// This wont pass
expect(scope.awesomeThings.length).toBe(3);
});
});
This is how I would do it for a typical view and not directive:
describe('Controller: MainCtrl', function() {
// load the controller's module
beforeEach(module('carouselApp'));
var MainCtrl,
scope;
// Initialize the controller and a mock scope
beforeEach(inject(function($controller, $rootScope) {
scope = $rootScope.$new();
// !!*** this is how I would inject the typical controller of a view **!! //
MainCtrl = $controller('MainCtrl', {
$scope: scope
});
}));
it('should attach a list of awesomeThings to the scope', function() {
expect(scope.awesomeThings.length).toBe(3);
});
});
How do I merge these two concepts so that I can expect self.awesomeThings.length).toBe(3)?
UPDATE:
Compile the element, and after calling $digest(), you will have access to the scope which contains carousel object with awesomeThings array:
describe('waCarousel Unit', function() {
var scope;
beforeEach(module('carouselApp'));
beforeEach(inject(function($rootScope, $compile) {
var element = '<test></test>';
scope = $rootScope.$new();
element = $compile(element)(scope);
scope.$digest();
}));
it('should have a list of awesomeThings', function() {
expect(scope.carousel.awesomeThings.length).toBe(3);
});
});
Also, here are some useful links to testing directives in angular:
Testing Directives
Testing AngularJS directive controllers with Jasmine and Karma
Introduction to Unit Test: Directives
Related
I'm working on an AngularJS app and I'm facing some problems with Jasmine's SpyOn in a concrete directive.
The directive is quite simple, just call a service's method and when it resolves/rejects the promise acts in consequence, setting some values or another ones.
The problem: When I try to mock SignatureService.getSignatureData SpyOn does not work as I expect, and acts as if I was invoking jasmine's callThrough method over getSignatureData.
I've been using spyOn and mocks in other directives and services, and there was no problem with those.
I've been trying to solve this issue the last two days, comparing with other solutions and user's answers, but I can not find a valid solution.
Here's my code:
AngularJS directive code:
angular
.module('module_name')
.directive('signatureDirective', signatureDirective);
angular
.module('GenomcareApp_signature')
.controller('signatureDController', signatureDController);
function signatureDirective() {
return {
restrict: 'E',
templateUrl: 'components/signature/signature.directive.html',
controller: signatureDController,
controllerAs: 'ctrl',
bindToController: true
};
}
signatureDController.$inject = [
'$scope',
'$rootScope',
'$location',
'SignatureService'
];
function signatureDController($scope, $rootScope, $location, SignatureService) {
var controller = this;
$scope.$on('pdfFileLoadSuccessfully', function (data) {
console.log(data);
controller.loadPdfSucceed = true;
});
$scope.$on('pdfFileLoadFails', function (data) {
console.error(data);
controller.loadPdfError = true;
});
function loadDirectiveInitData() {
var queryParameters = atob($location.search().data);
controller.email = queryParameters.split(';')[0];
controller.phone = queryParameters.split(';')[1];
controller.docid = queryParameters.split(';')[2];
SignatureService.getSignatureData(controller.email, controller.phone, controller.docid)
.then(
function (data) {
console.log(data);
controller.stampTime = data.stamp_time;
controller.fileUrl = data.original_file.url;
},
function (error) {
console.error(error);
controller.error = true
})
.finally(
function () {
controller.endLoad = true;
})
}
loadDirectiveInitData();
}
Jasmine test code:
'use strict';
/* global loadJSONFixtures */
describe('Test :: Signature directive', function () {
beforeEach(angular.mock.module('app'));
beforeEach(module('translateNoop'));
var $q, $compile, $rootScope, controller, $scope, $httpBackend, $location, SignatureService;
beforeEach(angular.mock.inject(function (_$controller_, _$q_, _$rootScope_, _$location_, _$compile_, _$httpBackend_, _SignatureService_) {
$q = _$q_;
$compile = _$compile_;
$location = _$location_;
$scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
SignatureService = _SignatureService_;
spyOn($location, 'search').and.returnValue({data: 'dGVzdEB0ZXN0LmNvbTsrMzQ2NjY2NjY2NjY7WG9TUFFnSkltTWF2'});
$httpBackend.whenGET('components/signature/signature.directive.html').respond(200, '');
controller = _$controller_('signatureDController', {$scope: $scope});
}));
describe('Testing directive', function () {
it('Init data should be set when promise resolves/rejects', function (done) {
// SpyOn DOES NOT MOCK THE SERVICE METHOD
spyOn(SignatureService, 'getSignatureData').and.callFake(function () {
return $q.resolve({...})
});
var element = angular.element('<signature-directive></signature-directive>');
element = $compile(element)($scope);
$scope.$digest();
done();
// ... some expect stuff
});
});
});
If any one can give me some advice or solution, I would be very thankful.
Thank you very much.
UPDATE1: I don't know why, but if I do not declare the controller variable in the global beforeEach, Jasmine's spyOn mocks the method as I expect.
Now the issue is how to get the controller to test that the controller values are set as expected.
Well... I realized that the problem was that the controller was being created before all, and somehow when the service was mocked the controller ignores it.
This idea came by accident, when I paste the service's spyOn in the global beforeEach.
So I decide to create a new instance of the controller and the corresponding spyOn with the desired result inside the beforeEach of each describe.
It works. Maybe it's not the best aproach, and I encourage to anyone who have the answer to post it. I'm going to be eternally greatful.
Here's my final test code:
describe('Test :: Signature directive', function () {
beforeEach(angular.mock.module('app'));
beforeEach(module('translateNoop'));
var $q, $compile, $rootScope, $scope, $httpBackend, $location, SignatureService, test_fixture;
beforeEach(angular.mock.inject(function (_$q_, _$rootScope_, _$location_, _$compile_, _$httpBackend_, _SignatureService_) {
$q = _$q_;
$compile = _$compile_;
$location = _$location_;
$scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
SignatureService = _SignatureService_;
// controller = _$controller_;
spyOn($location, 'search').and.returnValue({data: 'dGVzdEB0ZXN0LmNvbTsrMzQ2NjY2NjY2NjY7WG9TUFFnSkltTWF2'});
$httpBackend.whenGET('components/signature/signature.directive.html').respond(200, '');
}));
describe('Testing directive when service resolve promise', function () {
var controller;
beforeEach(inject(function(_$controller_) {
spyOn(SignatureService, 'getSignatureData').and.callFake(function () {
return $q.resolve({...})
});
controller = _$controller_('signatureDController', {$scope: $scope})
}));
it('Init data should be set', function () {
// spyOn($location, 'search').and.callThrough();
var element = angular.element('<signature-directive></signature-directive>');
element = $compile(element)($scope);
$scope.$digest();
// ... some expect(...).toEqual(...) stuff and more
});
});
});
Thank you for your time.
Try to use $q.defer(), here's an example:
it('Init data should be set when promise resolves/rejects', function (done) {
// SpyOn DOES NOT MOCK THE SERVICE METHOD
spyOn(SignatureService, 'getSignatureData').and.callFake(function () {
let deferred = $q.defer();
deferred.resolve({...});
return deferred.promise;
});
var element = angular.element('<signature-directive></signature-directive>');
element = $compile(element)($scope);
$scope.$digest();
done();
// ... some expect stuff
});
Karma keeps throwing TypeError: Cannot read property 'originalPath' of undefined given a directive with the following in the link function:
angular.module('myApp').directive('sidebar', ['$route', function ($route)
{
return {
restrict: 'E',
templateUrl: 'views/sidebar.html',
scope: {
activeNav: '#'
},
link: function (scope, element, attrs) {
scope.$on('$routeChangeSuccess', function (event, curr, prev) {
scope.activeNav = curr.$$route.originalPath || '/about';
});
}
}
and unit test:
describe('sidebar directive', function () {
var $compile,
$rootScope,
scope,
element;
beforeEach(module('MyApp'));
beforeEach(module('my.templates')); // ng-html2js karma template loader
beforeEach(inject(function(_$compile_, _$rootScope_){
$compile = _$compile_;
$rootScope = _$rootScope_;
scope = $rootScope.$new();
}));
it('defaults to /about route', function() {
element = $compile("<sidebar></sidebar>")(scope);
var linkScope = element.children().scope();
$rootScope.$broadcast('$routeChangeSuccess');
$rootScope.$digest();
expect(linkScope.activeNav).toBe('/about');
});
});
When logging the curr route from within the link I get Object{params: Object{}, pathParams: Object{}, locals: Object{}}. I've tried passing a mock route to the broadcasted message but nothing changed. How would I get the expected (default) route to pass into the directive? Is this even the right way to go about tracking a route change within a link? My guess is that I'm just new to Jasmine and unit testing.
I eventually figured out how to trigger the route change using $route.reload()
From the docs, it:
Causes $route service to reload the current route even if $location hasn't changed. As a result of that, ngView creates new scope and reinstantiates the controller.
So adding that to my test 'reboots' the route, forcing routeChangeSuccess to emit and, once digested, passes into the link. Here's the updated block from the spec file with an added $location change to test other routes:
describe('sidebar directive', function () {
var $compile,
$rootScope,
$route,
$location,
scope,
element;
beforeEach(module('MyApp'));
beforeEach(module('my.templates')); // ng-html2js karma template loader
beforeEach(inject(function(_$compile_, _$rootScope_, _$route_, _$location_){
$compile = _$compile_;
$rootScope = _$rootScope_;
$location = _$location_;
$route = _$route_;
scope = $rootScope.$new();
element = $compile("<sidebar></sidebar>")(scope);
}));
it('defaults to /about route', function() {
$route.reload();
$rootScope.$digest();
var linkScope = element.children().scope(); // Get directive's isolated scope
expect(linkScope.activeNav).toBe('/about');
});
it('sets activeNave to correct route on change', function() {
$location.path('/foo');
$route.reload();
$rootScope.$digest();
var linkScope = element.children().scope();
expect(linkScope.activeNav).toBe('/foo');
});
});
I have a list of items. When you click on an item, it brings up a modal to display data for that item. In the controller for the list, there's a function, openRecentsModal, that takes the data object from the ng-repeat list, and creates it on a new scope when the function is run. The new modal then has that object available as $scope.recentsFoldersData. I need to write a unit test to ensure recentsFolderData is defined on the scope, but everything I've tried results in "expected undefined to be defined." I'm hoping somebody can help.
Here's the method in the list controller to open a modal:
function openRecentsModal(obj) {
var scope = $rootScope.$new();
scope.recentsFoldersData = obj;
var controller = 'recentsFoldersDetailController';
$modal.open({
scope: scope,
controller: controller,
templateUrl: 'js/modal/recents/folder/recentsFoldersDetail.tpl.html'
});
}
Here's the modal's controller:
angular.module('modal.recents.folder', [])
.controller('recentsFoldersDetailController', recentsFoldersDetailController);
recentsFoldersDetailController.$inject = ['$scope', '$modalInstance'];
function recentsFoldersDetailController($scope, $modalInstance) {
$scope.close = function close() {
$modalInstance.dismiss('close');
};
}
Finally, here's the unit tests I'm working on (I've excluded the ones that are passing, as well as the helper functions that aren't needed for this test):
describe('recents folders modal controller tests', function() {
var scope, q, modal, mockDetailController, mockListController, mockRecentService, mockFolderService, mockModalInstance, $httpBackend;
beforeEach(module('mainApp'));
beforeEach(inject(function($rootScope, $q, $controller, $modal, _recentService_, _folderService_, $injector) {
q = $q;
scope = $rootScope.$new();
modal = $modal;
$httpBackend = $injector.get('$httpBackend');
$httpBackend.whenGET('js/modal/recents/folder/recentsFoldersDetail.tpl.html').respond(200, '');
mockRecentService = _recentService_;
mockFolderService = _folderService_;
mockModalInstance = {
dismiss: jasmine.createSpy('modalInstance.dismiss')
};
mockDetailController = function() {
return $controller('recentsFoldersDetailController', {
'$scope': scope,
'$modalInstance': mockModalInstance
});
};
mockListController = function() {
return $controller('recentsListFoldersController', {
'$scope': scope,
'$modal': modal,
'recentService': mockRecentService,
'folderService': mockFolderService
});
};
}));
describe('scope tests', function() {
it('should place the data on the scope when openRecentsModal is called', function() {
var obj = defaultSuccessfulRecentsDataResponse();
mockListController();
spyOn(scope, 'openRecentsModal');
scope.openRecentsModal(obj);
expect(scope.openRecentsModal).toHaveBeenCalledWith(obj);
mockDetailController();
expect(scope.recentsFoldersData).toBeDefined();
});
});
/* helper functions */
function defaultSuccessfulRecentsDataResponse() {
return {
id: 'id 1',
name: 'first name',
description: 'first description'
};
}
});
I was able to fix this with a simple change to the function calling the modal
function openRecentsModal(obj) {
$rootScope.recentsFoldersData = obj;
var controller = 'recentsFoldersDetailController';
$modal.open({
//scope: scope,
controller: controller,
templateUrl: 'js/modal/recents/folder/recentsFoldersDetail.tpl.html'
});
}
By allowing the object to be placed on $rootScope (UI-Bootstrap modal's default setting), rather than a new $scope, the test came back with a defined value
it('should put recents object on the scope', function() {
mockListController();
scope.openRecentsModal(defaultSuccessfulRecentsDataResponse());
mockDetailController();
expect(scope.recentsFoldersData).toBeDefined();
});
I'm trying to write unit-tests for an Angular application for the first time. Currently i'm having some problems running the tests. Running the application normally works fine, it doesn't give any errors. However, when running the tests using Karma and Jasmine i'm getting the following error:
TypeError: 'undefined' is not a function (evaluating '$scope.addActiveClassToMenuButton('menuButtonHome')')
I'm using the ui.router module. Not sure if that matters.
Parent controller
Parent controller contains the following method:
angular.module('testApp')
.controller('ParentCtrl', function ($scope, $resource) {
$scope.addActiveClassToMenuButton = function(buttonId) {
//Some code
}
}
Child controller
Child controller calls the parents method like this:
angular.module('testApp')
.controller('ChildCtrl', function ($scope, $resource) {
$scope.addActiveClassToMenuButton('menuButtonHome');
}
Child controller test file
The test file that fails:
describe('Child controller tests. ', function () {
beforeEach(module('testApp'));
var ChildCtrl, scope;
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
ChildCtrl = $controller('ChildCtrl', {
$scope: scope
});
}));
it('simple false test', function () {
expect(false).toBe(false);
});
});
Even though i'm not using the scope in the test yet, all tests fail because the code can't find the parents method.
Solution
Changing the test file to this worked:
describe('Child controller tests. ', function () {
beforeEach(module('testApp'));
var controller, scope, parentScope, childScope;
beforeEach(inject(function ($controller, $rootScope, $compile) {
scope = $rootScope.$new();
var el = angular.element('<div ng-controller="ParentCtrl"><div ng-controller="ChildCtrl"></div></div>');
$compile(el)(scope);
parentScope = el.scope();
childScope = el.children().scope();
}));
it('simple false test', function () {
expect(false).toBe(false);
});
});
Try this..
describe('Child controller tests. ', function () {
beforeEach(module('testApp'));
var ChildCtrl, scope;
beforeEach(inject(function ($controller, $rootScope, $compile) {
scope = $rootScope.$new();
var el = angular.element('<div ng-controller="ParentCtrl"><div ng-controller="ChildCtrl"></div></div>');
$compile(el)(scope);
// to access parent controller.
var parentScope = el.scope();
var childScope = el.children().scope();
// now you should be able to access from parent and child scopes.
}));
it('simple false test', function () {
expect(false).toBe(false);
});
});
This will instantiate ParentCtrl first and then extend the scope of it with the ChildCtrl's scope.
In the example that you have given only ChildCtrl is instantiated ParentCtrl is not instantiated.
I am trying to write units test for my app and I have the following issue
In my controller, I have something like
$scope.test1 = function() {
productFactory.getName()
.then(function(products){
$scope.result = products;
})
}
productFactory
angular.module('myApp').factory('productFactory', function($http) {
var factoryObj = {};
factoryObj.getName = function() {
return http.get(url)
}
return factoryObj
})
In my unit test file
describe('test here', function () {
var testCtrl, scope, httpBackend, mockFactory;
beforeEach(module('myApp', function($provide){
$provide.value('productFactory', mockFactory);
}));
// Initialize the controller and a mock scope
beforeEach(inject(function (_$controller_, _$rootScope_, _$httpBackend_, _productFactory_) {
scope = _$rootScope_.$new();
httpBackend = _$httpBackend_;
mockFactory = _productFactory_;
testCtrl = _$controller_('testCtrl', {
$scope: scope
});
it('should get product name', function() {
scope.test1();
//I am not sure how to test the results
});
}));
When I run karma test, it gives me
TypeError: 'undefined' is not an object (evaluating 'productFactory.getName()')
I am not sure how to test the http result and fix the error. Can anyone help me about it? Thanks a lot!
First of all, you don't need to worry about using $provide:
beforeEach(module('myApp'));
1. Without $httpBackend (mock out the service completely)
Then, productFactory will be passed into your controller, but you want to spyOn the getName():
// Initialize the controller and a mock scope
beforeEach(inject(function (_$controller_, _$rootScope_, _$httpBackend_, _productFactory_) {
scope = _$rootScope_.$new();
httpBackend = _$httpBackend_;
mockFactory = _productFactory_;
// add spy for the method, wrap with $q.when so it returns a promise
spyOn(mockFactory, 'getName').and.returnValue($q.when('Pizza!'));
testCtrl = _$controller_('testCtrl', {
$scope: scope,
productFactory: mockFactory // pass in here
});
Then, you've got to cause a $digest cycle, so that the promise will call through:
it('should get product name', function() {
scope.test1();
// hit the $digest
scope.$apply();
// expectation
expect(scope.result).toBe('Pizza!')
});
2. With $httpBackend
// Initialize the controller and a mock scope
beforeEach(inject(function (_$controller_, _$rootScope_, _$httpBackend_) {
scope = _$rootScope_.$new();
httpBackend = _$httpBackend_;
// set up httpBackent
httpBackend.when('GET', '/products')
.respond([{ name: 'Pizza!'}, {name: 'Sandwich'}]);
testCtrl = _$controller_('testCtrl', {
$scope: scope
});
We don't need to mock the factory in this case at all. Then, we just need to flush $httpBackend when we want the http call to return:
it('should get product name', function() {
scope.test1();
// hit the $digest with flush
httpBackend.flush();
// expectation
expect(scope.result.length).toBe(2)
});