8
8
9
9
import { HarnessEnvironment } from '@angular/cdk/testing' ;
10
10
import { ComponentFixture } from '@angular/core/testing' ;
11
+ import { Observable } from 'rxjs' ;
12
+ import { takeWhile } from 'rxjs/operators' ;
11
13
import { ComponentHarness , ComponentHarnessConstructor , HarnessLoader } from '../component-harness' ;
12
14
import { TestElement } from '../test-element' ;
15
+ import { TaskState , TaskStateZoneInterceptor } from './task-state-zone-interceptor' ;
13
16
import { UnitTestElement } from './unit-test-element' ;
17
+ import { ProxyZoneType } from './zone-types' ;
18
+
19
+ // Ensures that TypeScript knows that the global "zone.js" types
20
+ // are used in this source file.
21
+ /// <reference types="zone.js" />
14
22
15
23
/** A `HarnessEnvironment` implementation for Angular's Testbed. */
16
24
export class TestbedHarnessEnvironment extends HarnessEnvironment < Element > {
17
25
private _destroyed = false ;
18
26
27
+ /** Observable that emits whenever the test task state changes. */
28
+ private _taskState : Observable < TaskState > ;
29
+
19
30
protected constructor ( rawRootElement : Element , private _fixture : ComponentFixture < unknown > ) {
20
31
super ( rawRootElement ) ;
32
+ this . _taskState = this . _setupZoneTaskStateInterceptor ( ) ;
21
33
_fixture . componentRef . onDestroy ( ( ) => this . _destroyed = true ) ;
22
34
}
23
35
@@ -53,7 +65,12 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
53
65
}
54
66
55
67
this . _fixture . detectChanges ( ) ;
56
- await this . _fixture . whenStable ( ) ;
68
+
69
+ // Wait until the task queue has been drained and the zone is stable. Note that
70
+ // we cannot rely on "fixture.whenStable" since it does not catch tasks scheduled
71
+ // outside of the Angular zone. For test harnesses, we want to ensure that the
72
+ // app is fully stabilized and therefore need to use our own zone interceptor.
73
+ await this . _taskState . pipe ( takeWhile ( state => ! state . stable ) ) . toPromise ( ) ;
57
74
}
58
75
59
76
protected getDocumentRoot ( ) : Element {
@@ -72,4 +89,53 @@ export class TestbedHarnessEnvironment extends HarnessEnvironment<Element> {
72
89
await this . forceStabilize ( ) ;
73
90
return Array . from ( this . rawRootElement . querySelectorAll ( selector ) ) ;
74
91
}
92
+
93
+ /**
94
+ * Sets up the custom task state ZoneJS interceptor.
95
+ * @returns an observable that emits whenever the task state changes.
96
+ */
97
+ private _setupZoneTaskStateInterceptor ( ) : Observable < TaskState > {
98
+ if ( Zone === undefined ) {
99
+ throw Error ( 'Could not find ZoneJS. For test harnesses running in TestBed, ' +
100
+ 'ZoneJS needs to be installed.' ) ;
101
+ }
102
+
103
+ // tslint:disable-next-line:variable-name
104
+ const ProxyZoneSpec = ( Zone as any ) [ 'ProxyZoneSpec' ] as ProxyZoneType | undefined ;
105
+
106
+ // If there is no "ProxyZoneSpec" installed, we throw an error and recommend
107
+ // setting up the proxy zone by pulling in the testing bundle.
108
+ if ( ! ProxyZoneSpec ) {
109
+ throw Error (
110
+ 'ProxyZoneSpec is needed for the test harnesses but could not be found. ' +
111
+ 'Please make sure that your environment includes zone.js/dist/zone-testing.js' ) ;
112
+ }
113
+
114
+ // Ensure that there is a proxy zone instance set up.
115
+ ProxyZoneSpec . assertPresent ( ) ;
116
+
117
+ // Get the instance of the proxy zone spec.
118
+ const zoneSpec = ProxyZoneSpec . get ( ) ;
119
+ const currentDelegate = zoneSpec . getDelegate ( ) ;
120
+
121
+ // If there already is a delegate registered in the proxy zone, and it
122
+ // is type of the custom task state interceptor, we just use that state
123
+ // observable. This allows us to only intercept Zone once per test
124
+ // (similar to how `fakeAsync` or `async` work).
125
+ if ( currentDelegate && currentDelegate instanceof TaskStateZoneInterceptor ) {
126
+ return currentDelegate . state ;
127
+ }
128
+
129
+ // Since we intercept on environment creation and the fixture has been
130
+ // created before, we might have missed tasks scheduled before. Fortunately
131
+ // the proxy zone keeps track of the previous task state, so we can just pass
132
+ // this as initial state to the task zone interceptor.
133
+ const newZone = new TaskStateZoneInterceptor ( zoneSpec . lastTaskState ) ;
134
+
135
+ // Set the new delegate. This means that the "ProxyZone" will delegate
136
+ // all Zone information to the custom task zone interceptor.
137
+ zoneSpec . setDelegate ( newZone ) ;
138
+
139
+ return newZone . state ;
140
+ }
75
141
}
0 commit comments