-
Notifications
You must be signed in to change notification settings - Fork 6.8k
feat(cdk/testing): add method to wait for async tasks outside angular to complete #17408
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
Merged
mmalerba
merged 1 commit into
angular:master
from
devversion:fix/stabilize-cdk-testing-testbed-harness
Oct 21, 2019
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ ts_library( | |
"//src/cdk/keycodes", | ||
"//src/cdk/testing", | ||
"@npm//@angular/core", | ||
"@npm//rxjs", | ||
], | ||
) | ||
|
||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
/* | ||
* Type definitions of the "ProxyZone" implementation provided by the | ||
* ZoneJS testing bundles. These types are not part of the default ZoneJS | ||
* typings, so we need to replicate them here. Usually they would go into | ||
* the "zone-types.d.ts" file where other types are brought in as well, but | ||
* since internally in Google, the original zone.js types will be used, there | ||
* needs to be a separation of types which are replicated or the ones that can | ||
* be pulled in from the original type definitions. | ||
*/ | ||
|
||
import {HasTaskState, Zone, ZoneDelegate} from './zone-types'; | ||
|
||
export interface ProxyZoneStatic { | ||
assertPresent: () => ProxyZone; | ||
get(): ProxyZone; | ||
} | ||
|
||
export interface ProxyZone { | ||
lastTaskState: HasTaskState|null; | ||
setDelegate(spec: ZoneSpec): void; | ||
getDelegate(): ZoneSpec; | ||
onHasTask(delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState): void; | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {BehaviorSubject, Observable} from 'rxjs'; | ||
import {ProxyZone, ProxyZoneStatic} from './proxy-zone-types'; | ||
import {HasTaskState, Zone, ZoneDelegate} from './zone-types'; | ||
|
||
/** Current state of the intercepted zone. */ | ||
export interface TaskState { | ||
/** Whether the zone is stable (i.e. no microtasks and macrotasks). */ | ||
stable: boolean; | ||
} | ||
|
||
/** Unique symbol that is used to patch a property to a proxy zone. */ | ||
const stateObservableSymbol = Symbol('ProxyZone_PATCHED#stateObservable'); | ||
|
||
/** Type that describes a potentially patched proxy zone instance. */ | ||
type PatchedProxyZone = ProxyZone & { | ||
[stateObservableSymbol]: undefined|Observable<TaskState>; | ||
}; | ||
|
||
/** | ||
* Interceptor that can be set up in a `ProxyZone` instance. The interceptor | ||
* will keep track of the task state and emit whenever the state changes. | ||
* | ||
* This serves as a workaround for https://github.com/angular/angular/issues/32896. | ||
*/ | ||
export class TaskStateZoneInterceptor { | ||
/** Subject that can be used to emit a new state change. */ | ||
private _stateSubject: BehaviorSubject<TaskState> = new BehaviorSubject<TaskState>( | ||
this._lastState ? this._getTaskStateFromInternalZoneState(this._lastState) : {stable: true}); | ||
|
||
/** Public observable that emits whenever the task state changes. */ | ||
readonly state: Observable<TaskState> = this._stateSubject.asObservable(); | ||
|
||
constructor(private _lastState: HasTaskState|null) {} | ||
|
||
/** This will be called whenever the task state changes in the intercepted zone. */ | ||
onHasTask(delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) { | ||
if (current === target) { | ||
this._stateSubject.next(this._getTaskStateFromInternalZoneState(hasTaskState)); | ||
} | ||
} | ||
|
||
/** Gets the task state from the internal ZoneJS task state. */ | ||
private _getTaskStateFromInternalZoneState(state: HasTaskState): TaskState { | ||
return {stable: !state.macroTask && !state.microTask}; | ||
} | ||
|
||
/** | ||
* Sets up the custom task state Zone interceptor in the `ProxyZone`. Throws if | ||
* no `ProxyZone` could be found. | ||
* @returns an observable that emits whenever the task state changes. | ||
*/ | ||
static setup(): Observable<TaskState> { | ||
if (Zone === undefined) { | ||
throw Error('Could not find ZoneJS. For test harnesses running in TestBed, ' + | ||
'ZoneJS needs to be installed.'); | ||
} | ||
|
||
// tslint:disable-next-line:variable-name | ||
const ProxyZoneSpec = (Zone as any)['ProxyZoneSpec'] as ProxyZoneStatic|undefined; | ||
|
||
// If there is no "ProxyZoneSpec" installed, we throw an error and recommend | ||
// setting up the proxy zone by pulling in the testing bundle. | ||
if (!ProxyZoneSpec) { | ||
throw Error( | ||
'ProxyZoneSpec is needed for the test harnesses but could not be found. ' + | ||
'Please make sure that your environment includes zone.js/dist/zone-testing.js'); | ||
} | ||
|
||
// Ensure that there is a proxy zone instance set up, and get | ||
// a reference to the instance if present. | ||
const zoneSpec = ProxyZoneSpec.assertPresent() as PatchedProxyZone; | ||
|
||
// If there already is a delegate registered in the proxy zone, and it | ||
// is type of the custom task state interceptor, we just use that state | ||
// observable. This allows us to only intercept Zone once per test | ||
// (similar to how `fakeAsync` or `async` work). | ||
if (zoneSpec[stateObservableSymbol]) { | ||
return zoneSpec[stateObservableSymbol]!; | ||
} | ||
|
||
// Since we intercept on environment creation and the fixture has been | ||
// created before, we might have missed tasks scheduled before. Fortunately | ||
// the proxy zone keeps track of the previous task state, so we can just pass | ||
// this as initial state to the task zone interceptor. | ||
const interceptor = new TaskStateZoneInterceptor(zoneSpec.lastTaskState); | ||
const zoneSpecOnHasTask = zoneSpec.onHasTask; | ||
|
||
// We setup the task state interceptor in the `ProxyZone`. Note that we cannot register | ||
// the interceptor as a new proxy zone delegate because it would mean that other zone | ||
// delegates (e.g. `FakeAsyncTestZone` or `AsyncTestZone`) can accidentally overwrite/disable | ||
// our interceptor. Since we just intend to monitor the task state of the proxy zone, it is | ||
// sufficient to just patch the proxy zone. This also avoids that we interfere with the task | ||
// queue scheduling logic. | ||
zoneSpec.onHasTask = function() { | ||
zoneSpecOnHasTask.apply(zoneSpec, arguments); | ||
interceptor.onHasTask.apply(interceptor, arguments); | ||
}; | ||
|
||
return zoneSpec[stateObservableSymbol] = interceptor.state; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC All Rights Reserved. | ||
* | ||
* Use of this source code is governed by an MIT-style license that can be | ||
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
/* | ||
* Type definitions for "zone.js". We cannot reference the official types | ||
* using a triple-slash types directive because the types would bring in | ||
* the NodeJS types into the compilation unit. This would cause unexpected | ||
* type checking failures. We just create minimal type definitions for Zone | ||
* here and use these for our interceptor logic. | ||
*/ | ||
|
||
declare global { | ||
// tslint:disable-next-line:variable-name | ||
const Zone: {current: any}|undefined; | ||
} | ||
|
||
export type Zone = Object; | ||
export type ZoneDelegate = Object; | ||
export type HasTaskState = {microTask: boolean, macroTask: boolean}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.