Why does Angular need wrappers? - javascript

One thing I still don't understand about Angular is...
Why use $window when i could just use the window global object and get the same result? Why use $timeout when I could use setTimeout, etc.
I use this native javascript code sometimes and it works just fine, so why did AngularJS created these wrappers in the first place?

It is integrated into the digest cycle (will trigger the HTML compiler and the DOM refreshes). Also makes the code easier to test because you can mock the $timeout object and test that it was called.
For example with $timeout, you can call $timeout.flush() in your unit tests and it will act as if the timeout waited the appropriate amount of time and trigger the callback. This makes your tests run much faster which is also good for TDD.
Here is a simple async example - assume that asyncThing.method() uses $timeout and $log to output a message
describe('Async test', function () {
var asyncThing, $timeout, $log;
beforeEach(module('async'));
beforeEach(inject(function (_asyncThing_, _$timeout_, _$log_) {
asyncThing = _asyncThing_;
$timeout = _$timeout_;
$log = _$log_;
}));
it('should do some async stuff', function () {
asyncThing.method(some_arguments);
$timeout.flush();
expect($log.info.logs).toContain(['Some output']);
});
});

Related

Angular Unit Test, SpyOn stand-alone function

I am using Jasmine to test an Angular app and would like to test that the getItem() function within my controller is called when the ready() function of the controller is called.
--- Controller ---
var vm = this;
vm.items = [];
$ionicPlatform.ready(ready);
function ready() {
vm.items.push(getItem());
function getItem(){
var item = //do stuff to get item;
console.log('getItem called');
return item;
}
}
--- Spec ---
describe('Controller', function(){
//--- Load app with dependencies and module to test code omitted.
beforeEach(function(){
//How do I spy on getItem() to test that it was called?
//I've tried getItem = jasmine.createSpy()
//I've tried spyOn(window, 'getItem')
}
//--- Initialize the controller and a mock scope code omitted.
beforeEach(function(done){
$ionicPlatform.ready(function(){
done();
});
});
it('getItem function should get called', function(){
expect(getItem).toHaveBeenCalled();
//--- Note, getItem does not get called according to the expect statement,
//--- but does output 'getItem called' to the terminal when running the test.
});
});
Unfortunately, you've come upon a fundamental limit of Javascript unit testing with Jasmine-- you can only spyOn methods that are exposed on some object. If there is a function that is internal to another function, and not exposed in anyway, you cannot test it directly.
However, you do have two options available to you:
Expose the function in a way that it can be spied on (generally as a method of whatever Angular component you are testing).
Test it indirectly.
The first is probably relatively self-evident, but the latter may be a little confusing. Basically, you can't test directly if the getItems function is called, but the function may have downstream methods it calls or values it changes you can test. For instance, you can test that vm.items.push is larger after ready is called, or you can spyOn(console.log) and expect(console.log).toHaveBeenCalledWith('getItem called').
You can find arguments for both approaches on the internet-- I tend to prefer approach two because I don't like doing refactors solely for the purpose of testability, but many will argue that refactoring for testability generally yields better code. That choice is yours to make. Hope this helps!

Manage angularjs controller init with karma tests

Ive seen such questions and they all say 'extract logic out into a service and mock the service'. Simple, except i have as much as possible.
So when the controller inits i have a call to $scope.getWidgets() this method calls widgetService to get a list of widgets for the user, it will then display a toast notification using notifyService. Should widgetService reject its promise or the user have no widgets a call to notifyService is made. This is all done when the controller inits.
$scope.getWidgets = function () {
widgetService.getWidgets()
.then(function (widgets) {
if (widgets.length === 0) {
notifyService.notify('no widgets');
}
$scope.widgets = widgets;
})
.catch(function (e) {
notifyService.notify('oh noes');
});
}
//called at bottom of controller
$scope.getWidgets();
Now all my tests so for have not had need to run any digest cycles, run running promises etc. The method im trying to test calls a service to save a widget. Should it succeed or fail it will again call notifyService to send a notification to the user. I'm testing that the notification is triggered using karma's spyOn and also testing some flags are set correctly. There's no real logic, thats done in the service.
$scope.saveWidget = function (widget) {
widgetService.saveWidget(widget)
.then(function () {
notifyService.notify('all dandy');
//set some scope flags, no logic
})
.catch(function (e) {
notifyService.notify('oh noes');
//set some more scope flags, no logic
});
}
So now i have have to run the digest cycle to trigger my promises. The Trouble is my mock for widgetService.getWidgets() returns a promise. So when i run the digest first it resolves the promise registered in my init, which calls the notify service, which activates my spyon, concluding my test with false data.
Here are my tests, (please forgive the big code block, ive tried to strip it down as much as possible).
describe('tests', function () {
var scope, controlelr, _mockWidgetService, _mockNotifyService;
beforeEach(function () {
//Init the service mocks, angular.noop for methods etc
_mockWidgetService = init();
_mockNotifyService = init();
});
beforeEach(function () {
//Regisetr hooks to pull in my mocked services
angular.mock.module('myModule');
angular.mock.module(function($provide) {
$provide.value('widgetService', _mockWidgetService);
$provide.value('notifyService', _mockNotifyService);
})
});
angular.mock.inject(function ($controller, $rootScope, widgetService, notifyService) {
//create scope and inject services to create controller
scope = $rootScope.$new();
//Trouble is $scope.getWidgets() is called now.
controller = $controller('testController', {
$scope: scope,
widgetService: widgetService,
notifyService: notifyService
});
});
describe('getWidgets', function() {
it('myTest', function () {
//I cannow manipulate mock services moreso if needed just for this test
spyOn(_mockNotifyService, 'notfy');
//Call save widget
scope.saveWidget();
scope.$digest();
expect(_mockNotifyService.notify).toHaveBeenCalledWith(//values);
});
});
});
So as you can see im struggling, i could mock all the mocks required for the init code, but id rather only mock the services i need for that test. Plus i don't like the idea of the init code running every single test potentially causing false errors (it has its own tests).
Any advise or solutions to such use cases would greatly appreciated.
I have found a potential solution that seems to work. I'm not entirely sure its best practice so if there are better options id be very grateful to hear them.
Essentially instead of mocking up my widgetService.getWidgets with a resolved promise before every test i mock it up with a deferred promise which never resolves. Idea being for tests that do not require it the init code will never run as the promise never resolves.
Tests that do require it to run just overwrite the mock and have widgetService.getWidgets return what is required.
That did mean some small changes, the controller had to be instantiated manually for each test (using beforeEach where available) instead of at the top of the very first describe).

Can I access $timeout without using the DI container?

In AngularJS, can I access $timeout without resorting to using the DI container?
Edit:
For those asking for "why". I am using an older version of AngularJS and want to create a utility function that will perform a digest asynchronously.
The intention being that I can place logic inside a promise then for execution after a digest has occurred and the UI has taken into account the model change.
I do not want client code to have to use the injector to use said function.
I wanted something like this:
my-file.js
//...
model.watchedProperty = 'new value';
// Now I want to wait for a digest to occur so that I can ensure the UI is updated before proceeding...
digestAsync(localScope)
.then(function() {
// continue...
});
// ...
digest-async.js
function digestAsync(scope) {
return $timeout(function() { // I don't want to have to use the injector...
scope.$digest();
});
}
You can manually get the injector and then get the $timeout service.
var $injector = angular.injector(['ng']);
var $timeout = $injector.get('$timeout');
If you don't want to inject $timeout you can add $injector as a DI, and in your code you can put this:
$timeout = $injector.get('$timeout');
No, you can not. A lot of angular is itself written in angular, including $timeout. So you can access it in any way you can access any other self-written service - by Dependency Injection
You only need to use $timeout if you want the callback function to be executed inside an Angular digest cycle, and if you pass 0 as the interval then it will be executed in the next digest.
The setTimeout function from JS will execute the callback using the next "thread" cycle. That means that the current thread has to terminate first before the callback can be execute. That doesn't mean that the next thread cycle will also be an Angular digest.
This doesn't matter in your example because you are forcing a $digest, except that you should be using $apply and not $digest.
I think what you are trying to do is create a promise that resolves inside an Angular digest. That's not really the proper use for promises because a digest is not a resource to be resolved.
I think you can skip everything related to the $timeout and just use $apply as it was designed.
localscope.$apply(function(){
// do digest work here
});
That is the same as the following.
$timeout(function(){
// do digest work heere
},0);
Both can be executed outside of Angular, and both will execute the callback during the next digest cycle. $apply will call $digest if needed and it does state this in the documentation.
For times when you don't know if a $digest is in progress.
/**
* Executes the callback during a digest.
*
* #param {angular.IScope|angular.IRootScopeService} $scope
* #param {function()} func
*/
function safeApply($scope, func) {
if ($scope.$$phase) {
func();
} else {
$scope.$apply(function () {
func();
});
}
};
Since you plan to use it outside the app, there is zero chance that you will stumble upon infamous '$digest already in progress' error. Why? Because $digest isn't asynchronous process, more like the opposite of it. All that $digest() function does is calculating current scope state in loop - no promises, no timeouts.
That's exactly what
Don't do if (!$scope.$$phase) $scope.$apply(), it means your
$scope.$apply() isn't high enough in the call stack.
well-known statement refers to. The only time when 'already in progress' will happen is when $digest is triggered during $digest or within $apply, e.g. when outer JS function is used as Angular callback. This indicates poor application design and should be treated accordingly.
Thereby $digest function can be exposed to window
app.run(function ($rootScope) {
window.$digest = angular.bind($rootScope, $rootScope.$digest);
});
And used in synchronous manner. No promises. No timeouts.
model.watchedProperty = 'new value';
$digest();
// 'watchedProperty' watcher has been digested ATM
And I assume that you already know why mixing Angular and outer code like that is considered a bad practice and should be avoided.

Integrating non-Angular code?

I'm developing a Cordova/PhoneGap app, and I'm using the $cordovaPush plugin (wrapped for PushPlugin) to handle push notifications.
The code looks something like this:
var androidConfig = {
"senderID" : "mysenderID",
"ecb" : "onNotification"
}
$cordovaPush.register(androidConfig).then(function(result) {
console.log('Cordova Push Reg Success');
console.log(result);
}, function(error) {
console.log('Cordova push reg error');
console.log(error);
});
The "ecb" function must be defined with window scope, ie:
window.onNotification = function onNotification(e)...
This function handles incoming events. I'd obviously like to handle incoming events in my angular code - how can I integrate the two so that my onNotification function can access my scope/rootScope variables?
Usually, you'll wrap your 3rd party library in a service or a factory, but in the spirit of answering your particular scenario...
Here's one possibility:
angular.module('myApp').
controller('myController', function($scope, $window) {
$window.onNotification = function() {
$scope.apply(function() {
$scope.myVar = ...updates...
});
};
});
A couple of things to notice:
Try to use $window, not window. It's a good habit to get into as it will help you with testability down the line. Because of the internals of Cordova, you might actually need to use window, but I doubt it.
The function that does all of the work is buried inside of $scope.apply. If you forget to do this, then any variables you update will not be reflected in the view until the digest cycle runs again (if ever).
Although I put my example in a controller, you might put yours inside of a handler. If its an angular handler (ng-click, for example), you might think that because the ng-click has an implicit $apply wrapping the callback, your onNotification function is not called at that time, so you still need to do the $apply, as above.
...seriously... don't forget the apply. :-) When I'm debugging people's code, it's the number one reason why external libraries are not working. We all get bit at least once by this.
Define a kind of a mail controller in body and inside that controller use the $window service.
HTML:
<body ng-controller="MainController">
<!-- other markup .-->
</body>
JS:
yourApp.controller("BaseController", ["$scope", "$window", function($scope, $window) {
$window.onNotification = function(e) {
// Use $scope or any Angular stuff
}
}]);

AngularJS and WebSockets: Callback function needs $scope.$apply() to work

I am using AngularJS and a phone web service to make calls through WebSockets.
The web service has several callbacks such as Phone.onIncomingCall
When I use this function to set a $scope variable the view is not updated automatically except if I use $scope.$apply right after.
Phone.onIncomingCall = function(){
$scope.myVar = "newValue";
$scope.$apply(); // only works if I call this line
};
What is the reason for this behaviour (is it expected) and is there a way around using $scope.apply() in each function?
Angular is "unaware" of the update to the scope variable you've made, since you're updating it from outside of the Angular context. From the docs for $apply:
$apply() is used to execute an expression in angular from outside of
the angular framework. (For example from browser DOM events,
setTimeout, XHR or third party libraries). Because we are calling into
the angular framework we need to perform proper scope life cycle of
exception handling, executing watches.
By running $apply on the scope, $digest is called from $rootScope, which will trigger any $watchers registered on $scope.myVar. In this case, if you're using the variable in your view via interpolation, this is where the $watcher was registered from.
It is the expected behavior, angular works like that internally.
I recommend the following:
Phone.onIncomingCall = function () {
$scope.$apply(function () {
$scope.myVar = 'newValue';
});
}

Categories

Resources