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');
});
});
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
});
I am trying to test my d3 and angular app using Jasmine and Karma task runner. It works when I use the app in the browser but I am having problems setting up the tests. I am also using browserify.
The factory service loads in the d3 dependency (instead of putting d3 in a script tag) which is used by the world map service, this controller and the directive (below):
.factory('d3Service', ['$document', '$q', '$rootScope', '$window', d3Service])
.service('Category', ['$http', categoryService])
//most of the d3 methods are in this service
.service('WorldMap', ['d3Service', worldMapService])
.controller('MapCtrl', ['$scope', 'd3Service', 'Category', '$http',
function($scope, d3Service, Category, $http) {
// waits until d3 is loaded then gets the world
// data json file and set to controller's scope
d3Service.d3().then(function(d3){
$http.get("world.json").success(function(world) {
$scope.countries = topojson.feature(world, world.objects.countries).features;
});
});
}
])
//the directive is what contains the d3 map
.directive('wmMap', ['d3Service', 'Category', '$window', 'ngDialog', 'WorldMap', wmMap]);
The directive similarly waits for the d3 dependency to load:
var wmMap = function(d3Service, Category, $window, ngDialog, WorldMap){
return {
restrict: 'EA',
link: function(scope, ele, attrs){
d3Service.d3().then(function(d3) {
//do some stuff
// when world data json is loaded and scope is set
// call render to set map on page
scope.$watch('countries', function(countries){
if(countries !== undefined){
WorldMap.render(ele[0], zoom, countries, Category, ngDialog);
}
});
});
}
}
}
Tests - use $httpBackend.expectGet() to set some data and then $httpBackend.flush() to load it to the test in the 'it' block. The $scope data that should be loaded in for $scope.countries is not there...?
describe('d3', function(){
var data, $q, $rootScope, $compile, $window, $httpBackend, html, element;
beforeEach(function(){
mockd3Service = {};
mockMapService = {};
module('WorldMaps');
//provide services
module(function($provide){
$provide.value('d3Service', mockd3Service);
$provide.value('WorldMap', mockMapService);
});
inject(function($injector,_$compile_, _$rootScope_, _$window_, _$q_, _$controller_, _$httpBackend_){
$window = _$window_;
$compile = _$compile_;
$rootScope = _$rootScope_;
$controller = _$controller_;
$q = _$q_;
$httpBackend = _$httpBackend_;
// load in some mock data for http request
$httpBackend.expectGET('world.json')
.respond({arcs: ['abc'],
objects: {countries: {geometries: [{arcs:[], id: "Netherlands", type: "Polygon"}]}},
transform: {scale: [], translate: []},
type: "Topology"}
);
$scope = $rootScope.$new();
});
mockd3Service.d3 = function(){
var deferred = $q.defer();
deferred.resolve($window.d3);
return deferred.promise;
}
});
it('created', function(){
//check d3 service is running
html = '<wm-map></wm-map>';
element = angular.element(html);
element = $compile(html)($rootScope);
$rootScope.$digest();
expect($scope.countries).toBeUndefined();
ctrl = $controller('MapCtrl', {'$scope' : $scope});
$httpBackend.flush();
//$scope is logged out with a countries property
//but countries is undefined
console.log($scope);
});
})
You could get your JSON file with $http instead and mock that with $httpBackend, then pass the JSON data to D3.
Of course, if you are trying to test D3 rendering, this approach doesn't solve that, but if all you want to do is ensure that the HTTP request was made, you could take that approach instead.
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.
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
Why am I unable to access my function in my Controller? The code functions like I would expect it too, however, it doesn't seem to want to allow me access to my function that I'm trying to unit test. It should just return a simple bool, but it's getting killed somewhere.
Here's some code:
RTHelper.js
describe('Unit: LocationController', function () {
var $scope, $httpBackend, $location, injector, ctrl, $controller;
//beforeEach(function () {
// angular.module('TDE').controller('LocationController'); //
// inject(function ($injector) {
// $rootScope = $injector.get('$rootScope');
// $scope = $rootScope.$new();
// //ctrl = $injector.get('$controller')("LocationController", { $scope: $scope });
// injector = $injector;
// ctrl = $injector.get('$controller');
// //scope = $injector.get('$rootScope').$new();
// $httpBackend = $injector.get('$httpBackend');
// $location = $injector.get('$location');
// });
//});
//both beforeEach methods work(which one is better? I don't know), so things are getting loaded
beforeEach(function () {
angular.module('TDE');
inject(function ($injector) {
$location = $injector.get('$location');
$rootscope = $injector.get('$rootScope');
$scope = $rootscope.$new();
$controller = $injector.get('$controller');
ctrl = function () {
return $controller('LocationController', {
'$scope': $scope
})
};
})
});
it("should just be a holder for something for later", function () {
expect($scope.BoolCondition()).toBeDefined(); //I don't care what it returns as long as it's accessed honestly
});
})
LocationController.js
angular
.module('TDE')
.controller('LocationController', ['$rootScope', '$scope', '$location', '$window', '$document', 'LocationService', 'HeaderFooterService', 'SearchService', 'TranslationService', 'MTDE_CONFIG', 'LocationPartnerAssignmentService', 'ExperimentService', function ($rootScope, $scope, $location, $window, $document, $LocationService, $HeaderFooterService, $SearchService, $TranslationService, $MTDE_CONFIG, $LocationPartnerAssignmentService, $ExperimentService) {
$scope.BoolCondition = function(myCondition){
if(//blah blah condition test on myCondition)
{
return true
}
else
{
return false;
}
}
How would I go about getting to that BoolCondition? I'm new to this so you can imagine the struggle of writing unit tests after never having done unit testing. I've also gone through countless examples and I've done some generic tests, so I'm not totally un-versed.
You're not bootstrapping the module under test correctly. You should use angular.mock.module inside the test
You're not instantiating the controller (where's the call to ctrl()?)
Here's the complete working example and the fiddle that runs it:
describe('Unit: LocationController', function () {
var $scope, $location, ctrl;
beforeEach(function () {
angular.mock.module('TDE');
inject(function (_$location_, _$rootScope_, _$controller_) {
$location = _$location_;
$scope = _$rootScope_.$new();
ctrl = _$controller_('LocationController', {
'$scope': $scope
})
});
});
it("should just be a holder for something for later", function () {
expect($scope.BoolCondition()).toBeDefined();
expect($scope.BoolCondition()).toBeTruthy();
});
})