-
Notifications
You must be signed in to change notification settings - Fork 26.2k
feat(core): add task tracking to Testability #16863
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
4e0a2c9
to
be84dc9
Compare
scheduleMicroTask(fn); | ||
}); | ||
} | ||
import {fakeAsync, tick} from '@angular/core/testing'; |
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.
Thanks for cleaning up the old AsyncTestCompleter business.
})); | ||
|
||
it('should not fire whenstable callbacks synchronously if pending count is 0', () => { | ||
testability.whenStable(execute); | ||
expect(execute).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('should not call whenstable callbacks when there are pending counts', | ||
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => { | ||
it('should not call whenstable callbacks when there are pending counts', fakeAsync(() => { |
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.
As discussed offline, let's make these twoasync
tests to increase confidence
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.
Done
tick(200); | ||
expect(execute).toHaveBeenCalled(); | ||
const tasks = execute.calls.mostRecent().args[1] as PendingMacrotask[]; | ||
|
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(tasks.length).toEqual(1)
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.
Done
let timeout1Done = false; | ||
let timeout2Done = false; | ||
ngZone.run(() => setTimeout(() => timeout1Done = true, 500)); | ||
testability.whenStable(execute, 1000, execute2); |
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.
Maybe instead of execute2
make a new spy and call it something like updateCallback
?
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.
Done
|
||
interface WaitCallback { | ||
// Needs to be 'any' - setTimeout returns a number according to ES6, but | ||
// on NodeJS it returns a Timer. |
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.
Could it be a number|Timer
then?
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.
The problem is Timer is only defined in the NodeJS typings, which is only brought in for the platform-server tsconfig. I can't figure out a type that builds for both browser and server platforms, since in the browser setTimeout returns a number.
Of course, with Zone.js, setTimeout returns the same thing on both platforms - a Zone task. So the typings are wrong, anyway ¯_(ツ)_/¯
if (this.isStable()) { | ||
// Schedules the call backs in a new frame so that it is always async. | ||
scheduleMicroTask(() => { | ||
while (this._callbacks.length !== 0) { | ||
(this._callbacks.pop() !)(this._didWork); | ||
let cb = (this._callbacks.pop() as WaitCallback); |
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.
Why is casting here necessary? Shouldn't TS know it's an array of WaitCallback
s?
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 accidentally dropped the not null assertion !
, so I needed a cast. I put it back, but I do find the!
a little easy to miss.
* @param timeout Optional. The maximum time to wait for Angular to become stable. If not | ||
* specified, whenStable() will wait forever. | ||
* @param updateCb Optional. If specified, this callback will be invoked whenever the set of | ||
* pending macrotasks changes. If this callback returns true doneCb will not be invoked. |
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.
... and no further updates will be issued.
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.
Done
a5333f8
to
90629ba
Compare
LGTM, cc @vikerman for additional approval. |
fee9b00
to
cf3fc55
Compare
PTAL - I had to make some slight changes to get CI to pass - updated the public API. Also changed it so you only get an error when the task tracking zone isn't available if you pass a timeout or update callback to whenStable() |
LGTM - can we add an end to end test with Protractor for this? |
Added an integration test for whenStable() timeout. |
I believe Vikram said that Igor should take a look at this - ping @IgorMinar |
isStable(): boolean; | ||
whenStable(callback: Function): void; | ||
whenStable(doneCb: DoneCallback, timeout?: number, updateCb?: UpdateCallback): void; |
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.
UpdateCallback does not seem to be exported?
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.
Fixed.
} | ||
|
||
export type DoneCallback = (didWork: boolean, tasks?: PendingMacrotask[]) => void; | ||
export type UpdateCallback = (tasks: PendingMacrotask[]) => boolean; |
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.
UpdateCallback
and DoneCallback
should show up in the API guard. The fact that they don't implies that they are not visible to the developer (ie they are not exported at top level).
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.
Fixed.
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.
How do we expect developers to receive this update?
I suspect that once they update to karma that depends on this API they will get the missing zone spec error, not understanding what that means or what to do about it.
If they read up on the change in some changelog they'll figure out that they need to add the new zone spec.
If they do add these zone spec, would they know how to remove them in production?
And if they are not removed from production builds, do we understand the performance impact of having task tracking enabled all the time?
It seems to me that the PR as is will create a lot of pain for developers not suspecting or understanding this change. Am I missing something?
@@ -15,6 +15,7 @@ require('zone.js/dist/zone-node.js'); | |||
var JasmineRunner = require('jasmine'); | |||
var path = require('path'); | |||
require('zone.js/dist/long-stack-trace-zone.js'); | |||
require('zone.js/dist/task-tracking.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.
doesn't this mean that task tracking must be enabled in all apps for testability to work?
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.
No, the timeout and update callback are optional. If those aren't passed, testability should continue to work as before.
describe('times out with list of tasks', () => { | ||
const URL = '/core/testability/ts/whenStable/'; | ||
|
||
it('', (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.
it what? '' :)
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.
oops, fixed :)
} | ||
|
||
/** | ||
* Wait for angular to be stable with a timeout. If the timeout is hit before Angular becomes |
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.
"for application to be stable.."
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.
Done
*/ | ||
whenStable(doneCb: DoneCallback, timeout?: number, updateCb?: UpdateCallback): void { | ||
if ((timeout || updateCb) && !this.taskTrackingZone) { | ||
throw new Error('Task tracking zone required when using whenStable() with a timeout!'); |
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.
If I were a regular developer, I'd have no idea what to do about this error. :-(
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 tried to make it more clear. Also, this API isn't really intended for end users as much as it is for tool developers, like Protractor. On the Protractor side, we'll try to turn this into a more actionable, understandable error.
@@ -67,12 +92,14 @@ export class Testability implements PublicTestability { | |||
}); | |||
} | |||
|
|||
/** @deprecated pending requests are now tracked with zones */ |
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.
does this mean that the old way of using this api still works? what happens if you have task tracking enabled and you use older protractor that doesn't have the task tracking support yet? Will it just work?
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.
Yes, the old way of using the API should still work without errors. If task tracking is enabled but Protractor isn't passing an update callback, it will just work with the current behavior.
@@ -375,6 +375,9 @@ export interface DoCheck { | |||
ngDoCheck(): void; | |||
} | |||
|
|||
/** @experimental */ | |||
export declare type DoneCallback = (didWork: boolean, tasks?: PendingMacrotask[]) => void; |
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.
Do we have to export these types? Can't we just inline them?
It's weird that someone can do import {DoneCallback} from '@angular/core';
- not only that the name is too ambiguous, I'm not sure if we want to be on the hook to support such api if someone decides to create their implementation.
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.
Yeah. I kind of like having the types for the callbacks available so I know what parameters they need to accept, but it's probably not a good thing to have this in the public API. The Protractor side of this is written in ES5, anyway, so it's not like I'd actually be able to use these types yet.
@@ -698,6 +701,15 @@ export declare const Output: OutputDecorator; | |||
/** @experimental */ | |||
export declare const PACKAGE_ROOT_URL: InjectionToken<string>; | |||
|
|||
/** @experimental */ | |||
export interface PendingMacrotask { |
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.
same as above. we should expose as little as possible. Do we need to provide all of this type info?
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.
Yup, I'll just inline this or make it Function.
@@ -1034,6 +1046,9 @@ export interface TypeDecorator { | |||
export interface TypeProvider extends Type<any> { | |||
} | |||
|
|||
/** @experimental */ | |||
export declare type UpdateCallback = (tasks: PendingMacrotask[]) => boolean; |
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.
inline (as above)?
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.
see above
That should not happen - at least it's not the intention. The error for the missing zone spec is only thrown when someone tries to call The current behavior for Protractor is to pass a done callback and wait forever. The timeout is actually handled by WebDriver's script execution timeout. Moving the timeout and task tracking logic to Testability will allow us to greatly simplify Protractor's browser.waitForAngular
That's a good point, which Misko brought up when we talked this idea over with him. The goal is to fix that with #17390, which adds a way to explicitly set which zone specs get loaded by setting flags in session state. The idea is that if
The goal of this PR is to preserve current behavior, but allow us to add task tracking in future Protractor releases. This will provide better debugging information when Protractor times out waiting for stability, and will also enable us to build a more high-level API for waiting on macrotasks. The change should be transparent. If Protractor attempts to use task tracking but the zone isn't present, it can log a warning (or error or whatever we decide is appropriate). Once #17390 is in we'll be able to continue this work on the Protractor side. We're also planning on doing the same changes to whenStable for AngularJS. |
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 discussed this with @heathkit and we resolved all the remaining issues. lgtm.
caretaker note: can you please run a g3 presubmit on this one before merging? thanks! |
great. the presubmit passed. @kara can you please merge this? thanks |
@heathkit can you please check why the circleci tests are failing? they are the same unit tests that execute on travis, but they run under bazel which also need the new zone spec configuration. you should be able to reproduce this failure locally. |
Allow passing an optional timeout to Testability's whenStable(). If specified, if Angular is not stable before the timeout is hit, the done callback will be invoked with a list of pending macrotasks. Also, allows an optional update callback, which will be invoked whenever the set of pending macrotasks changes. If this callback returns true, the timeout will be cancelled and the done callback will not be invoked. If the optional parameters are not passed, whenStable() will work as it did before, whether or not the task tracking zone spec is available. This change also migrates the Testability unit tests off the deprecated AsyncTestCompleter.
@IgorMinar Thanks, that was it. Updated BUILD.bazel and init_node_spec.ts, circle is passing now. |
awesome! thanks @heathkit |
@kara the previous presubmit is still valid since the fixes that Michael made do not affect g3. |
Allow passing an optional timeout to Testability's whenStable(). If specified, if Angular is not stable before the timeout is hit, the done callback will be invoked with a list of pending macrotasks. Also, allows an optional update callback, which will be invoked whenever the set of pending macrotasks changes. If this callback returns true, the timeout will be cancelled and the done callback will not be invoked. If the optional parameters are not passed, whenStable() will work as it did before, whether or not the task tracking zone spec is available. This change also migrates the Testability unit tests off the deprecated AsyncTestCompleter. PR Close angular#16863
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
Allow passing an optional timeout to Testability's whenStable(). If
specified, if Angular is not stable before the timeout is hit, the
done callback will be invoked with a list of pending macrotasks.
Also, allows an optional update callback, which will be invoked whenever
the set of pending macrotasks changes. If this callback returns true,
the timeout will be cancelled and the done callback will not be invoked.
Implements #15917
Please check if the PR fulfills these requirements
What kind of change does this PR introduce? (check one with "x")
What is the current behavior? (You can also link to an open issue here)
Testability.whenStable()
will wait an indefinite amount of time for Angular to become stable. Protractor relies on a script timeout to know when waiting has timed out (typically longer than 10 seconds or so).What is the new behavior?
Users can now pass a timeout to
whenStable()
. If there are still macrotasks pending when the timeout occurs, the callback will receive a list of pending tasks (provided by the TaskTracking zone spec).whenStable()
also now accepts an optional callback to invoke whenever the state of pending macrotasks changes. If this callback returns true,whenStable()
will cancel the pending done callback, allowing users more control over which macrotasks they wait for.Does this PR introduce a breaking change? (check one with "x")