-
Notifications
You must be signed in to change notification settings - Fork 27.4k
fix(ngRoute): make route.resolve count as a pending request #14159
Conversation
(Note to self: Related to #13782.) |
@sjelin, this seems reasonable. It needs rebasing and definitely some tests (ideally both unit and e2e). Would you be interested in moving this forward? |
Hi, sorry, yeah - I'll take a look. |
11956ba
to
d4114be
Compare
Update: I'm back on this. Just rebased, I'll add some tests after lunch |
6a73d5b
to
7d94d0a
Compare
@gkalpak I finally added tests! Sorry for not prioritizing this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left some comments. We also miss an e2e test.
(@sjelin, feel free to ping me if you need any help.)
return $q.all(locals).then(function(locals) { | ||
$browser.$$completeOutstandingRequest(function() {}); | ||
return locals; | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be $.all(locals).finally(function() { $browser.$$completeOutstandingRequest(function() {}); });
.
Because now you will never reach 0 outstanding requests once a resolve fails in the app.
@@ -786,7 +788,10 @@ function $RouteProvider() { | |||
if (angular.isDefined(template)) { | |||
locals['$template'] = template; | |||
} | |||
return $q.all(locals); | |||
return $q.all(locals).then(function(locals) { | |||
$browser.$$completeOutstandingRequest(function() {}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use angular.noop
here. Actually, I would initialize noop = angular.noop
(e.g. the same as isArray
) above.
@@ -194,5 +194,33 @@ describe('$$testability', function() { | |||
$$testability.whenStable(callback); | |||
expect(callback).toHaveBeenCalled(); | |||
})); | |||
|
|||
it('should wait for route promises before calling callbacks', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test does not belong here. It should be moved to routeSpec.js
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should also add a test for where a resolve
fails/throws.
7d94d0a
to
3fa796f
Compare
@gkalpak all comments addressed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While taking another look, I realized that ngRoute
has one more async feature (that might not be there when this PR was first created) which is worth taking into account: resolveRedirectTo
Basically, it is possible to decide about a redirection asynchronously (possibly after some delay). It sounds reasonable to take that delay into account too.
Thus, I suggest expanding the outstanding request increment/decrement "scope" to this block, instead of just around resolveLocals
. Maybe something like:
function decrementRequestCount() {
$browser.$$completeOutstandingRequest(noop);
}
...
$browser.$$incOutstandingRequestCount();
nextRoutePromise.
then(getRedirectionData).
then(handlePossibleRedirection).
then(function(keepProcessingRoute) {
return keepProcessingRoute && nextRoutePromise.
then(resolveLocals).
then(function(locals) {
// after route change
if (nextRoute === $route.current) {
if (nextRoute) {
nextRoute.locals = locals;
angular.copy(nextRoute.params, $routeParams);
}
$rootScope.$broadcast('$routeChangeSuccess', nextRoute, lastRoute);
}
});
}).
catch(function(error) {
if (nextRoute === $route.current) {
$rootScope.$broadcast('$routeChangeError', nextRoute, lastRoute, error);
}
}).
finally(decrementRequestCount);
WDYT, @sjelin?
@@ -786,7 +790,9 @@ function $RouteProvider() { | |||
if (angular.isDefined(template)) { | |||
locals['$template'] = template; | |||
} | |||
return $q.all(locals); | |||
return $q.all(locals).finally(function(locals) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
finally
callbacks don't take any arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
woops 😅
expect(element.all(by.tagName('li')).count()).toBe(5); | ||
}); | ||
it('should time out if the promise takes long enough', function() { | ||
// Don't try this at home kids, I'm a protractor dev |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😆
expect(error.message).toContain('Timed out waiting for asynchronous Angular tasks to finish after 0.001 seconds.'); | ||
}); | ||
}); | ||
afterAll(function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't make much difference atm, but I believe it is safer to do this afterEach
spec (just in case someone adds another test after the second it
block).
beforeEach(function() { | ||
loadFixture('ng-route-promise'); | ||
}); | ||
it('should wait for promises in resolve blocks', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add newlines before it
blocks.
<script src="angular.js"></script> | ||
<script src="angular-route.js"></script> | ||
<script src="script.js"></script> | ||
</head> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any particular reason for the <head>
element? Why not just put the scripts in <body>
(for brevity and consistency with other tests)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just habit - I'll remove it
} | ||
} | ||
}). | ||
otherwise({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: otherwise('/')
is shorter 😁
$routeProvider. | ||
when('/', { | ||
template: '<ul><li ng-repeat="letter in letters">{{letter}}</li><ul>', | ||
controller: 'LettersCtrl', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: You can get rid of the controller altogether and use $resolve.letters
directly in the template.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't have to (unless you really want to 😉), but this script could be simply written as:
'use strict';
angular
.module('lettersApp', ['ngRoute'])
.config(function($routeProvider) {
$routeProvider
.when('/', {
template: '<ul><li ng-repeat="letter in $resolve.letters">{{ letter }}</li></ul>',
resolve: {
letters: function($q) {
return $q(function(resolve) { setTimeout(resolve, 1000, ['a', 'b', 'c', 'd', 'e']); });
}
}
// Sigh :( - Missing arrow functions already.
// resolve: {letters: $q => $q(resolve => setTimeout(resolve, 1000, [...]))}
})
.otherwise('/');
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do! An yeah, not having arrow functions is the worse 😢
(Also, the Travis failures seem legit.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of expending the scope of this change to include resolveRedirectTo
, but I'm not sure how to handle the following race condition:
- A route uses
resolveRedirectTo
, causing$$incOutstandingRequestCount
to be called - Protractor queries for stability, getting enqueued by
whenStable
resolveRedirectTo
resolves to a new route, thereby calling$$completeOutstandingRequest
, and thereby calling thewhenStable
callbacks. Protractor will get the signal in a few milliseconds, since it takes time for things to go through the selenium server- The new route uses
resolve
- Protractor, thinking the page is stable, executes some webdriver commands commands even though
resolve
has not yet resolved resolve
resolves, but too late
@@ -786,7 +790,9 @@ function $RouteProvider() { | |||
if (angular.isDefined(template)) { | |||
locals['$template'] = template; | |||
} | |||
return $q.all(locals); | |||
return $q.all(locals).finally(function(locals) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
woops 😅
<script src="angular.js"></script> | ||
<script src="angular-route.js"></script> | ||
<script src="script.js"></script> | ||
</head> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just habit - I'll remove it
$routeProvider. | ||
when('/', { | ||
template: '<ul><li ng-repeat="letter in letters">{{letter}}</li><ul>', | ||
controller: 'LettersCtrl', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will do! An yeah, not having arrow functions is the worse 😢
This doesn't seem to be an issue, because of the relative timing of when things happen. The redirection is correctly taken into account (see the test in #15587). I have incorporated the proposed changes into #15587 (along with a fix in the second test). Feel free cherry-pick the changes into this PR. |
Oh, because javascript is synchronous huh? I get it now |
05b2cc7
to
aa05cde
Compare
$$testability.whenStable(callback); | ||
expect(callback).not.toHaveBeenCalled(); | ||
|
||
// ngRoute code is wrapped in a $browser.defer call (via $rootScope.evalAsync), which in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@gkalpak this comment is the result of my investigation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where does the $rootScope.$evalAsync()
come from?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Answering my own question: It comes from $location
(see here).
So, you were spot on 👍 (which is a good thing, because it confirms that the feature works out-of-the-box outside of unit tests).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll mention $location
too 😄
aa05cde
to
785c987
Compare
}, function(error) { | ||
var errorRe = new RegExp( | ||
'Timed out waiting for asynchronous Angular tasks to finish after ' + | ||
'(?:1\\.5 seconds|15\\d\\d ?ms)\\.', ''); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know you took this from my PR, but this RegExp is testing more than is necessary (and might give false negatives after a future update). I think it suffixes to have:
expect(error.message).toContain('Timed out waiting for asynchronous Angular tasks to finish');
// Restore old timeout limit | ||
browser.getProcessedConfig().then(function(config) { | ||
return browser.manage().timeouts().setScriptTimeout(config.allScriptsTimeout); | ||
}).then(function() { done(); }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Couldn't this just be .then(done)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably. In some jasmine setups, passing parameters to the done
callback indicates a failed test. This isn't an issue anymore, but I guess it's just a habit of mine now.
$$testability.whenStable(callback); | ||
expect(callback).not.toHaveBeenCalled(); | ||
|
||
// ngRoute code is wrapped in a $browser.defer call (via $rootScope.evalAsync), which in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where does the $rootScope.$evalAsync()
come from?
So, this generally LGTM (now that we have confirmed that the failures where due to unit-test mocks). Ideally, I would apply some more changes (mainly to tests): 927f355 |
So there's good news and bad news. 👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there. 😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that they're okay with their commits being contributed to this project. Please have them confirm that here in the pull request. Note to project maintainer: This is a terminal state, meaning the |
1 similar comment
So there's good news and bad news. 👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there. 😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that they're okay with their commits being contributed to this project. Please have them confirm that here in the pull request. Note to project maintainer: This is a terminal state, meaning the |
3b106ae
to
cb55b32
Compare
@gkalpak I adopted your changes but then made some extremely minor ones of my own: 3b106ae
|
@@ -321,7 +321,7 @@ function Browser(window, document, $log, $sniffer) { | |||
*/ | |||
self.defer = function(fn, delay) { | |||
var timeoutId; | |||
outstandingRequestCount++; | |||
self.$$incOutstandingRequestCount(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this buys us much and is inconsistent with the rest of the file (which either accesses/modifies outstandRequestCount
directly or through (the private) completeOutstandingRequest()
function).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree but I'll revert this change anyway 😛
LGTM (Nit: I don't particularly like the 3rd change 😁) @googlebot, I confirm that I am okay with my commits being contributed to this fine project 😃 |
cb55b32
to
4567b9d
Compare
CLAs look good, thanks! |
1 similar comment
CLAs look good, thanks! |
@gkalpak alright, should be good to go! I don't have permission to actually do the merge though |
Protractor users were having a problem where if they had asynchonous code in a route.resolve variable, Protractor was not waiting for that code to complete before continuing. See angular/protractor#789 (comment) for details Also enhanced ngMock to wait for pending requests before calling callbacks from `$browser.notifyWhenNoOutstandingRequests` Potentially breaking change if someone was relying on route.resolve not blocking `$browser.notifyWhenNoOutstandingRequests`
26d10a0
to
6aca56b
Compare
Awesome! I can take it from here. Thx for bearing with me till the end 😃 |
Protractor users were having a problem where if they had asynchonous code in a `route.resolve` or `route.resolveRedirectTo` variable, Protractor was not waiting for that code to complete before continuing. See angular/protractor#789 (comment) for details. This commit fixes it by ensuring that `$browser#outstandingRequestCount` is properly increased/decreased while `$route` (asynchronously) processes a route. Also, enhanced `ngMock` to wait for pending requests, before calling callbacks from `$browser.notifyWhenNoOutstandingRequests()`. Related to angular/protractor#789. Closes #14159
Protractor users were having a problem where if they had asynchonous code in a `route.resolve` or `route.resolveRedirectTo` variable, Protractor was not waiting for that code to complete before continuing. See angular/protractor#789 (comment) for details. This commit fixes it by ensuring that `$browser#outstandingRequestCount` is properly increased/decreased while `$route` (asynchronously) processes a route. Also, enhanced `ngMock` to wait for pending requests, before calling callbacks from `$browser.notifyWhenNoOutstandingRequests()`. Related to angular/protractor#789. Closes angular#14159
Make angular count pending
resolve
values as pending request, for the purposes of makingwhenStable
more accurate.Currently, if someone uses promises to asynchronously set a
resolve
, this is not counted as a pending request. The result is that callbacks towhenStable
can be invoked before these variables are available. This was causing protractor users problems (see angular/protractor#789 (comment) for details)Waiting for these variables now counts as a pending request.
I guess you could imagine a breaking change where someone wanted a
whenStable
callback to be invoked before the variables resolved.Not sure tests are necessary for such a small change. And since this brings behavior in line with user expectations not sure about the docs either.