问题
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.
回答1:
$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
回答2:
@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.
回答3:
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();
});
来源:https://stackoverflow.com/questions/24341544/getting-digest-already-in-progress-in-async-test-with-jasmine-2-0