I'm new to AngularJS. I'm currently looking at the $timeout service. I understand that it's like a wrapper for the setTimeout function. The documentation says that it provides exception handling. In addition, the documentation says I can cancel and flush a timeout.
Can someone please explain to me when an exception would happen with a timeout? I also don't understand why I need to flush a timeout. I would love an explanation or maybe a jsfiddle. For the life of me, I can't figure out why or even how to use these additional features.
Update:
When I attempt to run the stop function, the catch handler associated with myTimer get's thrown. Here is my code:
var myTimer = null;
$scope.hasStarted = false;
$scope.start = function () {
if ($scope.hasStarted === false) {
$scope.isTimerActive = true;
myTimer = $timeout(function () { $scope.isTimerActive = false; }, 5000);
myTimer.catch(function (err) {
alert("An error happened with the clock.");
});
}
}
$scope.stopClock = function () {
$timeout.cancel(myTimer);
$scope.isClockActive = false;
}
Thank you!
$timeout is most awesome indeed.
Exception handling
$timeout returns a promise which can have an error state. For example
var timePromise = $timeout(function(){
throw new Error('I messed up');
}, 10000);
timePromise.catch(function(err){
// do something with the error
});
read all about promises here.
Cancel
Canceling a $timeout is easy. Instead of using clearTimeout you pass the promise back.
var timePromise = $timeout(function(){
// do thing
}, 23432);
// wait I didn't mean it!
$timeout.cancel(timePromise);
Flush
Flush is most useful for unit testing, ultimately it fires any outstanding callbacks.
$timeout(function(){
console.log('$timeout flush');
}, 222);
$timeout(function(){
console.log('rocks my face');
}, 234232);
$timeout.flush(); // both console logs will fire right away!
or this file:
var itsDone = false;
$timeout(function(){
itsDone = true;
}, 5000);
with this test:
// old no flush way (async)
it('should be done', function(done){
expect(isDone).to.be.false;
setTimeout(function(){
expect(isDone).to.be.true;
done();
}, 5001);
});
// now with flush
it('should be done', function(){
expect(isDone).to.be.false;
$timeout.flush();
expect(isDone).to.be.true;
});
Related
Trying to write a jasmine test for the below code...
refreshCacheIfNewVersionIsAvailable();
//Check if a new cache is available on page load and reload the page to refresh app cache to the newer version of files
function refreshCacheIfNewVersionIsAvailable() {
$window.addEventListener('load', function (e) {
$window.applicationCache.addEventListener('updateready', function (e) {
if ($window.applicationCache.status == window.applicationCache.UPDATEREADY) {
// Manifest changed. Now Browser downloadeds a new app cache.
alert(textService.versioning.newVersionMessage);
$window.location.reload(true);
} else {
// Manifest didn't change. Nothing new to server.
}
}, false);
}, false);
}
Your challenge
I assume the challenge you are facing is that you are unable to see how to test the code in the callback functions. You just have to realize that you have access to the callback function when you spy on addEventListener, after the spy is executed in your service under test (refreshCacheIfNewVersionIsAvailable). Since you can get a reference to it, you can execute it, just as if it was the function you were testing.
Sample solution
The following is untested, written off the top of my head, but something along the lines of what I would expect to write if I had to test that code.
describe('refreshCacheIfNewVersionIsAvailable()', function() {
beforeEach(function() {
spyOn($window, 'addEventListener');
});
it('should register a load event handler on the window', function() {
refreshCacheIfNewVersionIsAvailable();
expect($window.addEventListener.calls.count()).toBe(1);
var args = $window.addEventListener.calls.argsFor(0);
expect(args.length).toBe(3);
expect(args[0]).toBe('load');
expect(typeof args[1]).toBe('function');
expect(args[2]).toBe(false);
});
describe('load event', function() {
var loadFunction;
beforeEach(function() {
refreshCacheIfNewVersionIsAvailable();
var args = $window.addEventListener.calls.argsFor(0);
loadFunction = args[1];
spyOn($window.applicationCache, 'addEventListener');
});
it('should register an updateready event handler in the window application cache', function() {
loadFunction();
expect($window.applicationCache.addEventListener.calls.count()).toBe(1);
var args = $window.applicationCache.addEventListener.calls.argsFor(0);
expect(args.length).toBe(3);
expect(args[0]).toBe('updateReady');
expect(typeof args[1]).toBe('function');
expect(args[2]).toBe(false);
});
describe('updateready event', function() {
var updateReadyFunction;
beforeEach(function() {
loadFunction();
var args = $window.applicationCache.addEventListener.calls.argsFor(0);
updateReadyFunction = args[1];
});
it('should reload the window if the status is UPDATEREADY', function() {
// You get the point
});
});
});
});
I'm trying to test a function in my controller that first watches for a dictionary to be loaded before it takes any action.
The problem i am getting is that my test fails because the watch doesn't appear to run.
My Controller Function
function setPageTitle(title) {
$rootScope.$watch('dictionary', function(dictionary) {
if (dictionary) {
if ($location.$$path != '/dashboard') {
$rootScope.pageTitle = $rootScope.dictionary.pageTitles[title] || $rootScope.dictionary.pageTitles.dashboard || 'Dashboard';
} else {
$rootScope.pageTitle = $rootScope.dictionary.pageTitles.dashboard || 'Dashboard';
}
}
});
}
My Test...
describe('AppController function', function() {
var rootScope, scope, $location, $window, controller, createController, cacheFactory, toastr;
beforeEach(module('mockedDashboard'));
beforeEach(inject(function(_$rootScope_, $controller, _$location_, _$window_, _toastr_, _$timeout_) {
$location = _$location_;
$window = _$window_;
$timeout = _$timeout_;
scope = _$rootScope_.$new();
rootScope = _$rootScope_;
toastr = _toastr_;
createController = function() {
return $controller('AppController', {
'$scope': scope
});
};
controller = createController();
}));
// We are using CacheFactory in this project, when running multiple tests on the controller
// we need to destroy the cache for each test as the controller is initialized for each test.
afterEach(inject(function(_CacheFactory_) {
cacheFactory = _CacheFactory_;
cacheFactory.destroy('defaultCache');
}));
describe('setPageTitle()', function() {
it('should update the $rootScope.pageTitle', function() {
scope.setPageTitle('logIn');
scope.$apply();
expect(rootScope.pageTitle).toBe('LOG IN');
});
});
});
the failure message
Expected undefined to be 'LOG IN'
rootScope.pageTitle is never set because the watch doesn't run when the test calls the function. How can i get around this?
I tried scope.$apply() which i read should trigger the $watch, but it still doesn't work.
EDIT
I tried using the done function, however the test still fails because rootScope.pageTitle still appears to remain undefined. 3000ms should be ample time for this to work, usually this is done in less that 500ms. (i also know that the code works because this test is being written too late)
describe('setPageTitle()', function() {
it('should update the $rootScope.pageTitle', function(done) {
scope.setPageTitle('logIn');
scope.$apply();
setTimeout(function() {
// console.log('dictionary: ' + rootScope.dictionary.pageTitles.logIn);
console.log('rootScope.pageTitle: ' + rootScope.pageTitle);
expect(rootScope.pageTitle).toBe('LOG IN');
// expect(true).toBe(false);
done();
}, 3000);
});
});
Well, it is pretty obvious - your test is failing because the code ... is not running.
$rootScope.$watch('dictionary', function(dictionary) {
if (dictionary) {
...
}
});
Most probably the problem is in $rootScope.dictionary being undefined due to not being explicitly set for testing purposes.
Try this
describe('setPageTitle()', function() {
it('should update the $rootScope.pageTitle', function() {
scope.setPageTitle('logIn');
rootScope.dictionary = {
'whatever you': 'need here'
};
scope.$apply();
expect(rootScope.pageTitle).toBe('LOG IN');
});
});
And by the way - there is absolutely no need in using async test case, as $digest will be invoked syncronously at scope.$apply(); line.
It looks like your test is a synchronous test, and therefore Karma thinks it's done as soon as the function returns. Unfortunately, this isn't actually the case since you need to wait around for the affects of the $apply to occur, which takes another pass through the event loop. You'll want to turn your test into an asynchronous test by adding a done parameter and calling it somehow after your watch has been triggered.
it('description', function(done) {
// do your setup things here
scope.$apply();
setTimeout(function() {
// make your assertions here
done()
}, 25);
})
Of course, using setTimeout is a pretty ugly way to wait the necessary time, and if there's some other event you can watch for, it would be more elegant.
Few small changes done on your test case, you can give a try for this,
describe('setPageTitle()', function() {
it('should update the $rootScope.pageTitle', function() {
rootScope.dictionary = "someValue"; // As you were watching this object assign some value and then apply $digest
// Instead of $apply use $digest with rootscope
rootscope.$digest();
expect(rootScope.pageTitle).toBe('LOG IN');
});
});
I'm trying to write tests for a method that returns an angular promise ($q library).
I'm at a loss. I'm running tests using Karma, and I need to figure out how to confirm that the AccountSearchResult.validate() function returns a promise, confirm whether the promise was rejected or not, and inspect the object that is returned with the promise.
For example, the method being tested has the following (simplified):
.factory('AccountSearchResult', ['$q',
function($q) {
return {
validate: function(result) {
if (!result.accountFound) {
return $q.reject({
message: "That account or userID was not found"
});
}
else {
return $q.when(result);
}
}
};
}]);
I thought I could write a test like this:
it("it should return an object with a message property", function () {
promise = AccountSearchResult.validate({accountFound:false});
expect(promise).to.eventually.have.property("message"); // PASSES
});
That passes, but so does this (erroneously):
it("it should return an object with a message property", function () {
promise = AccountSearchResult.validate({accountFound:false});
expect(promise).to.eventually.have.property("I_DONT_EXIST"); // PASSES, should fail
});
I am trying to use the chai-as-promised 'eventually', but all my tests pass with false positives:
it("it should return an object", function () {
promise = AccountSearchResult.validate();
expect(promise).to.eventually.be.an('astronaut');
});
will pass. In looking at docs and SO questions, I have seen examples such as:
expect(promise).to.eventually.to.equal('something');
return promise.should.eventually.equal('something');
expect(promise).to.eventually.to.equal('something', "some message about expectation.");
expect(promise).to.eventually.to.equal('something').notify(done);
return assert.becomes(promise, "something", "message about assertion");
wrapping expectation in runs() block
wrapping expectation in setTimeout()
Using .should gives me Cannot read property 'eventually' of undefined. What am I missing?
#runTarm 's suggestions were both spot on, as it turns out. I believe that the root of the issue is that angular's $q library is tied up with angular's $digest cycle. So while calling $apply works, I believe that the reason it works is because $apply ends up calling $digest anyway. Typically I've thought of $apply() as a way to let angular know about something happening outside its world, and it didn't occur to me that in the context of testing, resolving a $q promise's .then()/.catch() might need to be pushed along before running the expectation, since $q is baked into angular directly. Alas.
I was able to get it working in 3 different ways, one with runs() blocks (and $digest/$apply), and 2 without runs() blocks (and $digest/$apply).
Providing an entire test is probably overkill, but in looking for the answer to this I found myself wishing people had posted how they injected / stubbed / setup services, and different expect syntaxes, so I'll post my entire test.
describe("AppAccountSearchService", function () {
var expect = chai.expect;
var $q,
authorization,
AccountSearchResult,
result,
promise,
authObj,
reasonObj,
$rootScope,
message;
beforeEach(module(
'authorization.services', // a dependency service I need to stub out
'app.account.search.services' // the service module I'm testing
));
beforeEach(inject(function (_$q_, _$rootScope_) {
$q = _$q_; // native angular service
$rootScope = _$rootScope_; // native angular service
}));
beforeEach(inject(function ($injector) {
// found in authorization.services
authObj = $injector.get('authObj');
authorization = $injector.get('authorization');
// found in app.account.search.services
AccountSearchResult = $injector.get('AccountSearchResult');
}));
// authObj set up
beforeEach(inject(function($injector) {
authObj.empAccess = false; // mocking out a specific value on this object
}));
// set up spies/stubs
beforeEach(function () {
sinon.stub(authorization, "isEmployeeAccount").returns(true);
});
describe("AccountSearchResult", function () {
describe("validate", function () {
describe("when the service says the account was not found", function() {
beforeEach(function () {
result = {
accountFound: false,
accountId: null
};
AccountSearchResult.validate(result)
.then(function() {
message = "PROMISE RESOLVED";
})
.catch(function(arg) {
message = "PROMISE REJECTED";
reasonObj = arg;
});
// USING APPLY... this was the 'magic' I needed
$rootScope.$apply();
});
it("should return an object", function () {
expect(reasonObj).to.be.an.object;
});
it("should have entered the 'catch' function", function () {
expect(message).to.equal("PROMISE REJECTED");
});
it("should return an object with a message property", function () {
expect(reasonObj).to.have.property("message");
});
// other tests...
});
describe("when the account ID was falsey", function() {
// example of using runs() blocks.
//Note that the first runs() content could be done in a beforeEach(), like above
it("should not have entered the 'then' function", function () {
// executes everything in this block first.
// $rootScope.apply() pushes promise resolution to the .then/.catch functions
runs(function() {
result = {
accountFound: true,
accountId: null
};
AccountSearchResult.validate(result)
.then(function() {
message = "PROMISE RESOLVED";
})
.catch(function(arg) {
reasonObj = arg;
message = "PROMISE REJECTED";
});
$rootScope.$apply();
});
// now that reasonObj has been populated in prior runs() bock, we can test it in this runs() block.
runs(function() {
expect(reasonObj).to.not.equal("PROMISE RESOLVED");
});
});
// more tests.....
});
describe("when the account is an employee account", function() {
describe("and the user does not have EmployeeAccess", function() {
beforeEach(function () {
result = {
accountFound: true,
accountId: "160515151"
};
AccountSearchResult.validate(result)
.then(function() {
message = "PROMISE RESOLVED";
})
.catch(function(arg) {
message = "PROMISE REJECTED";
reasonObj = arg;
});
// digest also works
$rootScope.$digest();
});
it("should return an object", function () {
expect(reasonObj).to.be.an.object;
});
// more tests ...
});
});
});
});
});
Now that I know the fix, it is obvious from reading the $q docs under the testing section, where it specifically says to call $rootScope.apply(). Since I was able to get it working with both $apply() and $digest(), I suspect that $digest is really what needs to be called, but in keeping with the docs, $apply() is probably 'best practice'.
Decent breakdown on $apply vs $digest.
Finally, the only mystery remaining to me is why the tests were passing by default. I know I was getting to the expectations (they were being run). So why would expect(promise).to.eventually.be.an('astronaut'); succeed? /shrug
Hope that helps. Thanks for the push in the right direction.
I know that calling $digest or $apply manually during a digest cycle will cause a "$digest already in progress" error but I have no idea why I am getting it here.
This is a unit test for a service that wraps $http, the service is simple enough, it just prevents making duplicate calls to the server while ensuring that code that attempts to do the calls still gets the data it expected.
angular.module('services')
.factory('httpService', ['$http', function($http) {
var pendingCalls = {};
var createKey = function(url, data, method) {
return method + url + JSON.stringify(data);
};
var send = function(url, data, method) {
var key = createKey(url, data, method);
if (pendingCalls[key]) {
return pendingCalls[key];
}
var promise = $http({
method: method,
url: url,
data: data
});
pendingCalls[key] = promise;
promise.then(function() {
delete pendingCalls[key];
});
return promise;
};
return {
post: function(url, data) {
return send(url, data, 'POST');
},
get: function(url, data) {
return send(url, data, 'GET');
},
_delete: function(url, data) {
return send(url, data, 'DELETE');
}
};
}]);
The unit-test is also pretty straight forward, it uses $httpBackend to expect the request.
it('does GET requests', function(done) {
$httpBackend.expectGET('/some/random/url').respond('The response');
service.get('/some/random/url').then(function(result) {
expect(result.data).toEqual('The response');
done();
});
$httpBackend.flush();
});
This blows up as sone as done() gets called with a "$digest already in progress" error. I've no idea why. I can solve this by wrapping done() in a timeout like this
setTimeout(function() { done() }, 1);
That means done() will get queued up and run after the $digest is done but while that solves my problem I want to know
Why is Angular in a digest-cycle in the first place?
Why does calling done() trigger this error?
I had the exact same test running green with Jasmine 1.3, this only happened after I upgraded to Jasmine 2.0 and rewrote the test to use the new async-syntax.
$httpBacked.flush() actually starts and completes a $digest() cycle. I spent all day yesterday digging into the source of ngResource and angular-mocks to get to the bottom of this, and still don't fully understand it.
As far as I can tell, the purpose of $httpBackend.flush() is to avoid the async structure above entirely. In other words, the syntax of it('should do something',function(done){}); and $httpBackend.flush() do not play nicely together. The very purpose of .flush() is to push through the pending async callbacks and then return. It is like one big done wrapper around all of your async callbacks.
So if I understood correctly (and it works for me now) the correct method would be to remove the done() processor when using $httpBackend.flush():
it('does GET requests', function() {
$httpBackend.expectGET('/some/random/url').respond('The response');
service.get('/some/random/url').then(function(result) {
expect(result.data).toEqual('The response');
});
$httpBackend.flush();
});
If you add console.log statements, you will find that all of the callbacks consistently happen during the flush() cycle:
it('does GET requests', function() {
$httpBackend.expectGET('/some/random/url').respond('The response');
console.log("pre-get");
service.get('/some/random/url').then(function(result) {
console.log("async callback begin");
expect(result.data).toEqual('The response');
console.log("async callback end");
});
console.log("pre-flush");
$httpBackend.flush();
console.log("post-flush");
});
Then the output will be:
pre-get
pre-flush
async callback begin
async callback end
post-flush
Every time. If you really want to see it, grab the scope and look at scope.$$phase
var scope;
beforeEach(function(){
inject(function($rootScope){
scope = $rootScope;
});
});
it('does GET requests', function() {
$httpBackend.expectGET('/some/random/url').respond('The response');
console.log("pre-get "+scope.$$phase);
service.get('/some/random/url').then(function(result) {
console.log("async callback begin "+scope.$$phase);
expect(result.data).toEqual('The response');
console.log("async callback end "+scope.$$phase);
});
console.log("pre-flush "+scope.$$phase);
$httpBackend.flush();
console.log("post-flush "+scope.$$phase);
});
And you will see the output:
pre-get undefined
pre-flush undefined
async callback begin $digest
async callback end $digest
post-flush undefined
#deitch is right, that $httpBacked.flush() triggers a digest. The problem is that when $httpBackend.verifyNoOutstandingExpectation(); is run after each it is completed it also has a digest. So here's the sequence of events:
you call flush() which triggers a digest
the then() is executed
the done() is executed
verifyNoOutstandingExpectation() is run which triggers a digest, but you are already in one so you get an error.
done() is still important since we need to know that the 'expects' within the then() are even executed. If the then doesn't run then you might now know there were failures. The key is to make sure the digest is complete before firing the done().
it('does GET requests', function(done) {
$httpBackend.expectGET('/some/random/url').respond('The response');
service.get('/some/random/url').then(function(result) {
expect(result.data).toEqual('The response');
setTimeout(done, 0); // run the done() after the current $digest is complete.
});
$httpBackend.flush();
});
Putting done() in a timeout will make it executes immediately after the current digest is complete(). This will ensure that all of the expects that you wanted to run will actually run.
Adding to #deitch's answer. To make the tests more robust you can add a spy before your callback. This should guarantee that your callback actually gets called.
it('does GET requests', function() {
var callback = jasmine.createSpy().and.callFake(function(result) {
expect(result.data).toEqual('The response');
});
$httpBackend.expectGET('/some/random/url').respond('The response');
service.get('/some/random/url').then(callback);
$httpBackend.flush();
expect(callback).toHaveBeenCalled();
});
I'm working with backbone and jasmine and now
trying to test the callCount of 'sync' method, when model saved.
For some strange reason the sync handler continue to handle the sync even after the done variable is true (this is there i planned to stop the test)
I'm a newbie for jasmine so i guess i didn't understand something elementar here...
here is my speck:
describe('Model :: User', function() {
var mockData = { name: 'Foo Bar' };
beforeEach(function() {
var that = this,
done = false;
require(['app/namespace','app/models/UserModel','app/collections/UsersCollection'], function(namespace, UserModel ,UsersCollection) {
that.users = new UsersCollection();
that.user = new UserModel();
done = true;
});
waitsFor(function() {
return done;
}, "Create Models");
});
afterEach(function(){
var done = false,
isDone = function(){ return done; };
this.users.fetch({
success: function(c) {
console.log('after the test calling destory of collection...')
c.each(function(m){
m.destroy();
});
done = true;
}
});
waitsFor(isDone);
done = false;
this.user.destroy({
success: function(){
console.log('after the test calling destory of model...')
done = true;
}
});
waitsFor(isDone);
});
describe('.save()', function() {
it('should call sync when saving', function() {
var done = false,
spy = jasmine.createSpy();
this.user.on('sync', spy);
this.user.on('sync', function(){
console.log('checking spy.callCount-'+spy.callCount);
//------------------------------------------------
if(!done)//why i need this if ?!
expect(spy.callCount).toEqual(1);
done = true;
}, this);
this.user.save(mockData);
waitsFor(function() { return done; });
});
});
});
The test workiing correctly only if i add "if(!done)" condition before expect statement,
otherwise it continue to count sync calls that caused by destroy after the test...
Thanks forwards
There are some of issues with this test. First of all, you dont need to test that the sync event is fired when saving your model cause this is provided by another framework, which is hopefully tested.
Second you should use the fake server of SinonJs to not mess with async calls. With sinon your request will be called immediately, which means you dont need waitsFor. Also assertions in callback seems a bit odd.
this.server = sinon.fakeServer.create();
server.respondWith({data: 'someData'})
server.autoRespond = true; //so when the request start the fake server will immediately call the success callback
var spy = jasmine.createSpy();
this.user.on('sync', spy);
this.user.save(mockData);
expect(spy.callCount).toEqual(1);