I would like to set attrs to undefined from unit test, I tried few approaches and didn't get to set it to undefined. Below is my directive:
angular.module('myApp').directive('someElement', function () {
var directive = {};
directive.restrict = 'E';
directive.replace = true;
directive.transclude = true;
directive.templateUrl = function (element, attrs) {
var template = '';
if(attrs) { // would like to invoke this in unit test and set it to `undefined`
//do something
}
return template;
};
directive.scope = {...};
directive.compile = function () {
//do something
return directive;
});
here is a the snippet, assume that directive is compiled and $digest cycle is triggered. here is what i got in the test:
it('should set attrs to undefined', function () {
.....
attrs = {};
scope.$apply();
expect(attrs).toBeUndefined(); // I want this to pass!!
});
It is possible to get original directive factory from within a test and modify it before compiling and even unit-test it's methods, but I'am not sure if this approach to test directives is right.
If you take a look at the source code, you can see that angular registers each directive as a factory with a 'Directive' suffix. Knowing that, you can inject your directive factory in a test:
var directiveFactory;
beforeEach(inject(function ($injector) {
// considering that you directive is called 'myEl'
directiveFactory = $injector.get('myElDirective')[0];
}));
Here one should use [0]'s because factories are returned in array - that's the way angular deals with directives, which have an option multiElement: true (if you have this option enabled, then maybe this trick won't work).
As a result the variable directiveFactory now holds an actual factory, so original templateUrl could be replaced and fake called with undefined attrs:
it('should do something when attrs are undefined', function () {
var template = '<my-el></my-el>';
// backup original function
var originalTemplateUrl = directiveFactory.templateUrl;
// replace with fake one
directiveFactory.templateUrl = function (element, attrs) {
// call original function with undefined attrs
return originalTemplateUrl(element, undefined);
};
var element = $compile(template)($scope);
$scope.$digest();
// expect whatever
});
Now you could examine calls to your directive's original templateUrl and find out that attrs are undefined.
See the plunker
Related
I am using the controller as syntax from angular and i want to test my code using jasmine and sinon.
Let's say i want the following controller code :
angular
.module('Test')
.controller('TestController', TestController);
TestController.$inject = [];
function TestController() {
var viewModel = this;
viewModel.myFunction = myFunction;
function myFunction(){
//do something
//now call a helper function
helperFunction()
}
function helperFunction(){
// ....
}
}
My question is how i can test the helperFunction() or even put a spy on it ? My helper is not visible in my test.
Here is my test :
(function () {
'use strict';
var myController;
describe('Test', function () {
beforeEach(module('Test'));
beforeEach(inject(function ($controller, $injector) {
myController = $controller('TestController');
}));
it('Tests helperFunction', function (){
var sinonSpy = sinon.spy(myController, 'helperFunction');
//perform the action
myController.myFunction();
//expect the function was called - once
expect(sinonSpy .callCount).toEqual(1);
}
})
})
You cannot have access to those functions. When you define a named JS function it's the same as saying:
var helperFunction = function(){};
In which case it would be pretty clear to see that the var is only in the scope within the block and there is no external reference to it from the wrapping controller.
To make a function testable, you need to add it to the $scope of the controller.
viewModel.helperFunction = helperFunction;
But be aware that is not a good idea to be exposing everything just to make it testable. You really need to consider if testing it will actually add some value to your project
try to do so :
var vm = controller("helperFunction", { $scope: scope });
and then:
vm.myFunction();
Add the following code into your controller:
angular.extend($scope, {
helperFunction:helperFunction
});
The goal here is to have two different directives that are technically siblings share functionality. I will either use one or the other, never one inside the other.
However, the second directive will have all the capability of the first with some small additions. Because of this, I would like the functionality to inherit from the "Parent" directive to the "Child".
I'm achieving this by re-using the same directive definition object from the Parent on the Child, with the exception of the controller/template fields being changed.
This was all working well up until I hit the watchers from my ParentDirCtrl. For some reason the watcher seems to be set up correctly watching mydir.obj1 and yet somehow inside the watcher callback function mydir.obj1 becomes undefined.
I'm assuming something about _.extend/$controller is changing how the $scopes work so mydir.obj1 isn't defined in the ParentDirCtrl, but I'm not sure why that would be the case.
Plunk
angular.module('plunker', [])
// lodash
.constant('_', _)
.controller('MainCtrl', function($scope, $timeout) {
$scope.obj = {
name: 'John',
age: 30,
};
})
.controller('ParentDirCtrl', function($scope) {
var mydir = this;
mydir.doStuffInParent = function() {
alert('executed from the parent directive');
}
$scope.$watch('mydir.obj1', function() {
// ====================================
// ERROR
// Why is 'mydir.obj1' undefined when
// occupation is set?
// ====================================
mydir.obj1.occupation = 'Meteorologist';
});
})
.directive('parentDirective', parentDirective)
.directive('childDirective', function() {
// borrow the directive definition object from the parent directive
var parentDDO = parentDirective();
// uodate the template and controller for our new directive
parentDDO.template = [
'<div>',
'<p ng-click="mydir.doStuffInParent()">{{mydir.obj1.name}}</p>',
'<p ng-click="mydir.doStuffInChild()">{{mydir.obj1.age}}</p>',
'</div>'
].join('');
parentDDO.controller = function($scope, $controller, _) {
// extend 'this' with the Parent's controller
var mydir = _.extend(this, $controller('ParentDirCtrl', { $scope: $scope }));
mydir.doStuffInChild = function() {
alert("executed from the child directive");
};
};
return parentDDO;
});
// this will be moved to the top during declaration hoisting
function parentDirective() {
return {
restrict:'E',
scope: {},
bindToController: {
obj1: '=',
},
template: '<div>{{mydir.obj1}}</div>',
controller: 'ParentDirCtrl',
controllerAs: 'mydir',
};
}
obj1 is populated on the child controller instance - that's why mydir.obj1 is undefined in the parent watcher. You can access obj1 directly via scope or by using the reference passed into the watcher:
$scope.$watch('mydir.obj1', function(val) {
$scope.mydir.obj1.occupation = 'Meteorologist';
// or
val.occupation = 'Meteorologis';
});
There is no scope inheritance here - both controllers operate on the same scope. Controller-AS syntax is what confuses you - I'd get rid of it to make things clearer.
I'm familiarizing myself with controllerAs syntax in AngularJS, and I've come to a problem when I need to do a simple binding to a service variable. Typically a $scope.$watch or $scope.$on would do, but that would involve injecting $scope, which seems to defeat the purpose of controllerAs.
Currently what I have is that after clicking one of the buttons and calling config.setAttribute(attr), the controller calls the service's setAttribute function, but not getAttribute, so config.attribute never changes.
Is there something I'm overlooking in how I'm approaching this? Would I need to inject $scope or change the controller syntax to use $scope instead?
View:
<div data-ng-controller="ConfigCtrl as config">
<h3>Customize</h3>
<pre>Current attribute: {{config.attribute}}</pre>
<label>Attributes</label>
<div data-ng-repeat="attr in config.attributes">
<button ng-click="config.setAttribute(attr)">{{attr.name}}</button>
</div>
</div>
Service:
(function() {
'use strict';
angular.module('app')
.factory('Customization', Customization);
function Customization() {
var service = {
attribute: null,
getAttributes: getAttributes,
setAttribute: setAttribute,
getAttribute: getAttribute
}
return service;
/////
function getAttributes() {
return [
{name: 'Attr1', value: '1'},
{name: 'Attr2', value: '2'} // etc.
];
}
function setAttribute(attr) {
service.attribute = attr;
}
function getAttribute() {
return service.attribute;
}
}})();
Controller:
(function(){
'use strict';
angular.module('app')
.controller('ConfigCtrl', ConfigCtrl);
function ConfigCtrl(Customization){
var vm = this;
vm.attribute = Customization.getAttribute(); // bind
vm.attributes = [];
// Functions
vm.setAttribute = Customization.setAttribute;
init();
/////
function init(){
// Get attributes array
vm.attributes = Customization.getAttributes();
}
}})();
Here is what my controller looks like after injecting $scope and adding the watch for attribute:
(function(){
'use strict';
angular.module('app')
.controller('ConfigCtrl', ConfigCtrl);
function ConfigCtrl($scope, Customization){
var vm = this;
vm.attribute;
vm.attributes = [];
// Functions
vm.setAttribute = Customization.setAttribute;
init();
/////
function init(){
// Get attributes array
vm.attributes = Customization.getAttributes();
}
$scope.$watch(function() {
return Customization.getAttribute()
}, function() {
vm.attribute = Customization.getAttribute();
});
}})();
I also have the Karma test in case anyone is interested:
(function() {
'use strict';
describe('ConfigCtrl', function () {
var ConfigCtrl, scope;
beforeEach(module('app'));
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new();
ConfigCtrl = $controller('ConfigCtrl',
{$scope: scope}
);
}));
describe('#setAttribute', function(){
it('sets the current attribute', function(){
var selected = {
name:'Attr1',
value:'1'
};
ConfigCtrl.setAttribute(selected);
scope.$apply();
expect(ConfigCtrl.attribute).to.eql(selected);
});
});
});
})();
Thanks for the help. I'm welcome to any better answers anyone else might have.
I am working on a calculator that will consider AWS instance costs. I am pulling the data from a .js file on amazon and I would like to read it into an object but i keep getting an error "Uncaught ReferenceError: callback is not defined" .. here is my .js file.
(function() {
var app = angular.module('formExample', []);
var ExampleController = function($scope, $http) {
$scope.master = {};
$scope.update = function(user) {
$scope.master = angular.copy(user);
$scope.GetAws();
};
$scope.reset = function() {
$scope.user = "";
};
function callback(data) {
$scope.aws = data;
}
$scope.GetAws = function() {
var url = "http://a0.awsstatic.com/pricing/1/ec2/linux-od.min.js?callback=callback";
$http.jsonp(url);
};
$scope.reset();
};
app.controller('ExampleController', ['$scope', '$http', ExampleController]);
}());
It is weird that the aws link you are using supports jsonp but it does not take custom callback function name. (Atleast you can look up to find out if the query string they are looking for is callback or not). angular handles it when we provide callback=JSON_CALLBACK it gets translated to angular.callbacks_x which are exposed globally temporarily by angular to handle the request and resolve the promise accordingly. But for this the endpoint must take the callback argument and wrap the response in the same string and function invocation. However this endpoint does not seem to consider it and even without any callback it automatically wraps into default callback function invocation. So you would need to inject $window (Correct DI way) object and set callback function to it and ?callback=callback is irrelevant.
var ExampleController = function($scope, $http, $window) {
$scope.master = {};
//....
$window.callback = function(data) {
$scope.aws = data;
}
$scope.GetAws = function() {
var url = "http://a0.awsstatic.com/pricing/1/ec2/linux-od.min.js?callback=callback";
$http.jsonp(url);
};
$scope.reset();
};
app.controller('ExampleController', ['$scope', '$http', '$window', ExampleController]);
Plnkr
It is because the AWS script is looking to call a function called "callback" on the global scope (outside of Angular). Since your function is within the scope of another (IIFE) function, it cannot be accessed.
What I've done in a case like this is simply put the function in the global scope.
In cases where an application requires some API to have loaded before Angular can do it's magic and has a callback similar to your situation, I have done the following, manually bootstrapping Angular:
index.html
<script src="http://www.example.com/api?callback=myCallbackFunction"></script>
app.js
// callback function, in global (window) scope
function myCallbackFunction() {
// manually bootstrap Angular
angular.element(document).ready(function() {
angular.bootstrap(document, ['myApp']);
});
}
// your IIFE
(function() {
})();
Notice callback should be set in window scope.
So,one solution is like:
$scope.reset = function() {
$scope.user = "";
};
window.callback = function(data) {
$scope.aws = data;
}
$scope.GetAws = function() {
var url = "http://a0.awsstatic.com/pricing/1/ec2/linux-od.min.js?callback=callback";
$http.jsonp(url);
};
AngularJs 1.3.x, simple controller works but as soon as I re-write it using Typescript and Injection, it fails. If I reference 1.2.x it starts working again.
//This works in 1.3.x
scopeApp.controller('MyController', ['$scope', function ($scope) {
$scope.username = 'World';
$scope.sayHello = function () {
$scope.greeting = 'Hello ' + $scope.username + '!';
};
}]);
http://plnkr.co/edit/ssSuuZuGlrypemx3BU5r?p=preview
//This DOES NOT works in 1.3.x but does in 1.2.x
//The following code is produced via Typescript:
var MainFeature;
(function (MainFeature) {
var MainCtrl = (function () {
function MainCtrl($scope) {
this.scope = $scope;
this.name = "Sirar";
this.message = '';
}
MainCtrl.prototype.SetMessage = function () {
this.message = 'Hello' + this.name;
};
return MainCtrl;
})();
MainFeature.MainCtrl = MainCtrl;
})(MainFeature || (MainFeature = {}));
scopeApp.controller("MainCtrl", ["$scope", function ($scope) {
return new MainFeature.MainCtrl($scope);
}]);
Breaking changes docs that have valuable information but didn't help:
https://docs.angularjs.org/guide/migration
http://ng-learn.org/2014/06/Migration_Guide_from_1-2_to1-3/
http://wildermuth.com/2014/11/11/Angular_1_3_and_Breaking_Change_for_Controllers
You need to pass the constructor function, not some other function like you did. As I explained in another answer the controller is not created by calling new. It's created as follows:
instance = Object.create(controllerPrototype);
...
return fn.apply(self, args);
The catch is that the return value is not used, but the instance. In your case this would mean:
instance = Object.create({}); // should be MainCtrl.prototype
...
return fn.apply(self, args);
So "MainCtrl" ends up as empty object. You have to do what you should have done in the first place, pass the constructor:
scopeApp.controller("MainCtrl", ["$scope", MainFeature.MainCtrl]);