-
Notifications
You must be signed in to change notification settings - Fork 6.8k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cdk/testing): add method to wait for async tasks outside… (#17408)
Adds a new method to the `ComponentHarness` base class that can be used to wait for async tasks to complete. This is useful for harness authors as sometimes the component schedules tasks outside of the Angular zone which therefore won't be captured by `whenStable`. These tasks can be relevant for the harness logic though, so there should be an option for harness authors to await _all_ async tasks (not only the ones captured in the ng zone). e.g. the slider re-adjusts when the directionality changes. This re-rendering happens in the next tick. To make sure that `setValue` works when the directionality changed, we need to wait for the async tasks to complete, so that the slider completed re-rendering and the value can be changed through a mouse click.
- Loading branch information
1 parent
5e10833
commit c50aa21
Showing
13 changed files
with
267 additions
and
10 deletions.
There are no files selected for viewing
This file contains 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 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 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 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 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 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 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 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 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 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 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 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.