How to access the scope of the directive in Jasmine Test - javascript

I cant get the scope of my directive (another option is that I get the scope but my function is not there). I am using Jasmine over Karma.
My directive:
angular.module('taskApp').directive('nkAlertDir', ['$http', function ($http) {
return {
template: "<span>{{msg}}</span>",
replace: true,
link: function (scope, elem, attrs) {
scope.values = {
canvas: canvasSize,
radius: size,
center: canvasSize / 2
};
$http.get('someUrl')
.suncces(function (data) {
scope.msg = data;
})
.error(function (err) {
//there is always be error because this url does nor exist
//but this is not the point in this exercise
});
scope.returnNumbers = function () {
return [2, 2];
}
}
}
}]);
My test:
describe('Unit: Directives', function () {
var element;
var scope;
beforeEach(module('taskApp'));
beforeEach(inject(function ($compile, $rootScope) {
scope = $rootScope;
element = angular.element('<div nk-alert-dir></div>');
scope.size = 100;
$compile(element)(scope);
scope.$digest();
}));
it('should bind an alert message of "task added!"', function () {
var dirScope = element.isolateScope();
console.log(dirScope);
});
});
Somehow I always get the dirScope is undefined
I have tried this:
replaced the digest() in $apply()
replaced the $rootScope in $rootScope.$new()

You don't have an isolated scope in your directive, so element.isolateScope() is going to return undefined.
Try just accessing the scope: element.scope()
If you want an isolate scope on your directive, then you have to set the scope property of your directive definition object to a JS object and add the properties there. Then you'd be able to use element.isolateScope to get access to the isolate scope.
return {
template: "<span>{{msg}}</span>",
replace: true,
scope : {}, // Any properties you want to pass in to the directive on scope would get defined here
link: function (scope, elem, attrs) {
scope.values = {
canvas: canvasSize,
radius: size,
center: canvasSize / 2
};
$http.get('someUrl')
.suncces(function (data) {
scope.msg = data;
})
.error(function (err) {
//there is always be error because this url does nor exist
//but this is not the point in this exercise
});
scope.returnNumbers = function () {
return [2, 2];
}
}
}

Related

AngularJS override directive controller function

Here is the problem. I have some 3rd party directive called main-directive.
app.directive('mainDirective', function() {
return {
scope: {
foo: '&'
// attrs
},
controller: function($scope) {
$scope.click = function() {
window.alert($scope.foo());
}
},
template: '<button ng-click="click()">Click me</button>'
}
});
So I want to make my own directive called parent-directive which assign application specific default values to third party directive attributes.
app.directive('parentDirective', function() {
return {
scope: {
foo: '&?',
attr2: '='
// lots of attrs
},
controller: function($scope) {
$scope.attr1 = "some default value"
$scope.foo = function() {
return "not overrided"
}
if (this.foo) {
$scope.foo = this.foo
}
},
template: '<div class="some-styling"><main-directive foo="foo()" attr1="attr1" attr2="attr2"></main-directive></div>'
}
});
What if I want to make another child-directive that keeps parent-directive logic.
Overloading attribute is easy i can use "compile" function. But what about overriding functions is it possible?
app.directive('childDirective', function() {
return {
scope: false,
require: 'parentDirective',
link: function(scope, element, attr, controller) {
controller.foo = function() {
return "overrided";
}
},
compile: function(element, attr) {
attr.attr2 = "attr2";
}
}
});
Whole thing can be easily done by using child scope instead of isolated.
Or by using extending by template. But if I extends directive with template I would have to copy parent "scope" and "template" definition to child-directive and forward all the non-default attributes this doesn't seem like an elegant solution.
So the key question, is there a way to override parent-directive function using isolated scope without forwarding attributes.
Here is the DEMO
Ok, I have done some research and it turns out that there can be several approaches there
Scope inheritance
Since child-directive is not creating own scope it just creating new methods at parent-directive parent scope. So we can modify attributes during compile and specify overridden foo method.
app.directive('parentDirective', function() {
return {
scope: {
fooImpl: '&?',
// lots of attrs
},
controller: function($scope) {
$scope.foo = function() {
if ($scope.fooImpl) {
return $scope.fooImpl();
}
return "not overrided";
}
},
template: '<div class="some-styling"><main-directive foo="foo()"></main-directive></div>'
}
});
app.directive('childDirective', function() {
return {
scope: false,
require: 'parentDirective',
controller: function($scope) {
$scope.foo = function() {
return "overrided";
}
},
compile: function(element, attr) {
attr.fooImpl = "foo()";
}
}
});
Here is the DEMO1
Add to isolated scope
Angular provides special function. That can get isolated scope from element. So we can override our method during linking phase.
app.directive('parentDirective', function() {
return {
scope: {
fooImpl: '&?',
// lots of attrs
},
controller: function($scope) {
$scope.foo = function() {
if ($scope.fooImpl) {
return $scope.fooImpl();
}
return "not overrided";
}
},
template: '<div class="some-styling"><main-directive foo="foo()"></main-directive></div>'
}
});
app.directive('childDirective', function() {
return {
scope: false,
require: 'parentDirective',
link: function(scope, element, attr) {
var innerScope = angular.element(element[0]).isolateScope();
innerScope.foo = function() {
return "overrided";
}
}
}
});
Here is the DEMO2
Controller method
If we use controllerAs syntax. That means we exposing controller object variables as a scope. We can override function in child directive during linking phase.
app.directive('parentDirective', function() {
return {
scope: {
fooImpl: '&?',
// lots of attrs
},
controller: function($scope) {
var vm = this;
vm.foo = function() {
return "not overrided";
}
},
controllerAs : 'vm',
template: '<div class="some-styling"><main-directive foo="vm.foo()"></main-directive></div>'
}
});
app.directive('childDirective', function() {
return {
scope: false,
require: 'parentDirective',
link: function (scope, element, attr, controller) {
controller.foo = function() {
return "overrided";
}
}
}
});
Here is the DEMO3
Transclusion
Practically you can do the same thing with seperate parent and child directive and using transclusion. But anyway it would be combination of above approaches. Thanks for "Extending an existing directive in AngularJS"

Jasmine unit test to check the scope of an Angular Bootstrap modal

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

Undefined function in AngularJS directive unit test

I am trying to write a unit test for the toggleDetails function defined inside the following AngularJS directive:
angular.module('hadoopApp.cluster.cluster-directive', [])
.directive('cluster', [function() {
return {
templateUrl:'components/cluster/cluster.html',
restrict: 'E',
replace: true,
scope: {
clusterData: '=',
showDetails: '='
},
link: function(scope, element, attrs) {
scope.toggleDetails = function() {
console.log('Test');
scope.showDetails = !scope.showDetails;
};
},
// Default options
compile: function(tElement, tAttrs){
if (!tAttrs.showDetails) { tAttrs.showDetails = 'false'; }
}
};
}]);
And this is the unit test:
'use strict';
describe('hadoopApp.cluster module', function() {
// Given
beforeEach(module('hadoopApp.cluster.cluster-directive'));
var compile, mockBackend, rootScope;
beforeEach(inject(function($compile, $httpBackend, $rootScope) {
compile = $compile;
mockBackend = $httpBackend;
rootScope = $rootScope;
}));
var dummyCluster;
beforeEach(function() {
dummyCluster = {
id:"189",
name:"hadoop-189",
exitStatus:0
};
mockBackend.expectGET('components/cluster/cluster.html').respond(
'<div><div ng-bind="clusterData.name"></div></div>');
});
it('should toggle cluster details info', function() {
var scope = rootScope.$new();
scope.clusterData = dummyCluster;
// When
var element = compile('<cluster' +
' cluster-data="clusterData" />')(scope);
scope.$digest();
mockBackend.flush();
// Then
var compiledElementScope = element.isolateScope();
expect(compiledElementScope.showDetails).toEqual(false);
// When
console.log(compiledElementScope);
compiledElementScope.toggleDetails();
// Then
expect(compiledElementScope.showDetails).toEqual(true);
});
afterEach(function() {
mockBackend.verifyNoOutstandingExpectation();
mockBackend.verifyNoOutstandingRequest();
});
});
The test fails when calling compiledElementScope.toggleDetails() because the toggleDetails function is undefined:
TypeError: undefined is not a function
Printing the content of the isolated scope inside compiledElementScope I can see that in fact the function is not included in the object.
So, it looks like the toggleDetails function is not included in the isolated scope but I don't know why.
If you use the compile function within a directive, the link function is ignored. You should return the function within the compile method:
compile: function (tElement, tAttrs) {
if (!tAttrs.showDetails) {
tAttrs.showDetails = 'false';
}
return {
post: function (scope, element, attrs) {
console.log('Test');
scope.toggleDetails = function () {
console.log('Test');
scope.showDetails = !scope.showDetails;
};
}
};
}
Also, in order to make the test work, you should add:
scope.showDetails = false;
And the binding to the directive (because you require two values):
var element = compile('<cluster' +
' cluster-data="clusterData" show-details="showDetails" />')(scope);
Jsfiddle: http://jsfiddle.net/phu7sboz/

Unable to bind my fake array to a scope variable in my test

I'm unable to bind my fake array to a scope variable in my directive test.
My test:
describe('Directive: report - section', function () {
// load the directive's module
beforeEach(module('ReportApp'));
beforeEach(module('Templates')); // The external template file referenced by templateUrl
var element, scope;
beforeEach(inject(function ($rootScope) {
scope = $rootScope.$new();
}));
it('should have 1 section available', inject(function ($compile) {
var testSections = [
{
id: 'Example01',
visible: true,
img: 'image1.jpg'
},
{
id: 'Example02',
visible: false,
img: 'image2.jpg'
}
];
scope.sections = testSections;
element = angular.element('<section></section>');
element = $compile(element)(scope);
scope.$digest();
expect(element.find('li').length).toEqual(1);
}));
});
My directive:
angular.module('ReportApp')
.directive('section', function (report, reportStatus) {
return {
templateUrl: 'src/report/views/parts/section.html',
restrict: 'E',
controller: function( $scope, $element, $attrs){
var sections = report.getDatabase().sections;
$scope.sections = sections;
reportStatus.setActiveSection(sections[0]);
},
link: function postLink(scope, element, attrs) {
}
};
});
My test result:
Chrome 36.0.1985 (Mac OS X 10.9.2) Directive: report - section should have 1 section available FAILED
Expected 4 to equal 1.
Error: Expected 4 to equal 1.
at null.<anonymous> (/Users/user/MyAPPs/temp/report/app/src/report/directives/tests/spec/section.js:77:39)
at Object.invoke (/Users/user/MyAPPs/temp/report/app/vendor/bower_components/angular/angular.js:3678:17)
at workFn (/Users/user/MyAPPs/temp/report/app/vendor/bower_components/angular-mocks/angular-mocks.js:2102:20)
My problem is that the fake sections(testSections) are not being applied. So, this result "Expected 4 to equal 1" is due to the original sections that are being used instead my fake one.
Why this scope does not work ?
scope.sections = testSections;
The reason is your scope.sections = testSections; is replaced by $scope.sections = sections; in your directive code.
In this case, you have to spy your report.getDatabase() to return your testSections
describe('Directive: report - section', function () {
// load the directive's module
beforeEach(module('ReportApp'));
beforeEach(module('Templates')); // The external template file referenced by templateUrl
var element, scope, report;
beforeEach(inject(function ($rootScope,_report_) {
scope = $rootScope.$new();
report = _report_; //inject the report object and store in a variable
}));
it('should have 1 section available', inject(function ($compile) {
var testSections = [
{
id: 'Example01',
visible: true,
img: 'image1.jpg'
},
{
id: 'Example02',
visible: false,
img: 'image2.jpg'
}
];
spyOn(report,"getDatabase").and.returnValue({ sections : testSections });//spy the getDatabase function
//we just need a stub, so we could also write this:
//report.getDatabase = function (){
// return { sections : testSections };
//}
element = angular.element('<section></section>');
element = $compile(element)(scope);
scope.$digest();
expect(element.find('li').length).toEqual(1);
}));
});

How to get data from controller to Factory in angularjs

I am newbie to Angularjs World. I want to fetch some data from angularjs controller to Angularjs Factory.Controller and factory are defined in different js files.
code from Factory(File name Application.js)
var app=angular.module('myApp',[])
app.factory('autoCompleteDataService', [function(MyController) {
return {
getSource: function() {
return MyController.getFirstName();
}
}
}]);
app.directive('autoComplete', function(autoCompleteDataService) {
return {
restrict: 'A',
link: function(scope, elem, attr, ctrl) {
$(elem).autocompleteArray(autoCompleteDataService.getSource(), {
minLength: 2
});
}
}})
Controller code (File Name Controller.js)
function MyController($scope){
this.getFirstName= function ()
{
var arrayFName=[];
for(var k=0;k< $scope.MyData.length;k++)
{
arrayFName.push($scope.MyData[k].fname);
}
return arrayFName;
}
MyData is array containing some hard coded value for 'fname'
When i ran this code got error 'Error: MyController is undefined'. Is it possible to fetch data from controller if yes then how?
You should have source, or firstName, defined in the factory, and then set it from the controller. It will then be accessible from other controllers which use the factory.
var app=angular.module('myApp',[])
app.factory('autoCompleteDataService', [function () {
var _source;
return {
getSource: function () {
return _source;
},
setSource: function (source) {
_source = source;
}
}
}]);
And then subsequent controllers might be like:
app.controller('someController', ['$scope', 'autoCompleteDataService',
function ($scope, autoCompleteDataService) {
$scope.source = autoCompleteDataService.getSource();
// or even...
// $scope.getFirstName = autoCompleteDataService.getSource;
}]);

Categories