Currently, I am writing a test (using Jasmine) for a directive, and I suspect the link function is not being triggered.
The directive is as follows:
.directive('userWrapperUsername', [
'stringEntryGenerateTemplate',
'stringEntryGenerateLinkFn',
// UserWrapper username column
// Attribute: 'user-wrapper-username'
// Attribute argument: A UserWrapper object with a 'newData' key into an
// object, which contains a 'username' key holding the
// UserWrapper's username
function(stringEntryGenerateTemplate, stringEntryGenerateLinkFn) {
return {
template: stringEntryGenerateTemplate('username'),
restrict: 'A',
scope: true,
link: stringEntryGenerateLinkFn('userWrapperUsername', 'username')
};
}
])
So it makes use of 2 functions provided through factories, namely stringEntryGenerateTemplate and stringEntryGenerateLinkFn.
The stringEntryGenerateTemplate function takes in a string and returns a string.
The stringEntryGenerateLinkFn function, when called returns the actual link function. It mostly consists of event handlers so I shall simplify it to:
function stringEntryGenerateLinkFn(directiveName, key) {
return function(scope, element, attr) {
scope.state = {};
scope.userWrapper = scope.$eval(attr[directiveName]);
}
}
Here is how I use the directive:
<div user-wrapper-username="u"></div>
Here is my test case:
describe('UserWrapper Table string entry', function() {
var $scope
, $compile;
beforeEach(inject(function($rootScope, _$compile_) {
$scope = $rootScope.$new();
$compile = _$compile_;
}));
it('should be in stateDisplay if the value is non empty', function() {
var userWrapper = {
orgData: {
student: {
hasKey: true,
value: 'abcdef'
}
},
newData: {
student: {
hasKey: true,
value: 'abcdef',
changed: false
}
}
}
, key = 'student'
, elem
, elemScope;
$scope.userWrapper = userWrapper;
elem = $compile('<div user-wrapper-username="userWrapper"></div>')($scope);
elemScope = elem.scope();
expect(elemScope.userWrapper).toBe(userWrapper);
expect(elemScope.state).toEqual(jasmine.any(Object)); // this fails
});
});
So I get a test failure saying that elemScope.state is undefined. Recall that I had a scope.state = {}; statement and it should be executed if the link function is executed. I tried a console.log inside the link function and it is not executed as well.
So how do I trigger the link function?
Thanks!
It turns out that I have to initialize the module containing the factories stringEntryGenerateTemplate and stringEntryGenerateLinkFn, which is the same module that contains the userWrapperUsername directive by adding this into my test case:
beforeEach(module('userWrapper', function() {}));
where userWrapper is the name of the module.
So the test case becomes:
describe('UserWrapper Table string entry', function() {
var $scope
, $compile;
beforeEach(module('userWrapper', function() {}));
beforeEach(inject(function($rootScope, _$compile_) {
$scope = $rootScope.$new();
$compile = _$compile_;
}));
it('should be in stateDisplay if the value is non empty', function() {
var userWrapper = {
orgData: {
student: {
hasKey: true,
value: 'abcdef'
}
},
newData: {
student: {
hasKey: true,
value: 'abcdef',
changed: false
}
}
}
, key = 'student'
, elem
, elemScope;
$scope.userWrapper = userWrapper;
elem = $compile('<div user-wrapper-username="userWrapper"></div>')($scope);
elemScope = elem.scope();
expect(elemScope.userWrapper).toBe(userWrapper);
expect(elemScope.state).toEqual(jasmine.any(Object)); // this fails
});
});
This seems like quite a big oversight on my part. Hopefully this will help anyone facing a similar issue.
Related
I have a controller like this.
app.controller('MyCtrl', function() {
let ctrl = this;
if (ctrl.contract) {
ctrl.contract.typeId = ctrl.contract.type.id;
} else {
ctrl.contract = {
name: 'test'
};
}
....
It can either have, or not have contract bound to it.
Problem arises in my test
describe('MyCtrl', () => {
let ctrl;
beforeEach(angular.mock.module('mymodule'));
beforeEach(angular.mock.module('ui.router'));
describe('Update contract', () => {
beforeEach(inject((_$controller_) => {
ctrl = _$controller_('MyCtrl', {}, {
contract: {
type: {
id: 2
}
}
});
}));
it('should set the contract.typeId to be the id of the type of the contract that was sent in', () => {
expect(ctrl.contract.typeId).toBe(ctrl.contract.type.id);
});
})
});
I pass in a contract object, which means it should go into the first if in my controller and set the typeId.
But it always goes into the else no matter what I do.
How can I make sure the controller don't run or start before all my variables are bound to it?
I believe that the issue is that you need to get the $controller service directly and also pass a valid scope (not an empty object)
beforeEach(inject(function ($rootScope, $controller) {
scope = $rootScope.$new();
ctrl = $controller('MyCtrl', {
$scope: scope,
}, {
contract: {
type: {
id: 2
}
}
});
}));
I neeed to pass a value from this part of the code in my directive to a controller, but not sure how to achieve that:
if (!scope.multiple) {
scope.model = value;
console.log(scope.model);
return;
}
I get the value in the console.log, I just don't know how to pass it to the controller.
This is the complete directive:
angular.module('quiz.directives')
.directive('fancySelect', function($rootScope, $timeout) {
return {
restrict: 'E',
templateUrl: 'templates/directives/fancySelect.html',
scope: {
title: '#',
model: '=',
options: '=',
multiple: '=',
enable: '=',
onChange: '&',
class: '#'
},
link: function(scope) {
scope.showOptions = false;
scope.displayValues = [];
scope.$watch('enable', function(enable) {
if (!enable && scope.showOptions) {
scope.toggleShowOptions(false);
}
});
scope.toggleShowOptions = function(show) {
if (!scope.enable) {
return;
}
if (show === undefined) {
show = !scope.showOptions;
}
if (show) {
$rootScope.$broadcast('fancySelect:hideAll');
}
$timeout(function() {
scope.showOptions = show;
});
};
scope.toggleValue = function(value) {
if (!value) {
return;
}
if (!scope.multiple) {
scope.model = value;
console.log(scope.model);
return;
}
var index = scope.model.indexOf(value);
if (index >= 0) {
scope.model.splice(index, 1);
}
else {
scope.model.push(value);
}
if (scope.onChange) {
scope.onChange();
}
};
scope.getDisplayValues = function() {
if (!scope.options || !scope.model) {
return [];
}
if (!scope.multiple && scope.model) {
return scope.options.filter(function(opt) {
return opt.id == scope.model;
});
}
return scope.options.filter(function(opt) {
return scope.model.indexOf(opt.id) >= 0;
});
};
$rootScope.$on('fancySelect:hideAll', function() {
scope.showOptions = false;
});
}
};
});
Updated
I tried to do as suggested in the answers by #Zidane and defining my object first in the controller like this:
$scope.year = {};
var saveUser = function(user) {
$scope.profilePromise = UserService.save(user);
console.log($scope.year);
This is the template:
<fancy-select
title="Klassetrinn"
model="year"
options="years"
enable="true"
on-change="onChangeYears()"
active="yearsActive"
name="playerYear"
form-name="registerForm"
>
</fancy-select>
But I got an empty object in that case.
When I define my objects like this I get the right value in the controller but in the view the title is not being displayed anymore:
$scope.search = {
years: []
};
var saveUser = function(user) {
$scope.profilePromise = UserService.save(user);
console.log($scope.search.years);
<fancy-select
title="Klassetrinn"
model="search.years"
options="years"
enable="true"
on-change="onChangeYears()"
active="yearsActive"
name="playerYear"
form-name="registerForm"
>
</fancy-select>
As you defined an isolated scope for your directive like this
scope: {
...
model: '=',
...
},
you give your directive a reference to an object on your controller scope.
Declaring the directive like <fancy-select model="myModel" ....></fancy-select> you pass your directive a reference to scope.myModel on your controller. When you modify a property on the scope.model object in your directive you automatically modify the same property on the scope.myModel object in your controller.
So you have to do
myApp.controller('myController', function($scope) {
...
$scope.myModel = {};
...
}
in your controller and in your directive just do
if (!scope.multiple) {
scope.model.value = value;
return;
}
Then you can get the value in your controller via $scope.myModel.value.
For clarification: You have to define an object on your controller and pass the directive the reference for this object so that the directive can follow the reference and doesn't mask it. If you did in your directive scope.model = 33 then you would just mask the reference passed to it from the controller, which means scope.model wouldn't point to the object on the controller anymore. When you do scope.model.value = 33 then you actually follow the object reference and modify the object on the controller scope.
you can use services or factories to share data between your angular application parts, for example
angular.module('myapp').factory('myDataSharing', myDataSharing);
function myDataSharing() {
var sharedData = {
fieldOne: ''
};
return {
setData: setData,
getData: getData,
};
function setData(dataFieldValue) {
sharedData.fieldOne = dataFieldValue;
};
function getData() {
sharedData.fieldOne
};
directive:
myDataSharing.setData(dataValue);
controller:
angular.module('myapp').controller('myController' ['myDataSharing'], function(myDataSharing) {
var myDataFromSharedService = myDataSharing.getData();
}
I'm trying to unit test a function within my controller but am unable to get a $scope variable to be testable. I'm setting the variable in my controller's .then() and want to unit test to make sure this is set appropriately when it hits the .then block.
My test controller code:
function submit() {
myService.submit().then(function(responseData){
if(!responseData.errors) {
$scope.complete = true;
$scope.details = [
{
value: $scope.formattedCurrentDate
},
{
value: "$" + $scope.premium.toFixed(2)
},
];
} else {
$scope.submitError = true;
}
});
}
Where this service call goes is irrelevant. It will return JSON with action: 'submitted', 'response' : 'some response'. The .then() checks if errors are present on responseData, and if not it should set some details. These $scope.details are what I'm trying to test in my unit test below:
it('should handle submit details', function () {
var result;
var premium = 123.45;
var formattedCurrentDate = "2016-01-04";
var promise = myService.submit();
mockResponse = {
action: 'submitted',
response: 'some response'
};
var mockDetails = [
{
value: formattedCurrentDate
},
{
value: "$"+ premium.toFixed(2)
}
];
//Resolve the promise and store results
promise.then(function(res) {
result = res;
});
//Apply scope changes
$scope.$apply();
expect(mockDetails).toEqual(submitController.details);
});
I'm receiving an error that $scope.details is undefined. I'm not sure how to make the test recognize this $scope data changing within the controller.
Before each and other functions in my unit test:
function mockPromise() {
return {
then: function(callback) {
if (callback) {
callback(mockResponse);
}
}
}
}
beforeEach(function() {
mockResponse = {};
module('myApp');
module(function($provide) {
$provide.service('myService', function() {
this.submit = jasmine.createSpy('submit').and.callFake(mockPromise);
});
});
inject(function($injector) {
$q = $injector.get('$q');
$controller = $injector.get('$controller');
$scope = $injector.get('$rootScope');
myService = $injector.get('myService');
submitController = $controller('myController', { $scope: $scope, $q : $q, myService: myService});
});
});
How do I resolve the promise within my unit test so that I can $scope.$digest() and see the $scope variable change?
You should look how to test promises with jasmine
http://ng-learn.org/2014/08/Testing_Promises_with_Jasmine_Provide_Spy/
using a callFake would do what you try to mock
spyOn(myService, 'submit').and.callFake(function() {
return {
then: function(callback) { return callback(yourMock); }
};
});
Below is my plnkr
http://plnkr.co/edit/f6LYS2aGrTXGkZ3vrIDD?p=preview
I have issue on Search Page
angular.module('plexusSelect', []).directive('plexusSelect', ['$ionicModal',
function($ionicModal) {
// Runs during compile
return {
scope: {
'items': '=',
'text': '#',
'textIcon': '#',
'headerText': '#',
'textField': '#',
'textField2': '#',
'valueField': '#',
'callback': '&'
},
require: 'ngModel',
restrict: 'E',
templateUrl: 'templates/plexusSelect.html',
link: function($scope, iElm, iAttrs, ngModel) {
if (!ngModel) return; // do nothing if no ng-model
$scope.allowEmpty = iAttrs.allowEmpty === 'false' ? false : true;
$scope.defaultText = $scope.text || '';
$ionicModal.fromTemplateUrl('plexusSelectItems.html', {
'scope': $scope
}).then(function(modal) {
$scope.modal = modal;
$scope.modal['backdropClickToClose'] = false;
});
$scope.showItems = function($event) {
$event.preventDefault();
$scope.modal.show();
};
$scope.hideItems = function() {
$scope.modal.hide();
};
/* Destroy modal */
$scope.$on('$destroy', function() {
$scope.modal.remove();
});
$scope.viewModel = {};
$scope.clearSearch = function() {
$scope.viewModel.search = '';
};
/* Get field name and evaluate */
$scope.getItemName = function(field, item) {
return $scope.$eval(field, item);
};
$scope.validateSingle = function(item) {
$scope.text = $scope.$eval($scope.textField, item) + ($scope.textField2 !== undefined ? " (" + $scope.$eval($scope.textField2, item) + ")" : "");
$scope.value = $scope.$eval($scope.valueField, item);
$scope.hideItems();
if (typeof $scope.callback == 'function') {
$scope.callback($scope.value);
}
ngModel.$setViewValue($scope.value);
};
$scope.$watch('text', function(value) {
if ($scope.defaultText === value) $scope.placeholder = 'placeholderGray';
else $scope.placeholder = 'placeholderBlack';
});
}
};
}
])
Where in I have reference http://code.ionicframework.com/1.0.0-beta.14/js/ionic.bundle.js ionic bundle than my second directive search filter will stop working but at the same time, if I reference http://code.ionicframework.com/1.0.0-beta.1/js/ionic.bundle.js it works search filter in both directive.
In beta.14 angularjs 1.3 is used and in beta.1 angularjs 1.2
So somebody told me that it can be the migration issue, But I check angularjs migration documentation but I could not find anything. Kindly somebody help me what can be the issue.
Problem:
This is due to the following breaking change in Angular 1.3.6.
Excerpt:
filterFilter: due to a75537d4,
Named properties in the expression object will only match against
properties on the same level. Previously, named string properties
would match against properties on the same level or deeper.
...
In order to match deeper nested properties, you have to either match
the depth level of the property or use the special $ key (which still
matches properties on the same level or deeper)
In the first use of your directive items have the following structure:
[
{ property: 'Value' }
]
And in your second use:
[
{ Destination: { property: 'Value' } }
]
Sadly a bug fix that you probably need wasn't introduced until 1.3.8:
filterFilter:
make $ match properties on deeper levels as well
(bd28c74c, #10401)
let expression object {$: '...'} also match
primitive items (fb2c5858, #10428)
Solution:
Use Ionic with AngularJS 1.3.8 or later.
Change your HTML to the following:
<label ng-repeat="item in items | filter: { $: viewModel.search }" ...
Initialize viewModel.search as an empty string:
$scope.viewModel = { search: '' };
Demo: http://plnkr.co/edit/ZAM33j82gT4Y6hqJLqAl?p=preview
I want to watch angular factory variable from inside directive, and act upon change.
I must be missing something fundamental from Javascript, but can someone explain, why approach (1) using inline object works, and approach (2) using prototyping does not?
Does prototype somehow hide user variable scope from angular $watch?
How can i make this code more clean?
(1):
Plunkr demo
angular.module('testApp', [
])
.factory('myUser', [function () {
var userService = {};
var user = {id : Date.now()};
userService.get = function() {
return user;
};
userService.set = function(newUser) {
user = newUser;
};
return userService;
}])
.directive('userId',['myUser',function(myUser) {
return {
restrict : 'A',
link: function(scope, elm, attrs) {
scope.$watch(myUser.get, function(newUser) {
if(newUser) {
elm.text(newUser.id);
}
});
}
};
}])
.controller('ChangeCtrl', ['myUser', '$scope',function(myUser, $scope) {
$scope.change = function() {
myUser.set({id: Date.now()});
};
}]);
(2):
Plunkr demo
angular.module('testApp', [
])
.factory('myUser', [function () {
var user = {id : Date.now()};
var UserService = function(initial) {
this.user = initial;
}
UserService.prototype.get = function() {
return this.user;
};
UserService.prototype.set = function(newUser) {
this.user = newUser;
};
return new UserService(user);
}])
.directive('userId',['myUser',function(myUser) {
return {
restrict : 'A',
link: function(scope, elm, attrs) {
scope.$watch(myUser.get, function(newUser) {
//this watch does not fire
if(newUser) {
elm.text(newUser.id);
}
});
}
};
}])
.controller('ChangeCtrl', ['myUser', '$scope',function(myUser, $scope) {
$scope.change = function() {
myUser.set({id: Date.now()});
};
}]);
Case 1:
myUser.get function reference is called without a context (return user), and the user object is returned as a closure variable.
Case 2:
myUser.get function reference is called without a context (return this.user), and so this.user only just don't throw an error because you are not in "strict mode" where this is pointing to the window object, thus resulting in this.user being just undefined.
What you actually missed is the fact that giving myUser.get as a watcher check function is giving a reference to a function, which will not be applied to myUser as a context when used by the watcher.
As i remember angular watches only properties belonging to the object.
The watch function does this by checking the property with hasOwnProperty