Unit testing ControllerAs with forms in AngularJS - javascript

I have the following situation with Angular:
Controller
function Controller () {
// form used in template
this.form ...
}
Template that has a form and uses this controller
Template
<div ng-controller="Controller as ctrl">
<form name="ctrl.form">
...
</div>
I have to say I'm in general confused why Angular doesn't have a better way of adding the form to the controller other than automatically adding either this.form or $scope.form, depending on how you use controllers, but that's I guess another question.
The real issue I have right now is that I'm not sure how I should test this. If I just instantiate the controller in the test, then my form is undefined
$controller('Controller as ctrl')
I did find a way, by just compiling the template
$scope = $rootScope.$new();
$compile(template)($scope);
But because ng-controller in the template starts a new scope, I can't access the controller directly with $scope.ctrl, instead I'd have to do something like $scope.$$childHead.login
... and I feel it's getting too complicated. edit: not to mention that $$ indicates a 'private' property.

I've solved it myself, but I'm leaving it here unaccepted because I don't think the solution is very nice. If anyone knows a better way, please post.
The problem with compiling templates is that it's also not possible to insert mock services in the controller, at least I didn't figure it out. You get a controller instance on $scope, like $scope.ctrl, but that's it.
The second attempt was to locate just the form in the template, compile and add it to a controller that was instantiated separately. This worked, but not really, because the $scope for the form and controller were different and so any update to a field didn't reflect on the controller state.
The way it works in the end is to instantiate the controller with a $scope
ctrl = $controller('Controller as ctrl', {
someDependency: mockDependency,
$scope: $scope
});
and then to compile the partial template (just the form) with the same $scope
var formTpl = angular.element(template).find('form');
form = $compile(formTpl)($scope);
This way, the controller ends up in $scope.ctrl, but because the form is already named name="ctrl.form" it gets inserted into $scope.ctrl.form and it's visible inside a controller.

When using controllerAs, you can access your form in tests like so:
// 1. Create a new scope
var $scope = $rootScope.$new();
// 2. Run the controller
var Controller = $controller("Controller as ctrl", { $scope: $scope });
// 3. Compile the template against our scope
// This will add property $scope.ctrl.form (assuming that form has name="ctrl.form")
$compile(angular.element(templateHtml))($scope);
// 4. Controller.form should now also be defined
expect(Controller.form).toBeDefined();
Meanwhile mocking can be achieved by using angular's $provide:
beforeEach(angular.mock.module(function ($provide) {
function ApiServiceMock() {
this.getName() = function () {
return "Fake Name";
};
}
// Provide our mocked service instead of 'ApiService'
// when our controller's code requests to inject it
$provide.value("ApiService", ApiServiceMock);
}));
Full example (using both - mocking & form compilation with controllerAs):
Main app code:
angular.module("my.module", [])
.service("ApiService", function () {
this.getName = function () {
return "Real Name";
};
})
.controller("Controller", function (ApiService) {
var ctrl = this;
ctrl.someProperty = ApiService.getName();
});
HTML:
<div ng-controller="Controller as ctrl">
<form name="ctrl.form">
<input type="email" name="email" />
</form>
</div>
Test:
describe("Controller", function () {
var $scope,
Controller;
beforeEach(angular.mock.module("my.module"));
beforeEach(angular.mock.module(function ($provide) {
function ApiServiceMock() {
this.getName() = function () {
return "Fake Name";
};
}
$provide.value("ApiService", ApiServiceMock);
}));
beforeEach(inject(function ($rootScope, $controller, $compile) {
$scope = $rootScope.$new();
Controller = $controller("Controller as ctrl", { $scope: $scope });
// FIXME: Use your own method of retrieving template's HTML instead 'getTemplateContents' placeholder
var html = getTemplateContents("./my.template.html"),
formTpl = angular.element(html).find('form');
$compile(formTpl)($scope);
}));
it('should contain someProperty', function () {
expect(Controller.someProperty).toBeDefined();
});
it('form should contain an email field', function () {
expect(Controller.form.email).toBeDefined();
});
});

Related

Angular $scope function replacement

For an old angular app (version 1), I was asked to upload some data via it. I used Selenium to execute a javascript script that replaces the Angular app's $scope upload function to something I can work with.
ie
angular.element(document.querySelector('#somecontroller')).scope().uploadFunc() { ... }
Unfortunately, the new function does not have access to the $scope and various local non $scope functions found within that library.
ie.
...uploadFunc() {
localNonScopeFunc // ERROR: localNonScopeFunc not defined
$scope // ERROR: $scope not defined
}
I was able to get access to $scope indirectly but I still can't access any local functions.
I'm pretty sure I just need to bind the controller's this to function to resolve both issues but not sure how...
How would I bind the replaced $scope function to the angular app?
Update 1:
// existing library
var someApp= angular.module('wApp', ['oc.lazyLoad', 'lookup','menu','prompt','service']);
someApp.controller('somecontroller', function ($scope, $timeout, $interval, $http, $ocLazyLoad, $rootScope, service)
{
$scope.uploadFunc = function() {
$scope.doSomething();
NonScopeLibraryFunc();
...bad blocking code
};
}
function NonScopeLibraryFunc() {
...
}
I have to change the uploadFunc code since its blocking functionality. So I try
// selenium JavaScriptExecutor
angular.element(document.querySelector('#somecontroller')).scope().uploadFunc = function () {
$scope.doSomething(); // Error: $scope not defined
NonScopeLibraryFunc() // Error: NonScopeLibraryFunc not defined
...better non-blocking code
};
Neither $scope or NonScopeLibraryFunc() can be used. I was able to indirectly use $scope but calling NonScopeLibraryFunc is still a no go.
I also tried binding
const s = angular.element(document.querySelector('#somecontroller')).scope();
const newUploadFunc = function () {
$scope.doSomething(); // Error: $scope not defined
NonScopeLibraryFunc() // Error: NonScopeLibraryFunc not defined
...better non-blocking code
}.bind(s);
s.uploadFunc = newUploadFunc;
But it also does work.
Following example of overloading an angular scope function should give you the basics of what you need.
Where you might run into issues is with any arguments that might be passed into the scope function from the view
// Non angular code
document.querySelector('button').addEventListener('click', function() {
const someVar = 'Local var text';
// get angular scope
const angScope = angular.element(document.querySelector('#ang-app')).scope()
console.log('Remote access $scope.txt = ', angScope.txt);
// store reference to original scope function
const oldFunc = angScope.func
// overload original function
angScope.func = function(){
// modify scope variable with local value
angScope.txt = someVar;
// call original scope function
oldFunc();
// if modifying the original scope that needs to be changed in view use $.apply()
angScope.$apply()
}
angScope.func();
});
// Angular app
angular.module('myApp', [])
.controller('main', function($scope) {
$scope.txt = 'Scope text';
$scope.func = function(){
console.log('controller func() called')
$scope.log()
}
$scope.log = function(){
console.log('Scope txt:', $scope.txt);
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.7.5/angular.min.js"></script>
<button>Trigger modified scope func</button>
<div id="ang-app" ng-app="myApp" ng-controller="main">
Angular display: {{txt}}
</div>

Unit Testing Angular 1.5 component that requires ngModel

To test angular 1.5 components, the docs recommend you use ngMock's $componentController instead of using $compile if you don't need to test any of the DOM.
However, my component uses ngModel which I need to pass into the locals for $componentController, but there is no way to programmatically get the ngModelController; the only way to test it is to actually $compile an element with it on it, as this issue is still open: https://github.com/angular/angular.js/issues/7720.
Is there any way to test my components controller without resorting to $compiling it? I also don't want to have to mock the ngModelController myself as its behavior is somewhat extensive and if my tests rely on a fake one rather than the real thing there is a chance newer versions of Angular could break it (though that probably isn't an issue given Angular 1 is being phased out).
tl;dr: Solution is in the third code block.
but there is no way to programmatically get the ngModelController
Not with that attitude. ;)
You can get it programmatically, just a little roundabout. The method of doing so is in the code for ngMock's $componentController service (paraphrased here); use $injector.get('ngModelDirective') to look it up, and the controller function will be attached to it as the controller property:
this.$get = ['$controller','$injector', '$rootScope', function($controller, $injector, $rootScope) {
return function $componentController(componentName, locals, bindings, ident) {
// get all directives associated to the component name
var directives = $injector.get(componentName + 'Directive');
// look for those directives that are components
var candidateDirectives = directives.filter(function(directiveInfo) {
// ...
});
// ...
// get the info of the component
var directiveInfo = candidateDirectives[0];
// create a scope if needed
locals = locals || {};
locals.$scope = locals.$scope || $rootScope.$new(true);
return $controller(directiveInfo.controller, locals, bindings, ident || directiveInfo.controllerAs);
};
}];
Though you need to supply the ngModelController locals for $element and $attrs when you instantiate it. The test spec for ngModel demonstrates exactly how to do this in its beforeEach call:
beforeEach(inject(function($rootScope, $controller) {
var attrs = {name: 'testAlias', ngModel: 'value'};
parentFormCtrl = {
$$setPending: jasmine.createSpy('$$setPending'),
$setValidity: jasmine.createSpy('$setValidity'),
$setDirty: jasmine.createSpy('$setDirty'),
$$clearControlValidity: noop
};
element = jqLite('<form><input></form>');
scope = $rootScope;
ngModelAccessor = jasmine.createSpy('ngModel accessor');
ctrl = $controller(NgModelController, {
$scope: scope,
$element: element.find('input'),
$attrs: attrs
});
//Assign the mocked parentFormCtrl to the model controller
ctrl.$$parentForm = parentFormCtrl;
}));
So, adapting that to what we need, we get a spec like this:
describe('Unit: myComponent', function () {
var $componentController,
$controller,
$injector,
$rootScope;
beforeEach(inject(function (_$componentController_, _$controller_, _$injector_, _$rootScope_) {
$componentController = _$componentController_;
$controller = _$controller_;
$injector = _$injector_;
$rootScope = _$rootScope_;
}));
it('should update its ngModel value accordingly', function () {
var ngModelController,
locals
ngModelInstance,
$ctrl;
locals = {
$scope: $rootScope.$new(),
//think this could be any element, honestly, but matching the component looks better
$element: angular.element('<my-component></my-component>'),
//the value of $attrs.ngModel is exactly what you'd put for ng-model in a template
$attrs: { ngModel: 'value' }
};
locals.$scope.value = null; //this is what'd get passed to ng-model in templates
ngModelController = $injector.get('ngModelDirective')[0].controller;
ngModelInstance = $controller(ngModelController, locals);
$ctrl = $componentController('myComponent', null, { ngModel: ngModelInstance });
$ctrl.doAThingToUpdateTheModel();
scope.$digest();
//Check against both the scope value and the $modelValue, use toBe and toEqual as needed.
expect(ngModelInstance.$modelValue).toBe('some expected value goes here');
expect(locals.$scope.value).toBe('some expected value goes here');
});
});
ADDENDUM: You can also simplify it further by instead injecting ngModelDirective in the beforeEach and setting a var in the describe block to contain the controller function, like you do with services like $controller.
describe('...', function () {
var ngModelController;
beforeEach(inject(function(_ngModelDirective_) {
ngModelController = _ngModelDirective_[0].controller;
}));
});

Angular: Using "this" instead of " $scope" in "Controller As"

I want to use "this" instead of $scope because I have a controller structure that doesn't initialize $scope:
.controller('MyCtrl', function($scope) {
...
});
I must follow a tutorial with the structure above and put the $scope service to make it work.
Controller
(function() {
'use strict';
angular
.module('example.cancha')
.controller('CanchaController', CanchaController);
CanchaController.$inject = ['$state','$scope', 'canchaService'];
function CanchaController($state, $scope, canchaService) {
var vm = angular.extend(this, {
canchasComplejo: []
});
(function activate() {
cargarCanchasComplejo();
})();
//funcion que llama al servicio para obtener las canchas del complejo
function cargarCanchasComplejo() {
canchaService.obtenerCanchasComplejo()
.then(function(canchasComplejo) {
vm.canchasComplejo = canchasComplejo;
$scope.groups = [];
for (var i=0; i<canchasComplejo.length; i++) {
$scope.groups[i] = {
name: 'Cancha N°'+canchasComplejo[i].nroCancha+' ('+canchasComplejo[i].tipoCancha+')',
items: ['Información','Habilitada','Ver Agenda'],
show: false
}
};
$scope.toggleGroup = function(group) {
group.show = !group.show;
};
$scope.isGroupShown = function(group) {
return group.show;
};
});
}
}
})();
As you can see, this is not the regular structure. Can I avoid using $scope using "this"? Thanks!
remove $scope injection from your controller:
CanchaController.$inject = ['$state','$scope', 'canchaService'];
function CanchaController($state, $scope, canchaService) {
.....
should be:
CanchaController.$inject = ['$state', 'canchaService'];
function CanchaController($state, canchaService) {
then replace all $scope occurrences within controller scope with vm:
$scope.groups = []; becomes vm.groups = [];
now be aware that in the HTML using this controller you cannot access groups directly, but you should use ng-controller="CanchaController as vm" (free fill to use any name instead of vm) and access groups with vm.groups. Your HTML might look like:
<div ng-controller="CanchaController as vm">
<pre>{{ vm.groups | json }}</pre>
</div>
if this controller is used with routing or directive, then controllerAs: 'vm' should be set on JS level and not HTML
Sure you can use so called vm pattern from John Papa. Check the article here https://daveceddia.com/convert-scope-to-controlleras/
You should use the ControllerAs syntax instead of injecting the $scope into the function. This is the recommended way of settings up your controller ,see John Papa's style guide here. Specifically, look for rule [Style Y031].
Another option that you have, if you are using Angular 1.5.x, is to use Components instead of Controllers. When you declare the Component, you will specify the markup associated with it and that is all you have to do. The Component uses the ControllerAs syntax by default and it uses the alias $ctrl to reference the scope.
You can use "this" but it will always point on the function that triggers it. That will give you restrictions on the future or errors.
It's 100% recommended to use $scope in angular so you can have a standard format on each function to avoid problems

How to force view updates in angularjs by services?

I'm trying to hold a global MainCtrl controller that serves the navigation menus. From time to time these menu items should be updated by various controllers.
Now I thought I might just bind the navigation links to the controller, and update the controller variable as follows:
<div ng-controller="MainCtrl">
<li ng-repeat="nav in navigations">
{{nav.label}}
</li>
</div>
<div ng-view></div> <!-- renders different controllers, eg testController.js -->
app.controller('MainCtrl', ['$scope', 'navigationService', function($scope, navigationService) {
//binding the property of the service
$scope.navigations = navigationService.navigations;
}]);
app.service('navigationService', function() {
return {
navigations: null
};
});
But, when calling the service and updating the navigations variable inside, nothing is changed in the view. Why?
angular.module('test').controller('testController', ['$scope', '$http', 'navigationService', function($scope, $http, navigationService) {
$http.get(url)
.success(function(data) {
navigationService.navigations = data.some.navigations; //assume json data exists
});
}]);
How can I achieve this two-way databinding, forcing a view update from one controller to another?
You are returning a primitive from service. A primitive doesn't have inheritance.
Return an object instead:
app.service('navigationService', function() {
var nav ={}; // object that will be returned
function init(){
$http.get(url)
.success(function(data) {
// modify object returned from service rather than reassign a primitive value
nav.items = data.some.navigations; exists
});
}
init();//make request to load the data
return { // can add more properties if needed
nav: nav
};
});
Then in controller:
$scope.navigations = navigationService.nav;
// will initially be {} and later will inherit items property
In view
<div ng-repeat="item in navigations.items">
angular internal watches will pick up the changes now made to the object and render view accordingly
After using Angular for more than 2 years, I discovered, whenever you want that functionality with multiple binding from different services/controllers/directives, ALWAYS use json property, and NEVER ovverride variable instance:
I would replace that:
$scope.navigations = navigationService.navigations;
with that:
var state = {
navigations: []
};
$scope.state = state;
state.navigations = navigationService.navigations; // i prefer such syntax
// or
$scope.state.navigations = navigationService.navigations;
Why? Probably because of Angular automatic $watch()/$watchCollection() functions, which are bind to variable changes.
You need to use the $rootscope and broadcast to and keep eye on broadcast
Say your data is changed from x controller, so here you can broadcast like this
$rootScope.$broadcast('menu-need-to-refresh');
In your main controller keep eye, like this
$scope.$on('menu-need-to-refresh', function (event, data) {
$scope.menus= service.newMenu();
});
Hope it will help you
I solved a similar problem simply by using $scope.$watch
ex:
$scope.$watch(
function(){return navigationService.navigations;},
function(newVal, oldVal){$scope.navigations = newVal;}
)
this code is not tested, but you get the gist
Update : #charlietfl + #Dmitri Algazin method is more elegant as it takes advantage of javascript itself, by using references, and avoid using two watchers in controller + ngRepeat (watchCollection in this directive will do the work).
My original answer :
You should watch for changes in the service from MainCtrl using $scope.$watch :
app.controller('MainCtrl', ['$scope', 'navigationService', function($scope, navigationService) {
//binding the property of the service
$scope.$watch(
function(){
return navigationService.navigations;
}
, function(newValue, oldValue){
if(newValue !== oldValue){
$scope.navigations = newValue;
}
})
}]);

Testing angular controller: when to create new scope?

When testing an angular controller, is it not always necessary to create a new scope by calling $rootScope.new()?
Here is my controller:
myControllers.controller("myCtrl1", ['$scope', function($scope) {
$scope.todos = [{"name": "Learn Angular"}, {"name": "Install Karma"}];
$scope.date = '1/1/2014';
}]);
And here is my passing test:
describe("controllers", function() {
var $scope, $rootScope, $controller;
beforeEach(function() {
module("myApp.controllers");
});
beforeEach(inject(function(_$controller_) {
$controller = _$controller_;
//scope = $rootScope.new() **When would you do this?**
}));
//Basic Controller
it("sets todos on scope", function() {
var scope = {}; //**Creating an empty scope object**
$controller("myCtrl1", {$scope : scope});
expect(scope.todos.length).toBe(3);
});
});
I was under the assumption that I need to create a new clean scope every time I test a controller but apparently I was wrong as the above test passes. Any explanations?
Thanks in advance!
In your case it is passing because your controller is only assigning values to the scope, so it can be any object. But for example if the controller had to listen to an event then the test would fail.
$scope.$on('datachange', function(event, args) {
// do something
})
In that case you would have to create a new scope to make your test pass.
It's because you're not using any $scope's methods, like $on or $watch or whatever. Also, to trigger watches in a test, you quite often need to use $scope.$digest(). None of those will work if you pass an empty object as a scope to a controller, of course.
Depends on situation.

Categories