Testing the controller passed to an Angular Material Dialog instance - javascript

First off, I am trying to unit test the controller that is being passed to an Angular Material Dialog instance.
As a general question, does it make more sense to test such a controller separately, or by actually invoking$mdDialog.show()?
I am attempting the first method, but I'm running into some issues, mostly related to how Angular Material binds the "locals" to the controller.
Here is the code that I am using to invoke the dialog in my source code, which works as expected:
$mdDialog.show({
controller: 'DeviceDetailController',
controllerAs: 'vm',
locals: {deviceId: "123"},
bindToController: true,
templateUrl: 'admin/views/deviceDetail.html',
parent: angular.element(document.body),
targetEvent: event
});
I don't believe the docs have been updated, but as of version 0.9.0 or so, the locals are available to the controller at the time the constructor function is called (see this issue on Github). Here is a stripped-down version of the controller constructor function under test, so you can see why I need the variable to be passed in and available when the controller is "instantiated":
function DeviceDetailController(devicesService) {
var vm = this;
vm.device = {};
// vm.deviceId = null; //this field is injected when the dialog is created, if there is one. For some reason I can't pre-assign it to null.
activate();
//////////
function activate() {
if (vm.deviceId != null) {
loadDevice();
}
}
function loadDevice() {
devicesService.getDeviceById(vm.deviceId)
.then(function(data) {
vm.device = data.collection;
};
}
}
I am trying to test that the device is assigned to vm.device when a deviceId is passed in to the constructor function before it is invoked.
The test (jasmine and sinon, run by karma):
describe('DeviceDetailController', function() {
var $controllerConstructor, scope, mockDevicesService;
beforeEach(module("admin"));
beforeEach(inject(function ($controller, $rootScope) {
mockDevicesService = sinon.stub({
getDeviceById: function () {}
});
$controllerConstructor = $controller;
scope = $rootScope.$new();
}));
it('should get a device from devicesService if passed a deviceId', function() {
var mockDeviceId = 3;
var mockDevice = {onlyIWouldHaveThis: true};
var mockDeviceResponse = {collection: [mockDevice]};
var mockDevicePromise = {
then: function (cb) {
cb(mockDeviceResponse);
}
};
var mockLocals = {deviceId: mockDeviceId, $scope: scope};
mockDevicesService.getDeviceById.returns(mockDevicePromise);
var ctrlConstructor = $controllerConstructor('DeviceDetailController as vm', mockLocals, true);
angular.extend(ctrlConstructor.instance, mockLocals);
ctrlConstructor();
expect(scope.vm.deviceId).toBe(mockDeviceId);
expect(scope.vm.device).toEqual(mockDevice);
});
});
When I run this, the first assertion passes and the second one fails ("Expected Object({ }) to equal Object({ onlyIWouldHaveThis: true })."), which shows me that deviceId is being injected into the controller's scope, but apparently not in time for the if clause in the activate() method to see it.
You will notice that I am trying to mimic the basic procedure that Angular Material uses by calling $controller() with the third argument set to 'true', which causes $controller() to return the controller constructor function, as opposed to the resulting controller. I should then be able to extend the constructor with my local variables (just as Angular Material does in the code linked to above), and then invoke the constructor function to instantiate the controller.
I have tried a number of things, including passing an isolate scope to the controller by calling $rootScope.$new(true), to no effect (I actually can't say I fully understand isolate scope, but $mdDialog uses it by default).
Any help is appreciated!

The first thing I would try would be to lose the 'as vm' from your call to $controller. You can just use the return value for your expect rather than testing scope.
Try this:
var ctrlConstructor = $controllerConstructor('DeviceDetailController', mockLocals, true);
angular.extend(ctrlConstructor.instance, mockLocals);
var vm = ctrlConstructor();
expect(vm.deviceId).toBe(mockDeviceId);
expect(vm.device).toEqual(mockDevice);

Related

Pass object to Angular directive's '&' parent scope function

How might one go about passing an object to Angular's (Angular 1.4.8) & ampersand scope binding directive?
I understand from the docs that there is a key-destructuring of sorts that needs named params in the callback function, and the parent scope uses these names as args. This SO answer gives a helpful example of the expected & functionality. I can get this to work when explicitly naming the params on the parent controller function call.
However, I am using the & to execute actions via a factory. The parent controller knows nothing of the params and simply hands the callback params to a dataFactory, which needs varied keys / values based on the action.
Once the promise resolves on the factory, the parent scope updates with the returned data.
As such, I need an object with n number of key / value pairs, rather than named parameters, as it will vary based on each configured action. Is this possible?
The closest I have seen is to inject $parse into the link function, which does not answer my question but is the sort of work-around that I am looking for. This unanswered question sounds exactly like what I need.
Also, I am trying to avoid encoding/decoding JSON, and I would like to avoid broadcast as well if possible. Code stripped down for brevity. Thanks...
Relevant Child Directive Code
function featureAction(){
return {
scope: true,
bindToController: {
actionConfig: "=",
actionName: "=",
callAction: "&"
},
restrict: 'EA',
controllerAs: "vm",
link: updateButtonParams,
controller: FeatureActionController
};
}
Child handler on the DOM
/***** navItem is from an ng-repeat,
which is where the variable configuration params come from *****/
ng-click="vm.takeAction(navItem)"
Relevant Child Controller
function FeatureActionController(modalService){
var vm = this;
vm.takeAction = takeAction;
function _callAction(params){
var obj = params || {};
vm.callAction({params: obj}); // BROKEN HERE --> TRYING
//TO SEND OBJ PARAMS
}
function executeOnUserConfirmation(func, config){
return vm.userConfirmation().result.then(function(response){ func(response, config); }, logDismissal);
}
function generateTasks(resp, params){
params.example_param_1 = vm.add_example_param_to_decorate_here;
_callAction(params);
}
function takeAction(params){
var func = generateTasks;
executeOnUserConfirmation(func, params);
}
Relevent Parent Controller
function callAction(params){
// logs undefined -- works if I switch to naming params as strings
console.log("INCOMING PARAMS FROM CHILD CONTROLLER", params)
executeAction(params);
}
function executeAction(params){
dataService.executeAction(params).then(function(data){
updateRecordsDisplay(data); });
}
I think the example below should give you enough of a start to figure out your question:
<!DOCTYPE html>
<html ng-app="myApp">
<head>
<meta charset="utf-8">
<title>Angular Callback</title>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script>
var myApp = angular.module("myApp", []);
myApp.controller('appController', function($scope) {
$scope.var1 = 1;
$scope.handleAction1 = function(params) {
console.log('handleAction1 ------------------------------');
console.log('params', params);
}
$scope.handleAction2 = function(params, val1) {
console.log('handleAction2 ------------------------------');
console.log('params', params);
console.log('val1', val1);
}
});
myApp.controller('innerController', innerController);
innerController.$inject = ['$scope'];
function innerController($scope) {
$scope.doSomething = doSomething;
function doSomething() {
console.log('doSomething()');
var obj = {a:1,b:2,c:3}; // <-- Build your params here
$scope.callAction({val1: 1, params: obj});
}
}
myApp.directive('inner', innerDirective );
function innerDirective() {
return {
'restrict': 'E',
'template': '{{label}}: <button ng-click="doSomething()">Do Something</button><br/>',
'controller': 'innerController',
'scope': {
callAction: '&',
label: '#'
}
};
}
</script>
</head>
<body ng-controller="appController">
<inner label="One Param" call-action="handleAction1(params)"></inner>
<inner label="Two Params" call-action="handleAction2(params, val)"></inner>
</body>
</html>
In the appController I have two functions that will be called by the inner directive. The directive is expecting the outer controller to pass in those functions using the call-action attribute on the <inner> tag.
When you click on the button within the inner directive it called the function $scope.doSomething This, in turn calls to the outer controller function handleAction1 or handleAction2. It also passes a set of parameters val1 and params:
$scope.callAction({val1: 1, params: obj});
In your template you specify which of those parameters you want to be passed into your outer controller function:
call-action="handleAction1(params)"
or
call-action="handleAction2(params, val)"
Angular then uses those parameter names to look into the object you sent when you called $scope.callAction.
If you need other parameters passed into the outer controller function then just add then into the object defined in the call to $scope.callAction. In your case you would want to put more content into the object you pass in:
var obj = {a:1,b:2,c:3}; // <-- Build your params here
Make that fit your need and then in your outer controller you would take in params and it would be a copy of the object defined just above this paragraph.
It this is not what you were asking, let me know.

How do I reach $rootScope from a component?

I am very new to AngularJS/Ionic/Cordova programming and am trying to handle the visibility of a component using a global variable, so it can be hidden or shown from other components. I am creating the variable when calling the run function, assigning it to $rootScope.
app.run(function($rootScope, $ionicPlatform) {
$ionicPlatform.ready(function() {
// Some Ionic/Cordova stuff...
// My global variable.
$rootScope.visible = true;
});
})
My component is:
function MyComponentController($rootScope, $scope) {
var self = this;
self.visible = $rootScope.visible;
alert(self.visible);
}
angular.module('myapp')
.component('myComponent', {
templateUrl: 'my-component.template.html',
controller: MyComponentController
});
And the template:
<div ng-if="$ctrl.visible">
<!-- ... -->
</div>
However the alert message always shows "undefined". What am I missing?
$rootScope.visible isn't watched when being assigned as self.visible = $rootScope.visible. And it is undefined at the moment when component controller is instantiated.
It can be
function MyComponentController($rootScope, $scope) {
var self = this;
$scope.$watch(function () { return $rootScope.visible }, function (val) {
self.visible = val;
});
}
By the way, it is likely available as $scope.$parent.visible and can be bound in template as ng-if="$parent.visible", but this is antipattern that is strongly discouraged.
There may be better approaches:
top-level AppController and <my-component ng-if="visible">, so the component doesn't have to control its own visibility
broadcasting it with scope events, $rootScope.$broadcast('visibility:myComponent')
using a service as event bus (that's where RxJS may be helpful)
using a router to control the visibility of views, possibly with route/state resolver (this is the best way)
How about change self to $scope like this:
function MyComponentController($rootScope, $scope) {
$scope.visible = $rootScope.visible;
alert($scope.visible);
}

Unit Testing a Controller with specific syntax

Whenever, I am testing a controller and have something like this in it.
$scope.isSomething = function (Item) {
return ItemCollection.someItem(Item.attachedItem);
};
giving error on karma console:
TypeError: undefined is not an object (evaluating 'Item.attachedItem')
I am simply calling the function from the test file like this:
scope.isSomething();
I need to mock the Item.attachedItem or I am missing something here.. Please Explain in details as this is happening in multiple files.. thanks in advance
Also, for this type of code
.controller('itemCtrl', function (itemCollection) {
var vm = this;
this.itemCollection= itemCollection;
itemCollection.someItem().then(function (Item) {
vm.pageUrl = Item.pageUrl;
vm.Item= Item.someItems;
});
});
Also, this is also part of the code for more broad view here it gives Item.pageUrl is not a object error
Refer angular unit testing docs
The ItemCollection being a service, you could inject a mock while initialising a controller using
var ItemCollection, ItemCrtl;
beforeEach(inject(function($controller, $rootScope) {
$scope = $rootScope.$new();
ItemCollection = jasmine.createSpyObj('ItemCollection', ['someItem']);
ItemCrtl = $controller('ItemCtrl', {
$scope: scope,
ItemCollection: ItemCollection
});
});
For Item, the method isSomething should take care of checking if Item is undefined before doing Item.attachedItem
Testing an aync block is tricky. someItem returns a promise. $q an angular service to which can be used create async functions while testing.
We need to resolve the deferred object to test the async task.
var ItemCollection, ItemCrtl, deferedObj;
beforeEach(inject(function($controller, $rootScope, $q) {
$scope = $rootScope.$new();
deferedObj = $q.defer();
ItemCollection = jasmine.createSpyObj('ItemCollection', ['someItem']);
ItemCollection.someItem.andReturn(deferedObj.promise);
ItemCtrl = $controller('ItemCtrl', {
$scope: scope,
ItemCollection: ItemCollection
});
});
it('sets page url', function() {
deferedObj.resolve({ pageUrl: 'http://url', someItems: [1,2,3] });
scope.$apply();
expect(ItemCtrl.pageUrl).toEqual('http://url');
});
you have to use mock Item data in test like this (assuming attachedItem value is boolean)
var item={attachedItem:true}
scope.isSomething(item)
$scope.isSomething = function (Item) {
if(!Item.attachedItem){
Item.attachedItem=YOUR_MOCK_VALUE;
}
return ItemCollection.someItem(Item.attachedItem);
};

The $scope variable is undefined unless I forced it to retrieve from service again

Here is the code snippet about an Angular Controller. I'm trying to learn Angular from this github project
The questionable part is located in function addStock. I have already defined $scope.watchlist in the initilizations part, however, if I remove the re-declaration inside $scope.addStock function, $scope.watchlist will not be populated with right values. Can any Angular experts point me out?
If you want to see my full project code - here is the link.
angular.module('stockCatApp')
.controller('WatchlistCtrl', function ($scope, $routeParams, $modal, WatchlistService, CompanyService) {
// Initializations
$scope.companies = CompanyService.query();
$scope.watchlist = WatchlistService.query($routeParams.listId);
$scope.stocks = $scope.watchlist.stocks;
$scope.newStock = {};
var addStockModal = $modal({
scope: $scope,
template: 'views/templates/addstock-modal.html',
show: false
});
$scope.showStockModal = function () {
addStockModal.$promise.then(addStockModal.show);
};
$scope.addStock = function () {
//The following line needs to be put here again in order to retrieve the right value
$scope.watchlist = WatchlistService.query($routeParams.listId);
///////////////////////////////////////////////////////////////////////
$scope.watchlist.addStock({
listId: $routeParams.listId,
company: $scope.newStock.company,
shares: $scope.newStock.shares
});
addStockModal.hide();
$scope.newStock = {};
};
});
Can you do a console.log($scope.watchlist); after both of your $scope binds to see if the data and its type match?
$scope.watchlist = WatchlistService.query($routeParams.listId);
console.log($scope.watchlist);
$scope.watchlist.addStock({
listId: $routeParams.listId,
company: $scope.newStock.company,
shares: $scope.newStock.shares
});
console.log($scope.watchlist);
Feel free to post the logs.
It appears as if you are calling $scope.addStock() from a modal. In which case the modal does not necessarily inherit the parent scope. It depends on which versions of everything you are using. It looks like you are using angular-strap (I looked in your bower.json). If you look in their $modal source code you will find this:
https://github.com/mgcrea/angular-strap/blob/master/src/modal/modal.js
var scope = $modal.$scope = options.scope && options.scope.$new() || $rootScope.$new();
So, when you are opening your modal you are creating a new scope. You might be able to access the original scope using $parent though.

Angular Parent Scope Variable not changing

I have a parent controller where I set instantiate an object called links. I assign a property with a value that I want to change within another function. However when I set the variable in the instagramModel the links.imagesa doesn't get updated.
I print the value out in the console and the parentscope doesn't get updated. I have thought I followed the rules of prototypical inheritance.
Why is $scope.links.imagesa not updating?
.controller('HomeCtrl', function HomeController($scope, titleService, config, $sails, $timeout, $upload, leafletData, $modal, $log) {
$scope.links = {};
$scope.links.imagesa = "This should change";
$scope.instagramModal = function (size) {
var modalInstance = $modal.open({
templateUrl: 'instagramModal.html',
controller: 'InstagramModalInstanceCtrl',
size: size,
resolve: {
items: function () {
return $sails.get("/instagram/self").success(function (response) {
return response.data;
}).error(function (response) {
console.log('error');
});
}
}
});
modalInstance.result.then(function (selectedItem) {
$scope.links.imagesa = "wept";
}, function () {
$log.info('Modal dismissed at: ' + new Date());
});
};
$scope.ask = function () {
console.log($scope.links.imagesa);
};
});
If you want the parent's scope to get updated, then you must use $scope.$parent.links.imagesa since the changes made in child scope are not reflected in the parent scope directly.
I had the HomeCtrl instantiated in the UI Router and also on the template page using ng-controller. This messed up the scope.
Angular UI's modals use $rootScope by default. See documentation at "http://angular-ui.github.io/bootstrap/#/modal"
You can pass a scope parameter with a custom scope when you open the modal – e.g. scope: $scope if you want to pass the parent scope. The modal controller will create a sub-scope from that scope, so you will only be able to use it for your initial values.
Hence, if you want to update any value, keep the object/data in rootScope.

Categories

Resources