How do I unit test $scope.broadcast, $scope.$on using Jasmine - javascript

I'm newbie to AngularJs/NodeJs world, so forgive if this is a basic question to some.
So in a nutshell I've two controllers, the first controller $broadcast an 'Id' and the second controller fetches that Id with $on and then passes that Id to an intermediate service, which makes an $http ajax call and returns a single Book object.
How do I unit test $scope.broadcast, $scope.$on using Jasmine
firstCtrl
.controller('firstCtrl', function($scope, ...){
$scope.selectGridRow = function() {
if($scope.selectedRows[0].total !=0)
$scope.$broadcast('id', $scope.selectedRows[0].id);//Just single plain ID
};
});
secondCtrl
.controller('secondCtrl',
function($scope, bookService) {
$scope.$on('id', function(event, id) {
bookService.getBookDetail(id).then(function(d) {
$scope.book = d.book;
});
});
});
expected Json obj
var arr = "book" : [ {
"id" : "1",
"name" : "Tomcat",
"edition" : "9.1"
}
]
Let me know if anyone wants me to post the $http service that's used by the second controller.
expected behavior
So from the top of my head, ideally, I would like to test every possible scenario, but something like below, which can then expend:
expect(scope.book).toEqual(arr);
expect(scope.book).not.toEqual(undefined);
Thanks everyone!

First you should do the broadcast on $rootScope then you can receive on $scope.
Now to the testing. I assume you want to include real request to your API via bookService and $http. This can be mocked but I'll focus on the real call. Let me know if you need the mocked one.
Before the actual test, you will need to do some injections/instantiations:
Initialize your app
Inject $controller, $rootScope, $httpBackend and bookService
Create scopes for firstController and SecondController and store it in a variable
Store bookService and $httpBackend in variables
Instantiate the controllers and store them
Then in the actual test you must tell $httpBackend what to do when it caches request for the books (or books). Construct $httpBackend.whenGET("/api/books/1").passThrough(); will pass request with url "/api/books/1" to the server.
Next your must setup property selectedRows on firstScope so it fulfills the condition in function selectGridRow in your firstCtrl.
Now you can call function selectGridRow to trigger the broadcast and API call. But you must wrap it in runs function so Jasmine recognizes this as an async call and will wait for it to finish. The 'waiting' is defined in waitsFor call. It will wait until it gets a book and it waits max 5000 ms then the test will be marked as failed.
Last step is to check expected result. We don't have to check for undefined anymore as the test would not get to here anyway. The check must be wrapped again runs call so it is executed afters successful 'waitsFor'.
Here is the full code:
describe("Broadcast between controllers", function () {
beforeEach(module('app')); //app initialization
var firstScope;
var secondScope;
var bookService;
var $httpBackend;
var firstController;
var secondController;
beforeEach(inject(function ($controller, $rootScope, _bookService_, _$httpBackend_) {
firstScope = $rootScope.$new();
secondScope = $rootScope.$new();
bookService = _bookService_;
$httpBackend = _$httpBackend_;
firstController = $controller('firstCtrl', { $scope: firstScope });
secondController = $controller('secondCtrl', { $scope: firstScope, bookService: bookService });
}));
it("should work", function () {
$httpBackend.whenGET("/api/books/1").passThrough();
firstScope.selectedRows = [{ id: 1, total: 1000 }];
secondScope.book = null;
runs(function () {
firstScope.selectGridRow();
});
waitsFor(function () {
return secondScope.book != null;
}, "Data not received in expected time", 5000);
runs(function () {
expect(secondScope.book[0].id).toEqual(1);
});
});
});

Related

Angular Test does not call a promise callback

Environment:
Angular 1.5.8
Unit Tests with Karma/Jasmine
This is my controller, which aims to get a value from $stateParams and uses that, to perform a DELETE request, later on.
Right now it should ask the user, wether to delete the object.
This is done with a sweetalert.
I have removed ngdoc comments and arbitrary SWAL config.
ClientDeleteController.js:
angular.module('app.data').controller('ClientDeleteController', [
'$stateParams', '$q',
function ($stateParams, $q) {
'use strict';
var vm = this;
vm.clientId = $stateParams.id;
vm.promptDeferred = null;
vm.prompt = function () {
// create promise
var d = $q.defer();
vm.promptDeferred = d;
// create prompt
swal({ .... }, vm.swalCallback);
};
vm.swalCallback = function (confirmed) {
if (confirmed) {
console.info('resolving...');
vm.promptDeferred.resolve();
} else {
console.info('rejecting...');
vm.promptDeferred.reject();
}
};
vm.delete = function () {
vm.prompt();
vm.promptDeferred.promise.then(vm.performDelete);
};
vm.performDelete = function () {
console.info('performing');
};
}]);
This is the test suite:
ClientDeletecontrollerSpec.js
describe('Controller:ClientDeleteController', function () {
var controller
, $httpBackend
, $rootScope
, $controller
, scope
, $q
, $stateParams = {id: 1}
, resolvePromise = true
;
swal = function (options, callback) {
console.info('i am swal, this is callback', callback+'', resolvePromise);
callback(resolvePromise);
};
beforeEach(function () {
module('app');
module('app.data');
module('ui.bootstrap');
module(function ($provide) {
$provide.service('$stateParams', function () {
return $stateParams;
});
});
inject(function (_$controller_, _$rootScope_, _$q_) {
$controller = _$controller_;
$rootScope = _$rootScope_;
$q = _$q_;
scope = $rootScope.$new();
controller = $controller('ClientDeleteController', {$scope: scope, $q: $q});
});
});
describe('basic controller features', function () {
it('should be defined', function () {
expect(controller).toBeDefined();
});
it('should get the client id from the state params', function () {
expect(controller.clientId).toBeDefined();
expect(controller.clientId).toEqual($stateParams.id);
});
});
fdescribe('client delete process', function () {
it('should ask the user if he really wants to delete the client', function () {
spyOn(controller, 'prompt').and.callThrough();
controller.delete();
expect(controller.prompt).toHaveBeenCalled();
});
it('should create a promise', function () {
controller.prompt();
expect(controller.promptDeferred).toBeDefined();
});
it('should delete when the user clicked yes', function () {
spyOn(controller, 'performDelete').and.callThrough();
controller.delete();
expect(controller.performDelete).toHaveBeenCalled();
});
it('should not delete when the user clicked no', function () {
spyOn(controller, 'performDelete').and.callThrough();
resolvePromise = false;
controller.delete();
expect(controller.performDelete).not.toHaveBeenCalled();
});
});
});
The test should delete when the user clicked yes fails, the test should not delete when the user clicked noreturns a false positive.
The console.info(...) within the swal mock logs the correct callback function. The logs in the function itself are also logged, which tells me, the callback is fired.
Since in the next line, I call either vm.promptDeferred.resolve() resp. .reject(), I expect the call to actually happen.
Nonetheless, the test result is Expected spy performDelete to have been called..
I have mocked swal in another test the same way and it works fine.
I don't get why the promise won't be resolved.
Notice: When I don't store the promise directly on the controller but return it from prompt() and use the regular .prompt().then(...), it won't work either.
The logs are the same and I like to split functions as much as possible, so it is easier to understand, easier to test and to document.
There are hundreds of other tests in this application but I can't see, why this one won't work as expected.
Thank you for any insight.
What happens if you do $rootScope.$digest() just after invoking delete in the test?
In my experience something like that is necessary to make angular resolve promises during tests (either that or $rootScope.$apply() or $httpBackend.flush())

angular 1.5 access data in http.get without scope

I am using Angular 1.5.
I can't access my data, from the http.get, out the http.get.
Let me explain:
I have my component:
(function(){
'use strict';
class myComponent {
constructor(
$http,
$scope)
{
var self = this;
self.test="this is a test";
$http.get(MYAPI).then(function(response){
self.MYDATA = response.data;
console.log(self.MYDATA)
});
console.log(self.test)
console.log(self.MYDATA)
}
}
angular.module('myApp')
.component('myApp.test', {
templateUrl: 'myTemplate.html',
controller: myComponent,
controllerAs:'vm',
});
})();
The console.Log give me:
this is a test --> for the test
undefined --> out the http.get
Object {id: 1…} --> in the http.get
So I can't access to my data out the http.get and this is what I want.
$http.get is an asynchronous call meaning the program execution doesn't wait for the data to be fetched from server .
Thus by the time console.log(self.MYDATA) located outside of $http.get() is executed , the data has not been fetched from server that is why you get a undefined error.
To solve this problem or to handle asynchronous calls , you could do something like this :
var promise = $http.get(MYAPI);
then access data in the following manner with the help of callbacks :
promise.then(
function successCallBack(response)
{
self.MYDATA = response.data;
console.log(self.MYDATA)
}
).then(
function errorCallBack(response)
{
console.log(response);
}
);
Here is a nice article on callbacks that could help you !

Angular Meteor $reactive vs getReactively

I'm learning Angular Meteor and I have a question:
What is the difference between using $reactive and getReactively?
If you take a look at the API reference you get this for $reactive (http://www.angular-meteor.com/api/1.3.2/reactive):
A service that takes care of the reactivity of your Meteor data, and updates your AngularJS code.
This service wraps context (can be used with this or $scope) - so you can use it with any context as you wish
And this for getReactively (http://www.angular-meteor.com/api/1.3.2/get-reactively):
Use this method to get a context variable and watch it reactively, so each change of this variable causes the dependents (autorun, helper functions and subscriptions) to run again.
The getReactively method is part of the ReactiveContext, and available on every context and $scope.
As far as I could understand $reactive will make everything reactive ('this', '$scope' and son on as long as you apply it to them) and getReactively will make only that particular variable, or object reactive.
So if I make this:
controller: function ($scope, $reactive) {
var vm = $reactive(this).attach($scope);
vm.sort = {
name: 1
};
this.helpers({
parties: () => {
return Parties.find({}, { sort : vm.sort });
}
});
this.subscribe('parties', () => {
return [{
sort: vm.sort
},
this.getReactively('searchText')
]
});
});
Why don't I get the same result as if I was doing this:
controller: function ($scope, $reactive) {
var vm = $reactive(this).attach($scope);
vm.sort = {
name: 1
};
this.helpers({
parties: () => {
return Parties.find({}, { sort : this.getReactively('sort') });
}
});
this.subscribe('parties', () => {
return [{
sort: this.getReactively('sort')
},
this.getReactively('searchText')
]
});
});
If $reactive takes care of reactivity I was expecting to see anything inside this and $scope to be reactive and, like in getReactively, whenever something is changed, to cause its dependents to run again.
So: what am I missing?

angularJS services - return promise for retrieving data AND object for managing data?

I have a question regarding angularJS services.
From what I have read, there are two ways of using services.
[1] Have a service return a promise to return data. If you use this method, in your routeProvider, you can make sure Angular resolves this promise to return data BEFORE it loads the page.
e.g.
App.factory('BooksService', function($q, $http) {
var deferred = $q.defer();
$http.get('/rest/books').then(function(data) {
deferred.resolve(data);
}, function(err) {
deferred.reject(data);
});
return deferred.promise;
};
Then, in my route provider:
...
$routeProvider.when('/books', {
controller : 'BooksCtrl',
templateUrl: '/partials/books.html',
resolve: {
books: 'BooksService'
}
});
...
Then, in my controller:
App.controller('AddPaypalAccountCtrl', function($scope, BooksService) {
$scope.books = BooksService;
}
[2] Have a service return an object that contains functions and data.
e.g.
App.factory('BooksService', function($q, $http) {
var books = [];
var service = {
getBooks : function() {
return books;
},
addBook: function(book) {
books.push(book);
}
};
return service;
};
My question: Is it possible to get the best of both worlds and have a service return a promise that when resolves returns an object that contains functions and data?
I want the $http call to get the books to be resolved before I load the '/books' page, BUT I also want access to a service that can manage said books. Of course I can write two separate services, but I wonder if it's more efficient to keep them both in the same service and write a service that kills two birds with one stone like so:
Here's an example of my factory that returns a promise for retrieving the books.
App.factory('BooksService', function($q, $http) {
var books = [];
var service = {
getBooks: function() {
return books;
},
addBook: function(book) {
books.push(book);
}
}
var deferred = $q.defer();
$http.get('/books').then(function(data) {
books = data.data;
deferred.resolve(service);
, function(err){
deferred.reject(err);
});
return service;
};
Then, as per before, my route provider is as follows, requiring that books be retrieved before I go to the /books page:
...
$routeProvider.when('/books', {
controller : 'BooksCtrl',
templateUrl: '/partials/books.html',
resolve: {
books: 'BooksService'
}
});
...
Then, in my controller, I will attach books to the scope like so.
App.controller('AddPaypalAccountCtrl', function($scope, BooksService) {
$scope.books = BooksService.getBooks();
}
I haven't seen anyone do this yet, so I'm wondering if this is OK.
I feel you are trying to break the SRP - Single Responsibility Principle.
What is the Responsibility of your service?
Provide an API for async request or make the request?
If it provides API, it should not be loaded async.
If too make the request, it should be a method of the service, not the service itself. A service is the interface to your request, not the request!
Rarely you may need to get logic back from your server, but again, you have to separate concerns:
Get the logic (e.g. Angular expression as a string) from server.
Parse into a function performing the logic (can be done with Angular $parse service).
Inject your logic function wherever you need to use it.

Angular.js multiple rest calls scope issue

i'm a biginner when it comes to Angular.js, and i have a problem with $scope not getting additional value from one of two $resource rest calls. Here's my code:
controller: function ($scope, $modalInstance, $route) {
$scope.server = {}
$scope.submit = function () {
//AddNewServer is a $resource service
AddNewServer.post($.param({
'name': $scope.server.name,
'ip': $scope.server.ip,
'port': $scope.server.port
}));
//ServerStats is a $resource service
ServerStats.postServerStats(function success(data) {
$scope.server.bytesIn = data.returnValue.bytesIn
}, function err(err) {
console.log("Error: " + err)
})
$modalInstance.dismiss('cancel');
$route.reload()
//BELLOW LOG RETURNS Object {name: "asd", ip: "asd", port: 2} NO bytesIn
console.log($scope.server)
}
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
$route.reload()
};
}
Question is how do i add bytesIn from my other service call into my server object? I'm sure it a pretty obvious thing but i'm still in learning phase. Thanks in advance.
Your postServerStats() call is asynchronous, so it's likely that your success function isn't being called before the console.log($scope.server) statement.
Put console.log($scope.server) in your success function, after you assign $scope.server.bytesIn.
Perhaps you mean to do more work in your postServerStats() callback?
Or better yet, look into angular promises

Categories